Appearance
@polyspec/fsm-npm
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 ./srctext
[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): State | code의 상태. 안전한 정수가 아니거나 상태가 없는 값은 E_UNKNOWN_CODE를 던진다 |
isFinal(state: State): boolean | 상태가 최종 상태인지 |
eventsFrom(state: State): Event[] | 상태에서 edge가 있는 event, 선언 순서 |
target(expected: State, event: Event): State | event가 이르는 상태. 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 |
event | event 위치(1부터). 없으면 undefined |
error instanceof fsm.Error로 검사하고 error.code를 읽는다.
속성이 어떤 code에서 설정되는지는 명세 B-6(docs/spec/fsm.md)의 규칙이며, tests/vectors.mjs의 vector가 이를 단언한다:
| Code | expected | actual | event |
|---|---|---|---|
E_CONFLICT | 설정됨 | 설정됨 | 설정됨 |
E_INVALID_TRANSITION | 설정됨 | undefined | 설정됨 |
E_UNKNOWN_CODE, E_RESERVED_CODE | undefined | undefined | undefined |
메시지는 기계 이름을 담으며, 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이 지금 쓰는 것과 같은지 검사한다.