Skip to content

@polyspec/fsm-npm ​

English

fsm의 npm 소비자(docs/plans/fsm-v1.md, C-2)다. 패키지는 비공개이며 test bed이고 공개하지 않는다. fsm 정의로부터 생성한 npm 코드를 어떻게 쓰는지 보여주고, 그 코드를 conformance vector로 검사한다. 생성 코드는 type 선언(.js와 .d.ts)을 가진 ES module이며 런타임 의존성이 없다.

프로젝트에 생성 ​

fsm.config.json이 store를 지정한 프로젝트 디렉터리를 주고, 패키지의 소스 디렉터리에 쓴다. 생성 파일이 ES module이므로 package.json에 "type": "module"이 필요하다:

sh
fsm-gen gen --project /path/to/order-machines --kind npm --out ./src
text
[fsm-gen] gen: ok

정의를 바꿀 때마다 명령을 다시 실행한다. vector는 fsm-gen vectors --kind npm --out <file>로 자신의 test 파일에 쓴다(test bed는 tests/vectors.mjs에 쓴다).

생성 파일 ​

파일내용
src/fsm.js, src/fsm.d.ts공유 module fsm: 고정된 객체 Code(Conflict: 'E_CONFLICT' 등)와 class Error
src/order.js, src/order.d.ts기계마다 하나인 module: 고정된 객체 State(각 member의 값이 code)와 Event(위치, 1부터), Step 타입, 함수
src/index.js, src/index.d.ts진입점: export * as order from './order.js'와 export * as fsm from './fsm.js'

각 파일은 // Code generated by fsm-gen from <machine>; DO NOT EDIT.로 시작한다.

API ​

진입점은 기계마다 namespace 하나와 공유 fsm namespace를 내보낸다:

js
import { order, fsm } from './src/index.js';

order의 함수(src/order.d.ts):

함수결과
initial(): State초기 상태
code(state: State): number상태의 code
key(state: State)상태의 key, 예: 'draft'
fromCode(value: number): Statecode의 상태. 안전한 정수가 아니거나 상태가 없는 값은 E_UNKNOWN_CODE를 던진다
isFinal(state: State): boolean상태가 최종 상태인지
eventsFrom(state: State): Event[]상태에서 edge가 있는 event, 선언 순서
target(expected: State, event: Event): Stateevent가 이르는 상태. edge가 없으면 E_INVALID_TRANSITION을 던진다
apply(expected: State, event: Event, actual: State): Step한 번 시도의 결과(루트 README의 전이 절 참고)

State.Draft는 1, Event.Next는 1이다. Step은 { to, changed }다.

영속화 예제 examples/persist.mjs는 transaction 안에서 apply를 완전하게 쓰는 예이며, 따라 할 패턴이다(영속화 절). database를 거치지 않는 순수 호출은 다음과 같다:

js
import { order, fsm } from './src/index.js';

const step = order.apply(order.State.Draft, order.Event.Next, order.State.Draft);
console.log(step);
try {
  order.apply(order.State.Draft, order.Event.Next, order.State.Shipped);
} catch (error) {
  console.log(error instanceof fsm.Error, error.code, error.expected, error.actual, error.event);
}

이 program은 이 checkout의 생성 파일을 쓴 임시 소비자 프로젝트("type": "module")에서 실행했다. 출력은 다음과 같다:

text
{ to: 2, changed: true }
true E_CONFLICT 1 3 1

오류 ​

함수는 fsm.Error(src/fsm.js)를 던진다. Error의 하위 class다:

속성값
code'E_CONFLICT', 'E_INVALID_TRANSITION', 'E_UNKNOWN_CODE' 또는 'E_RESERVED_CODE'. 같은 문자열이 fsm.Code의 member(Conflict, InvalidTransition, UnknownCode, ReservedCode)다
message기계 이름을 담은 메시지
expected기대한 상태 code. 없으면 undefined
actual저장된 상태 code. 없으면 undefined
eventevent 위치(1부터). 없으면 undefined

error instanceof fsm.Error로 검사하고 error.code를 읽는다.

속성이 어떤 code에서 설정되는지는 명세 B-6(docs/spec/fsm.md)의 규칙이며, tests/vectors.mjs의 vector가 이를 단언한다:

Codeexpectedactualevent
E_CONFLICT설정됨설정됨설정됨
E_INVALID_TRANSITION설정됨undefined설정됨
E_UNKNOWN_CODE, E_RESERVED_CODEundefinedundefinedundefined

메시지는 기계 이름을 담으며, E_UNKNOWN_CODE에서는 값도 담는다(order: 99 is not a state of order).

영속화 ​

examples/persist.mjs는 SQLite와 polyspec orm 위에서 docs/spec/fsm.md, B-4의 패턴이다. 저장된 상태는 row의 code다. 전이는 한 transaction 안에서 실행한다. row를 잠금(forUpdate) 아래 읽고, event를 호출자가 관찰한 상태에 적용하며, 결과의 changed가 참일 때만 row를 갱신한다:

js
// packages/js/examples/persist.mjs, transition
export async function transition(db, machine, id, expected, event) {
  return db.transaction(async () => {
    const row = await new Item().connect(db).id(id).forUpdate().get();
    const step = machine.apply(expected, event, machine.fromCode(row.getStatus()));
    if (step.changed) await row.setStatus(machine.code(step.to)).update();
    return step;
  });
}

machine은 기계의 생성 module(./src/order.js)이므로, 같은 함수가 모든 기계에 쓰인다. apply의 오류는 transaction 안에서 던져지고 transaction이 되돌아간다. 같은 expected로 다시 시도하면 changed: false가 되어 아무것도 쓰지 않는다. 같은 파일의 open, seed, statusOf는 table을 만들고 row를 읽는다. 모델 Item은 orm-gen이 contracts/schema/persist.dbs로부터 examples/model/에 생성한다.

테스트 ​

checkout에서 실행한다:

sh
make -C packages/js install   # npm ci: the pinned orm release and the TypeScript compiler
make -C packages/js test      # tests/conformance.test.mjs through kit's run-tests
make test-npm                      # the same from the root

실행 결과는 89개 case 중 89개 통과다. tests/conformance.test.mjs는 tests/vectors.mjs의 vector case마다 하나의 test를 case 이름 <feature>__<id>로 실행한다. 영속화 case는 FSM_TEST_DIALECT의 dialect(기본은 SQLite)에서 examples/persist.mjs를 실행한다. tests/persist.test.mjs는 한 row의 두 전이를 동시에 실행하고 deadlock 재시도를 검사한다(make npm-persist-test). 루트의 tests/npm-types는 TypeScript 컴파일러로 .d.ts 파일을 검사한다(make npm-types). 루트의 make gen-check는 생성 파일이 fsm-gen이 지금 쓰는 것과 같은지 검사한다.