small-state 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.ja.md +564 -0
- package/README.md +566 -2
- package/dist/index.cjs +1754 -0
- package/dist/index.d.cts +369 -0
- package/dist/index.d.mts +369 -0
- package/dist/index.mjs +1748 -0
- package/package.json +47 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 qyvvy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.ja.md
ADDED
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
# small-state
|
|
2
|
+
|
|
3
|
+
A state machine inspired by [XState](https://stately.ai/docs/xstate).
|
|
4
|
+
|
|
5
|
+
## 特徴・長所
|
|
6
|
+
|
|
7
|
+
- TypeScript対応
|
|
8
|
+
- 少ない記述量
|
|
9
|
+
- 迷子になりにくい記述
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
npm i small-state
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## FSM(Finite State Machine / 有限ステートマシン)とは
|
|
20
|
+
|
|
21
|
+
有限個の状態と、状態間の遷移を定義するモデル。
|
|
22
|
+
|
|
23
|
+
ある状態のときに、あるイベントが投げられたなら、そのイベントに応じて指定された処理を行い、内部状態を更新し、場合によって別の状態へ遷移する。<br>
|
|
24
|
+
状態と条件が複雑なものを扱うとき、プログラミングミスが減り、見通しがよくなり、威力を発揮する。
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## 例
|
|
28
|
+
|
|
29
|
+
まず `makeMachine` でマシンを定義する。
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { makeMachine } from 'small-state'
|
|
33
|
+
|
|
34
|
+
const loginMachine = makeMachine(
|
|
35
|
+
{
|
|
36
|
+
name: 'login',
|
|
37
|
+
types: {
|
|
38
|
+
context: {} as { failedAttempts: number },
|
|
39
|
+
events: {} as { type: 'LOGIN_FAILED' } | { type: 'LOGIN_SUCCESS' } | { type: 'UNLOCK' },
|
|
40
|
+
},
|
|
41
|
+
initContext: () => ({ failedAttempts: 0 }),
|
|
42
|
+
},
|
|
43
|
+
({ on }) => ({
|
|
44
|
+
idle: [
|
|
45
|
+
on('LOGIN_FAILED', [
|
|
46
|
+
{
|
|
47
|
+
to: 'locked',
|
|
48
|
+
guard: ({ context }) => context.failedAttempts >= 1,
|
|
49
|
+
actions: ({ update, context }) => update({ failedAttempts: context.failedAttempts + 1 }),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
actions: ({ update, context }) => update({ failedAttempts: context.failedAttempts + 1 }),
|
|
53
|
+
},
|
|
54
|
+
]),
|
|
55
|
+
on('LOGIN_SUCCESS', { to: 'loggedIn' }),
|
|
56
|
+
],
|
|
57
|
+
locked: [
|
|
58
|
+
on('UNLOCK', { to: 'idle', actions: ({ update }) => update({ failedAttempts: 0 }) })
|
|
59
|
+
],
|
|
60
|
+
loggedIn: null,
|
|
61
|
+
}),
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
このマシンは idle, locked, loggedIn という3つの状態を持つ。<br>無指定時、マシンの初期状態は先頭に定義されたものになる。つまりこのマシンの場合は idle になる。
|
|
66
|
+
|
|
67
|
+
このマシンを動かすには `makeRuntime` にマシンを渡す。
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { createRuntime, type Runtime } from 'small-state'
|
|
71
|
+
|
|
72
|
+
const logRuntime = (idx: number, { runtimeStatus, stateValue, context }: Runtime<any>) =>
|
|
73
|
+
console.log(idx, { runtimeStatus, stateValue, context })
|
|
74
|
+
|
|
75
|
+
const loginRt = createRuntime(loginMachine)
|
|
76
|
+
logRuntime(0, loginRt)
|
|
77
|
+
/*
|
|
78
|
+
0 {
|
|
79
|
+
runtimeStatus: "notStarted",
|
|
80
|
+
stateValue: undefined,
|
|
81
|
+
context: {
|
|
82
|
+
failedAttempts: 0,
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
*/
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
作成した runtime は .start() しない限り runtimeStatus が "notStarted" のままで停止している。<br>この状態では stateValue が undefined になる。
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
loginRt.start()
|
|
92
|
+
showStatus(1, loginRt)
|
|
93
|
+
/*
|
|
94
|
+
1 {
|
|
95
|
+
runtimeStatus: "active",
|
|
96
|
+
stateValue: "/idle",
|
|
97
|
+
context: {
|
|
98
|
+
failedAttempts: 0,
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
*/
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
start すると runtimeStatus: "active" に変わる。stateValue は現在のステートを指す。現在 "idle" 状態なので、これは "/idle" になる。<br>
|
|
105
|
+
なお、ステートはフルパスで表現されるので先頭に "/" が付く。
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
loginRt.send('LOGIN_FAILED')
|
|
109
|
+
showStatus(2, loginRt)
|
|
110
|
+
/*
|
|
111
|
+
2 {
|
|
112
|
+
runtimeStatus: "active",
|
|
113
|
+
stateValue: "/idle",
|
|
114
|
+
context: {
|
|
115
|
+
failedAttempts: 1,
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
*/
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
LOGIN_FAILED イベントを送ると idle に定義された LOGIN_FAILED の定義配列を上から順に guard を評価し、最初にマッチした要素の actions を実行する。
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
[
|
|
125
|
+
{
|
|
126
|
+
to: 'locked',
|
|
127
|
+
guard: ({ context }) => context.failedAttempts >= 2,
|
|
128
|
+
actions: ({ update, context }) => update({ failedAttempts: context.failedAttempts + 1 }),
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
actions: ({ update, context }) => update({ failedAttempts: context.failedAttempts + 1 }),
|
|
132
|
+
},
|
|
133
|
+
]
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
context の failedAttempts は 0 なので最初の要素の guard にマッチせず2番目の要素にマッチする。guard が設定されていない要素には無条件にマッチする。<br>
|
|
137
|
+
context が更新され failedAttempts が +1 され 1 になる。
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
loginRt.send('LOGIN_FAILED')
|
|
141
|
+
showStatus(3, loginRt)
|
|
142
|
+
loginRt.send('LOGIN_FAILED')
|
|
143
|
+
showStatus(4, loginRt)
|
|
144
|
+
/*
|
|
145
|
+
3 {
|
|
146
|
+
runtimeStatus: "active",
|
|
147
|
+
stateValue: "/idle",
|
|
148
|
+
context: {
|
|
149
|
+
failedAttempts: 2,
|
|
150
|
+
},
|
|
151
|
+
}
|
|
152
|
+
4 {
|
|
153
|
+
runtimeStatus: "active",
|
|
154
|
+
stateValue: "/locked",
|
|
155
|
+
context: {
|
|
156
|
+
failedAttempts: 3,
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
*/
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
さらに2回 LOGIN_FAILED を発生させると failedAttempts が 2 以上になり、 `guard: ({ context }) => context.failedAttempts >= 2` の条件を満たす。`to` は遷移先を表す。`to` が指定されているなら、action が実行された後、その状態に遷移する。この場合 "locked" ステートに遷移するので、 stateValue が "/locked" になる。
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
loginRt.send('LOGIN_FAILED')
|
|
166
|
+
showStatus(5, loginRt)
|
|
167
|
+
/*
|
|
168
|
+
5 {
|
|
169
|
+
runtimeStatus: "active",
|
|
170
|
+
stateValue: "/locked",
|
|
171
|
+
context: {
|
|
172
|
+
failedAttempts: 3,
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
*/
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
locked には LOGIN_FAILED イベントが定義されていないので send しても何も起こらない。
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
loginRt.send('UNLOCK')
|
|
182
|
+
showStatus(6, loginRt)
|
|
183
|
+
/*
|
|
184
|
+
6 {
|
|
185
|
+
runtimeStatus: "active",
|
|
186
|
+
stateValue: "/idle",
|
|
187
|
+
context: {
|
|
188
|
+
failedAttempts: 0,
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
*/
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
UNLOCK イベントを起こすと failedAttempts が 0 に更新され "idle" に遷移する。
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
loginRt.send('LOGIN_SUCCESS')
|
|
198
|
+
showStatus(7, loginRt)
|
|
199
|
+
/*
|
|
200
|
+
7 {
|
|
201
|
+
runtimeStatus: "done",
|
|
202
|
+
stateValue: "/loggedIn",
|
|
203
|
+
context: {
|
|
204
|
+
failedAttempts: 0,
|
|
205
|
+
},
|
|
206
|
+
}
|
|
207
|
+
*/
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
idle 内で LOGIN_SUCCESS を受け取ると、定義に従い "loggedIn" ステートに遷移する。<br>loggedIn は null なのでここでマシンは停止し runtimeStatus は "done" になる。
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
## API
|
|
215
|
+
|
|
216
|
+
### 型
|
|
217
|
+
|
|
218
|
+
- マシン定義
|
|
219
|
+
- `type AnyMachine = Machine<any, any, any, any, any, any, any, any, any>`
|
|
220
|
+
- 任意のマシン
|
|
221
|
+
- `type MContext = Record<string, any>`
|
|
222
|
+
- context。マシン内部に状態として持つことができる値。ちなみに `_` で始まるプロパティ名はマシン内部用となり外部に公開されない。
|
|
223
|
+
- `type SendableEvent = { type: string;[key: string]: any }`
|
|
224
|
+
- 主に外部から runtime.send や sendTo を通じてステートマシンに送られるイベント。
|
|
225
|
+
- `type EmittableEvent = { type: string;[key: string]: any }`
|
|
226
|
+
- ステートマシン内部から外部に向かって emit を通じて送られるイベント。
|
|
227
|
+
- `type ServiceFn<TInput = any> = (input: TInput, emit: Emit<EmittableEvent>, listen: Listen<SendableEvent>, signal: AbortSignal) => unknown`
|
|
228
|
+
- MachineConfig.services や invoke の引数に渡せるサービス関数の型。
|
|
229
|
+
- emit や listen で送出したり受けたりするイベントは親のイベントではなくこの関数独自に定義した型になる。
|
|
230
|
+
- `input: any` - invoke の input オプションから渡された値
|
|
231
|
+
- `emit: (event: EmittableEvent) => void`
|
|
232
|
+
- 子から親に通信するときに使用する関数。この関数の引数に渡したイベントオブジェクトは親からは `on` 関数を利用して購読できる。
|
|
233
|
+
- `listen: (listener: (event: SendableEvent) => void) => void`
|
|
234
|
+
- 親から `sendTo` で送られたイベントを購読するリスナー関数を登録する。
|
|
235
|
+
- `signal: AbortSignal`
|
|
236
|
+
- この関数がもうすでに停止扱いになったかどうかを内部から検知するためのオブジェクト。`signal.aborted` が `ture` のとき、その関数はステートを抜けたなどの理由で停止扱いになっている。
|
|
237
|
+
- `type Emit<EM extends EmittableEvent> = (event: EM) => void`
|
|
238
|
+
- defServiceFn でラップしたサービス関数の第2引数のemit関数に指定して任意の EmittableEvent を型引数として与えることで型レベルでその関数の EmittableEvent の型を親に認知させることができる。
|
|
239
|
+
- `type ListenFn<E extends SendableEvent> = (listener: (event: E) => void) => void`
|
|
240
|
+
- defServiceFn でラップしたサービス関数の第3引数のlisten関数に指定して任意の SendableEvent を型引数として与えることで親から `sendTo` でそのイベントを受け付けることができる。
|
|
241
|
+
- ランタイム
|
|
242
|
+
- `type RuntimeStatus = 'notStarted' | 'active' | 'stopped' | 'done' | 'error'`
|
|
243
|
+
- ランタイムの稼働状態。
|
|
244
|
+
- 'notStarted' - まだ .start() されていない。
|
|
245
|
+
- 'active' - .start() され、稼働している。
|
|
246
|
+
- 'stopped' - .stop() され、停止している。
|
|
247
|
+
- 'done' - 最終状態に入り完了し、正常停止した。
|
|
248
|
+
- 'error' - エラーで停止した。
|
|
249
|
+
- `type Runtime<M extends AnyMachine>`
|
|
250
|
+
- `type Snapshot<M extends AnyMachine>` - Runtime の getSnapshot の項参照。
|
|
251
|
+
|
|
252
|
+
### 補助関数
|
|
253
|
+
|
|
254
|
+
- `isMachine(v: unknown): v is AnyMachine` - 引数がマシンかどうかを判定。
|
|
255
|
+
- `isRuntime(v: unknown): v is Runtime<AnyMachine>` - 引数がランタイムかどうかを判定。
|
|
256
|
+
- `defServiceFn(serviceFn: ServiceFn): ServiceFn`
|
|
257
|
+
- サービス関数を定義する。
|
|
258
|
+
- マシン外部でサービス関数を定義するときか、invoke に直接関数を渡すときに裸の関数だと型推論が崩れてしまうが、この関数でラップすることで型を通すことができる。
|
|
259
|
+
- `waitFor<R extends Runtime<any>>(runtime, cond, options?): Promise<SnapshotFrom<R>>` - 渡した runtime が条件を満たすまで待機する。
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
### メイン関数
|
|
263
|
+
|
|
264
|
+
- `makeMachine(config, states): Machine` - マシンを作る。
|
|
265
|
+
- `makeRuntime(machine, option?): Runtime` - マシンを実際に動かすランタイムを作る。
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
### 予約されているイベントタイプ
|
|
269
|
+
|
|
270
|
+
`':fooBar:'` のように先頭と末尾が `:` で挟まれたイベントタイプはシステムが使う可能性がある。
|
|
271
|
+
イベントタイプに限らず任意につけられる名前について、 `:` に挟まれた名前は将来システムが使う可能性がある。
|
|
272
|
+
以下のイベントタイプ名が予約されている:
|
|
273
|
+
|
|
274
|
+
- 通常ステート内
|
|
275
|
+
- `':always:'` - ステートに遷移し初期処理が終了した後や、コンテキストが更新された後など、スナップショットが更新されるような処理が発生した際に発火する
|
|
276
|
+
- `':childDone:'` - 配下のサブステートやパラレルステートが全て終了したときに発火する
|
|
277
|
+
- invoke 内
|
|
278
|
+
- `{type: ':done:', output: unknown}` - そのサービスが正常終了したときに発火する
|
|
279
|
+
- `{type: ':error:', error: unknown}` - そのサービスが異常狩猟したときに発火する
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
### `makeMachine(config: MachineConfig, defineStates: (tools)=>Record<string, StateDefinition>): Machine`
|
|
283
|
+
|
|
284
|
+
#### makeMachine > `MachineConfig`
|
|
285
|
+
|
|
286
|
+
マシンの基本的な設定をする辞書オブジェクト。以下のプロパティを持つ。
|
|
287
|
+
|
|
288
|
+
- `types?: { context?: C; events?: E; emittable?: EM; input?: I; output?: O }`
|
|
289
|
+
- マシンで使う各要素の型を `{} as ...` の形式で指定する。
|
|
290
|
+
- `context: Record<string, any>` - マシン内外で参照し更新できる任意の値。これらのうち、`_` で始まるプロパティはマシン内部用となり、外部に公開されなくなる。
|
|
291
|
+
- `events: SendableEvent` - 外部からマシンに向かって発行され得るイベント。
|
|
292
|
+
- `emittable: EmittableEvent` - マシンから外部に向かって発行されうるイベント。
|
|
293
|
+
- `input: any` - マシン起動時外部から受け取る値の型。
|
|
294
|
+
- `output: any` - マシン終了時外部に出力する値の型。
|
|
295
|
+
- 型引数でなく `{} as ...` で型を指定する奇妙な方式の理由は、型を一部だけ与えつつ型推論を有効にするためである。
|
|
296
|
+
|
|
297
|
+
- `name?: string`
|
|
298
|
+
- マシン名。ログに使われたり、ランタイム id の一部に使われることがある。
|
|
299
|
+
- `initialState?: string`
|
|
300
|
+
- マシン開始時の初期ステートをステート名で指定する。
|
|
301
|
+
- `initContext?: (input: I) => C`
|
|
302
|
+
- マシン開始時の初期コンテキストを返す関数を指定する。コンテキストを使う場合はほぼ必須である。
|
|
303
|
+
- `output?: (args: CommonArgs) => O`
|
|
304
|
+
- マシンが final の状態になったら実行され、これがマシンの最終出力となる。この値は ランタイムの snapshot.output から参照できる。
|
|
305
|
+
- マシンが子マシンとして invoke されているときには親マシンのイベント `{type: ':done:', output: O}` の output の値として設定される。
|
|
306
|
+
- この関数に渡されるイベントも `{type: ':done:', output: unknown}` である。output には final オプションが出力した値があればそれが設定され、そうでなければ undefined が設定される。
|
|
307
|
+
- `guards?: Record<string, (args: CommonArgs, params: unknown)=>boolean>`
|
|
308
|
+
- 遷移時に各アクションを実行するかどうかの判定に使われる関数を登録する。ここに登録した guard は名前でアクセスできるようになる。
|
|
309
|
+
- 第二引数 params に型を指定すると呼び出された時に任意の値を追加で受け取ることができる。params を設定した guard を呼ぶときには文字列の代わりにキーにガード名、値に params の値か、paramsの値を返す関数を指定したオブジェクト`{[guardName:string]: ParamsValueOrFn}`を代わりに使う。
|
|
310
|
+
- `actions?: Record<string, (arg: {context, c, event, e, input, update, emit}, params: unknown)=>unknown>`
|
|
311
|
+
- 遷移時に実行するアクション関数を登録する。ここに登録した action は名前でアクセスできるようになる。
|
|
312
|
+
- 第二引数 params に型を指定すると呼び出された時に任意の値を追加で受け取ることができる。params を設定した action を呼ぶときには文字列の代わりにキーにアクション名、値に params の値か、paramsの値を返す関数を指定したオブジェクト`{[actionName:string]: ParamsValueOrFn}`を代わりに使う。
|
|
313
|
+
- `services?: Record<string, AnyMachine | ServiceFn>`
|
|
314
|
+
- キーにサービス名、値にマシンか関数を指定する。サービスは invoke で起動させることができる。
|
|
315
|
+
- 非同期関数を指定することもできる。現状、small-state が非同期関数を扱う方法はサービスとして実行する以外に手段がない。
|
|
316
|
+
- サービス関数には4つの引数が渡され、戻り値が output として扱われる。`ServiceFn` の項を参照。
|
|
317
|
+
- マシン定義の外でサービス関数を定義する場合は `defServiceFn` を使うこと。
|
|
318
|
+
|
|
319
|
+
#### makeMachine > `defineStates: (tools: {on, entryHook, exitHook, subStates, parallel, invoke})=> Record<string, StatesDefinition>`
|
|
320
|
+
|
|
321
|
+
引数に補助関数をまとめたオブジェクトを取りステート定義を返す関数。
|
|
322
|
+
ステート定義となる辞書オブジェクトはキーがステート名であり、値は配列か null である。
|
|
323
|
+
|
|
324
|
+
値を null にした場合はそのステートは最終状態であると見なされ、そのステートに入った直後、そのステートがあるステートのサブステートである場合はそのサブステートは終了したと見なされる。サブステートでない場合、そのマシン自体が終了したと見なされる。
|
|
325
|
+
|
|
326
|
+
値の配列にはオブジェクトか補助関数の戻り値を入れることができる。
|
|
327
|
+
オブジェクトを指定する場合、以下のプロパティを設定できる:
|
|
328
|
+
|
|
329
|
+
- `final?: boolean | (args: CommonArgs) => any`
|
|
330
|
+
- この値が真の場合、そのステートは最終状態と見なされる。ステートに `entryHook` など何か他の値も設定したいときに代わりにこれを使うとよい。逆に他の値を設定しないのであれば、ステートに配列の代わりに null を指定することでもそのステートを最終状態にすることができる。
|
|
331
|
+
- 関数を設定した場合、その戻り値は `':done:'` イベントの `output` プロパティに渡される。
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
補助関数は以下のとおりである:
|
|
335
|
+
|
|
336
|
+
- `on(eventType: string, exec: Exec|Exec[])`
|
|
337
|
+
- マシンがあるステートであるときの、 SendableEvent に反応し実行される処理を定義する。exec に配列が指定されている場合、guard条件に最初にマッチしたアクションだけが実行される。
|
|
338
|
+
- `eventType: string`
|
|
339
|
+
- このイベントが send されたときのアクションを定義する。特別なイベント名として `':always:'` `':childDone:'` がある。
|
|
340
|
+
- `':always:'` - そのステートに遷移した直後やコンテストを更新した直後など、1つの処理のループが終わった直後に実行される。`guard` を設定して実行に条件をつけるか、 `to` を設定して必ずその状態を抜けるかしないと永久に実行されるので注意。
|
|
341
|
+
- `':childDone:'` - そのステートにある subState か parallel state が終了状態に達したときに発火する。`{type: ':childDone:', output: unknown}` 型のイベントであり、output には子ステートが final で出力した値が入る。
|
|
342
|
+
- `Exec` - 以下のプロパティを持つオブジェクトである。
|
|
343
|
+
- `guard?: OneOrMany<string | (args: CommonArgs)=>boolean>`
|
|
344
|
+
- booleanを返す関数か、MachineConfig の guards で指定したガード名を単体または配列で指定する。配列を指定した場合、全てが true なら真と見なされる。
|
|
345
|
+
- eventType が発火したとき、Exec の guard が先頭から順に評価されていき、最初に真になった Exec が実行される。
|
|
346
|
+
- `actions?: OneOrMany<string | ActionFn>`
|
|
347
|
+
- 関数か、MachineConfig の actions で指定したアクション名を単体または配列で指定する。配列を指定した場合、複数のアクションを先頭から順に実行する。
|
|
348
|
+
- アクションは context の更新や別サービスとの通信など副作用を行う。
|
|
349
|
+
- `to?: string` - 遷移先ステート名を指定する。アクション実行後に指定したステートに遷移する。
|
|
350
|
+
- 単にステート名だけ指定した場合、この on が定義されたステートを起点として子ステートからステートを探し見つかればそれに遷移する。見つからなければ兄弟ステートから探す。
|
|
351
|
+
- `'./foo'` のように `./` から始まるパスで指定した場合、子ステートだけを対象にして遷移先を探す。
|
|
352
|
+
- `'../foo'` のように `../` から始まるパスで指定した場合、兄弟ステートだけを対象にして遷移先を探す。
|
|
353
|
+
- `'/path/to/state'` のように `/` から始まるパスで指定した場合、ルートからの絶対パスとして遷移先を探す。子や兄弟ステート以外を対象に遷移するにはこの絶対パスで指定する必要がある。
|
|
354
|
+
|
|
355
|
+
- `entryHook(...actions: ActionFn[])`
|
|
356
|
+
- そのステートに遷移した直後に最初に実行されるアクション。可変長引数に複数のアクションを設定した場合、前から順番に実行される。`on(':always:', ...)` で設定されたイベントより前に実行される。
|
|
357
|
+
- `exitHook(...actions: ActionFn[])`
|
|
358
|
+
- そのステートから離れる直前に実行されるアクション。可変長引数に複数のアクションを設定した場合、前から順番に実行される。
|
|
359
|
+
- `subStates(stateDefinitions: Record<string, StateDefinition>, initialState?: string)`
|
|
360
|
+
- 1つのステートにつき1つしか指定できず、 parallel と共に指定することはできない。もしやると定義時にエラーが発生する。
|
|
361
|
+
- そのステートの子ステートを定義する。stateDefinitions の形式は makeMachine の第二引数の戻り値の形式と同じ、キーにステート名、値に配列または null を指定したオブジェクトである。
|
|
362
|
+
- サブステートが最終状態になったとき、親ステートに `':childDone:'` イベントが定義されていればそれが発火する。
|
|
363
|
+
- あるステートに入ったとき、サブステートが指定されているのなら、自動的にそれの initialState に入る。initialState が省略された場合、先頭に定義されたステートに入る。
|
|
364
|
+
- `parallel(parallelDefinition: { [Region: string]: [Record<string, StateDefinition>] | [MetaItem[], Record<string, StateDefinition>] })`
|
|
365
|
+
- 1つのステートにつき1つしか指定できず、 subStates と共に指定することはできない。もしやると定義時にエラーが発生する。
|
|
366
|
+
- 同時に複数の状態を並行して取るステートを定義する。ステートは子ステートと孫ステート以下を持つことになる。子ステートはそのステートがアクティブである間必ず全てアクティブでありRegionと呼ばれる。それ以下のステートがそのRegionのステートとなる。
|
|
367
|
+
- 辞書オブジェクトを引数としてとり、キーがRegion 名、値は長さ1か2のタプルである。
|
|
368
|
+
- タプルに1つ辞書オブジェクトを指定するとそれがそのリージョンのステート定義になる。ステート定義は makeMachine の第二引数の戻り値や subStates の第一引数と同じ形式である。
|
|
369
|
+
- タプルの最初に MetaItemの配列を指定することができる。それはそのリージョン自身がもつ属性となり、 `on` `entryHook` `exitHook` を持たせることができる。その場合タプルの2つ目がステートの定義となる。
|
|
370
|
+
- すべての Region が最終状態になったとき、親に `':childDone:'` イベントが設定されているならそれが発火する。
|
|
371
|
+
- Region の子ステートに final が設定され、戻り値が設定されている場合、`':childDone:'` イベントの `output` は Region 名をキー、final の戻り値を値にした辞書が設定される。final に戻り値が設定されていなければその Region のプロパティは設定されない。
|
|
372
|
+
- NOTE: この parallel の API は気に入っていないので将来的に変更される可能性がある。
|
|
373
|
+
- `invoke(service, config?, ...events)`
|
|
374
|
+
- そのステートに入ったとき、service 引数に指定された関数やマシンを起動させる。
|
|
375
|
+
- 1つのステートにつき複数の invoke を指定することもできる。
|
|
376
|
+
- `':done:'` と `':error:'` イベントが提供されている。`on(':done:', ...)` や `on(':error:', ...)` の形式で、events 引数に指定すると、サービスが正常終了した時かエラー終了した時に処理を実行できる。
|
|
377
|
+
- invoke されたサービスはそのステートを抜けるときに停止させられる。その場合は `':done:'` イベントは発火しない。
|
|
378
|
+
- 複数 invoke したサービスが同時に親の遷移イベントを起動させた場合、競合し、一番最初に処理されたイベント以外は処理されない。
|
|
379
|
+
- 引数 `service`
|
|
380
|
+
- service 引数には直接マシンや関数を指定することも、 makeMachine の MachineConfig の services で登録したサービス名を指定することもできる。
|
|
381
|
+
- 関数をリテラルで指定する場合、その関数が引数を受け取るように定義すると型推論が上手く効かなくなるので、その場合は関数を `defServiceFn` 関数でラップしたうえで関数の引数に型を指定して定義すること。
|
|
382
|
+
- サービス関数には4つの引数(`input: InvocationInput, emit: Emit, listen: Listen, signal: AbortSignal`)が渡され、戻り値が `':done:'` イベントの output として使われる。
|
|
383
|
+
- ただし、サービス関数が関数を返すと、`':done:'` イベントは発火しない。その関数はクリーンアップ関数と見なされ、そのステートを抜けるときに発火する。
|
|
384
|
+
- emit や listen で送出したり受けたりするイベントは親のイベントではなくこの関数独自に定義した型になる。
|
|
385
|
+
- サービス関数に渡される4つの引数
|
|
386
|
+
- `input: InvocationInput` - config の input から渡された値
|
|
387
|
+
- `emit: (event: EmittableEvent) => void`
|
|
388
|
+
- 子から親に通信するときに使用する関数。この関数の引数に渡したイベントオブジェクトは `...events`引数で購読できる。
|
|
389
|
+
- これに渡すことのできるイベント種類は親の Event ではなく、この関数自身が独自に定義することのできるイベントである。
|
|
390
|
+
- `defServiceFn` を用いつつ、emit 引数に `Emit<EmittableEvent>`型を用いて型指定すると、親マシンで型が認識され受けつけられるようになる。
|
|
391
|
+
- `listen: (listener: (event: SendableEvent) => void) => void`
|
|
392
|
+
- 親から `sendTo` で送られたイベントを購読するリスナー関数を登録する。
|
|
393
|
+
- `signal: AbortSignal`
|
|
394
|
+
- その関数がもうすでに停止扱いになったかどうかを内部から検知するためのオブジェクト。`signal.aborted` が `ture` のとき、その関数はステートを抜けたなどの理由で停止扱いになっている。
|
|
395
|
+
- 引数 `config` - オプショナルな設定オブジェクト。以下のプロパティを持つことができる。
|
|
396
|
+
- `input?: (args: CommonArgs) => InvocationInput`
|
|
397
|
+
- この関数の戻り値を service の input 値として利用することができる。どんな型でも良い。
|
|
398
|
+
- `subKey?: string|number`
|
|
399
|
+
- services に登録しているサービスの内、同じ名前のサービスを複数同時に起動するときにはこれの設定が必須である。そうでなければエラーが発生する。
|
|
400
|
+
- これは名前付きサービスはその名前で起動されるが、同じ名前で複数起動できないためであり、それの回避のためである。
|
|
401
|
+
- これが指定されているとき、`snapshot._children` では `${serviceName}:${subKey}` というキーで登録されてサービスが起動される。
|
|
402
|
+
- これが指定されているとき、 `sendTo` ではサブキーも同時に指定しないと送信できなくなる。
|
|
403
|
+
- 引数 `...events`
|
|
404
|
+
- `on` 関数を用いてそのサービス内で起こるイベントにフックした処理を設定することができる。組み込みで用意されているイベントの他に、そのサービス内から `emit` を使って発行されたイベントにフックすることもできる。
|
|
405
|
+
- 組み込みで用意されているイベントの型は以下である:
|
|
406
|
+
- `{type: ':done:', output: unknown}`
|
|
407
|
+
- そのサービスが正常終了した時に発行される。
|
|
408
|
+
- output にはそのサービスが関数であるときにはAwaited された戻り値が、マシンであるときには output 関数が返す戻り値が設定される。
|
|
409
|
+
- `{type: ':error:', error: unknown}`
|
|
410
|
+
- そのサービスが異常終了した時に発行される。
|
|
411
|
+
|
|
412
|
+
#### makeMachine > 関数に渡される引数
|
|
413
|
+
|
|
414
|
+
多くの関数では一番目の引数として下記のプロパティが設定されたオブジェクトが渡される:
|
|
415
|
+
|
|
416
|
+
- `CommonArgs`
|
|
417
|
+
- `context`
|
|
418
|
+
- マシンの現在のコンテキストの値。マシンの initContext を設定していないと null になる。
|
|
419
|
+
- イミュータブルであり、変更は許されていない。更新するときには ActionFn の update 関数を通して更新すること。
|
|
420
|
+
- `c` - context のショートハンド。使用頻度が高いので怠惰のために用意しただけで context と内容は同じ。
|
|
421
|
+
- `event` - 現在のアクションのきっかけとなったイベント
|
|
422
|
+
- `e` - event のショートハンド。使用頻度が高いので怠惰のために用意しただけで event と内容は同じ。
|
|
423
|
+
- `input` - マシンランタイム作成時に input オプションから与えられた値。基本的に静的なデータであるべきで、マシン内で更新するべきではない。作成時に一度与えられるだけの値の割にはライフサイクルを通じてずっと参照することができる。
|
|
424
|
+
- input に副作用をもたらす関数を渡してマシン内部で実行するという使い方もできるが、どちらかというと、副作用は emit を通じて行われるべきである。
|
|
425
|
+
|
|
426
|
+
参考までに、この種の関数には以下のものがある:
|
|
427
|
+
- makeMachine の MachineConfig.output, guard, actionsやguards の params, invoke の input, final
|
|
428
|
+
|
|
429
|
+
entryHook や exitHook や on で使うことができる ActionFn の第一引数オブジェクトは追加で以下のプロパティを持つ:
|
|
430
|
+
|
|
431
|
+
- `update: (updates: Partial<Context>) => void`
|
|
432
|
+
- Context の部分プロパティを持つオブジェクトを渡すと Context を部分更新する関数。
|
|
433
|
+
- この関数を実行しても更新されたcontextがその関数内ですぐに利用できるわけではなく、その関数のスコープ内では context は古い値のままである。なぜなら context はイミュータブルだからだ。
|
|
434
|
+
- この後に別のアクション関数を追加すると、そこで更新後の context を利用することができる。
|
|
435
|
+
- `emit: (event: EmittableEvent) => void`
|
|
436
|
+
- 外部にイベントを発行する関数。ここでのイベントは外側から send で送られてきてステート内の `on` で購読する SendableEvent とは別物である。
|
|
437
|
+
- マシンの場合、 MachineConfig.types.emittable で型を指定する。
|
|
438
|
+
- ランタイム側からは runtime.onEmitted でこのイベントを購読できる。
|
|
439
|
+
- もし invoke されているサービスから emit した場合、 invoke 関数内の on から購読することができる。
|
|
440
|
+
- `sendTo: (service: string, subKey?: string|number, event: EventOfService) => void`
|
|
441
|
+
- invoke されて現在起動しているマシンや関数にイベントを send する。
|
|
442
|
+
- 受け付けられるイベントはマシンの場合そのマシンが send で受け取れるイベント、関数の場合、listen で登録されているイベントである。
|
|
443
|
+
- MachineConfig の services に登録されたサービスしか対象にできない(invoke にリテラル関数やマシン定義を直接渡したときには使えない)。services に登録した名前でサービスを指定する。
|
|
444
|
+
- invoke のオプションで subKey を指定して起動した場合は subKey も同時に指定しないといけない。
|
|
445
|
+
- `check`
|
|
446
|
+
- `check: (type: string, params?: ParamsValue | ((args: CommonArgs)=>ParamValue)) => boolean`
|
|
447
|
+
- `check: (type: {[guardName: string]: ParamsValueOrFn}) => boolean`
|
|
448
|
+
- enqueue と組み合わせることで実行するアクションを動的に組み立てることができる。
|
|
449
|
+
- 他に update と組み合わせることで条件によって更新するかどうか決めることができる。
|
|
450
|
+
- type に guard 名を指定することでその guard の現在の判定結果を返す。
|
|
451
|
+
- params 付きの guard は第二引数に params を与えるか、キーに guard 名、値に params を指定したオブジェクトとして与えるかすることができる。
|
|
452
|
+
- `enqueue`
|
|
453
|
+
- `enqueue: (type: string, params?: ParamsValue | ((args: CommonArgs)=>ParamValue)) => void`
|
|
454
|
+
- `enqueue: (type: {[guardName: string]: ParamsValueOrFn}) => void`
|
|
455
|
+
- `enqueue: (fn: (args: {context, c, event, e, input, update, emit})=>void) => void`
|
|
456
|
+
- check と組み合わせることで実行するアクションを動的に組み立てることができる。enqueue に与えられたアクションはすぐに実行されるのではなく実行が予約され、現在のアクションを抜けた後に enqueue された順に実行が開始される。
|
|
457
|
+
- type に action名を指定するか、パラメーター付きのアクションをオブジェクトで指定するほかに、関数のリテラルを直接渡すこともできる。
|
|
458
|
+
- 外側の関数内や直前の enqueue で update を実行した場合、外側の context は更新反映前の古い context になるので、context を使うのなら enqueue の内側の関数に渡される引数の context を参照する必要がある。
|
|
459
|
+
- enqueue 内の関数でさらに enqueue することは今のところ禁じられている。もし enqueue した場合エラーが投げられる。
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
### `makeRuntime<M extends AnyMachine>(machine: M, options?: RuntimeOptions): Runtime`
|
|
464
|
+
|
|
465
|
+
マシンを実行可能なランタイムを作る。ランタイムは作っただけではマシンは起動せず、 `.start()` を実行しないと起動しない。
|
|
466
|
+
|
|
467
|
+
- `RuntimeOptions` - 以下のプロパティを持つオブジェクトである。
|
|
468
|
+
- `id?: string` - ランタイムの id。ログなどに使われる。無指定時には マシンの name と番号を組み合わせた名前が勝手につけられる。
|
|
469
|
+
- `input?: () => I`
|
|
470
|
+
- ランタイム作成時にマシンの input に設定される値を返す関数。この値は initContext でも使うことができ、マシンのあらゆる文脈から参照することができる。
|
|
471
|
+
- あくまで参照用の値として使い、マシン側からこの値を変更するのは避けるべきである。
|
|
472
|
+
|
|
473
|
+
Runtime は以下のメンバーを持つ:
|
|
474
|
+
|
|
475
|
+
- `id: string` - ランタイムのid
|
|
476
|
+
- `machine: M` - 渡されたマシンの参照。
|
|
477
|
+
- `getSnapshot(): Snapshot` - 現在のランタイムの状態を表すスナップショットを得る。
|
|
478
|
+
- あくまでその取得時の瞬間のものなので時間経過によって古くなる可能性がある。
|
|
479
|
+
- `.start()` されていないとき、snapshot は空オブジェクトになる。
|
|
480
|
+
- Snapshot はオブジェクトであり、以下のメンバーを持つ:
|
|
481
|
+
- `runtimeId: string` - そのスナップショットのランタイムID。識別のために用意されている。
|
|
482
|
+
- `runtimeStatus: RuntimeStatus` - ランタイムの稼働状態。型の `RuntimeStatus` 参照。
|
|
483
|
+
- `states: string[]` - 現在のステートのフルパスのリスト。現在ステートが parallel な子ステートを持つ場合、配列の長さは 2 以上になることがある。そうでないときには配列の長さは 1 である。
|
|
484
|
+
- 他のステートマシンと違って、ステート名の前に '/' が付き、階層は '/' で区切られる。
|
|
485
|
+
- `stateValue: string | string[] | undefined` - parallel を扱わないマシン定義のために用意されたショートカット。states の長さが 1 のときには最初のステートパスを返す。そうでないときには states をそのまま返す。
|
|
486
|
+
- `context: PrivateKeyRemovedContext` - コンテキスト。ただし、マシン内部で context を参照するときと違い、`_` から始まるプロパティは公開されない。
|
|
487
|
+
- `c` - 怠惰のために用意された context のショートハンド。中身は context と同じ。
|
|
488
|
+
- `output: O | undefined` - ランタイムが最終状態に入るまでは undefined。最終状態に入ったとき計算され結果がここに設定される。
|
|
489
|
+
- `error: unknown` - エラーが生じてランタイムが停止したときにそのエラー内容が入る。それまでは undefined。
|
|
490
|
+
- `_children` - デバッグ用。業務では使用しないのが望ましい。キーにサービス名、値に invoke などで起動中のサービスが入る。subKey を設定して起動している場合、キーは `${serviceName}:${subKey}` になる。
|
|
491
|
+
- `_runtime` - このスナップショットを作ったランタイム自身。
|
|
492
|
+
- 怠惰のために runtime から直接プロパティでsnapshot を参照できる getter が用意されている。以下のものが設定されている:
|
|
493
|
+
- states, stateValue, context, runtimeStatus, output
|
|
494
|
+
- `subscribe(onSnapshot: ((current: Snapshot, prev: Snapshot)=>void)|undefined, onError?:(err:unknown)=>void): UnsubscribeFn`
|
|
495
|
+
- snapshot が更新されるたびに呼ばれる観測関数を登録する。ただし第一引数の関数はエラーは監視しない。それは省略可能な第二引数の関数の役割である。
|
|
496
|
+
- React の useSyncExternalStore のように更新とエラーの両方を監視する関数が必要な場合、2つの関数をまとめて記述する必要がある。
|
|
497
|
+
```ts
|
|
498
|
+
const subscribe = useCallback((onStoreChange) => {
|
|
499
|
+
const unsubscribe = rt.subscribe(onStoreChange, onStoreChange)
|
|
500
|
+
return unsubscribe
|
|
501
|
+
}, [rt]);
|
|
502
|
+
const snapshot = useSyncExternalStore(subscribe, () => rt.getSnapshot())
|
|
503
|
+
```
|
|
504
|
+
- もしもエラーを監視する関数が全く登録されていない場合、エラーが発生するとそのまま例外として投げられる。
|
|
505
|
+
- UnsubscribeFn を返す。実行すると登録を解除する。
|
|
506
|
+
- onSnapshot
|
|
507
|
+
- `current` - 現在のスナップショット
|
|
508
|
+
- `prev` - 1つ前のスナップショット
|
|
509
|
+
- NOTE: この引数は無意味に感じているので仕様から削除される可能性がある。
|
|
510
|
+
- `UnsubscribeFn: () => void` - 登録を削除する。
|
|
511
|
+
- `start(): this` - ランタイムを起動する。runtimeStatus が 'active' になり最初のステートに遷移し、 send を受け付けるようになる。
|
|
512
|
+
- `send(event: SendableEvent | string): this`
|
|
513
|
+
- 外部からマシンにイベントを送る。`type` 以外のプロパティのないイベントの場合、オブジェクトでなく文字列で送ることもできる。このイベントを送った結果は getSnapshot() や subscribe で受けることができる。
|
|
514
|
+
- `stop(): this` - 'active' のランタイムを停止する。一度停止させると再開することはできない。runtimeStatus は 'stopped' になる。
|
|
515
|
+
- `onEmitted(emittedEventType: string, handler: (emittedEvent: EmittableEvent) => void): UnsubscribeFn`
|
|
516
|
+
- マシン内部から送られたイベント EmittableEvent に対応した処理を設定する。主に副作用を設定すると良い。
|
|
517
|
+
- `emittedEventType` に指定できる特別な値として `'*'` がある。これはすべての EmittedEvent を対象にする。
|
|
518
|
+
- `UnsubscribeFn: () => void` - 登録を削除する。
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
## XState との差異
|
|
523
|
+
|
|
524
|
+
| | XState | small-state |
|
|
525
|
+
| - | - | - |
|
|
526
|
+
| パス区切り文字 | `.` | `/` |
|
|
527
|
+
| ロジックを持つのは | マシン | ランタイム |
|
|
528
|
+
| context 更新 | assign | update: action内引数 |
|
|
529
|
+
| 子から親への通信 | sendParent | emit |
|
|
530
|
+
| 親から子への通信に使うもの | actor id | service name (と subKey) |
|
|
531
|
+
| 兄弟アクター間の通信 | sendTo | できない (親経由で代替可能) |
|
|
532
|
+
| 子から外界への emit | emit | なし(親経由で代替可能) |
|
|
533
|
+
| eventless | always | on(':always:', ...) |
|
|
534
|
+
| subState の終了フック | onDone | on(':childDone:', ...) |
|
|
535
|
+
| invokeの終了フック | onDone | on(':done:', ...) |
|
|
536
|
+
| invokeのerrorフック | onError | on(':error:', ...) |
|
|
537
|
+
|
|
538
|
+
- initial を指定しなければ最初のステートがアクティブになる。
|
|
539
|
+
- 階層を持つステートがアクティブなら、必ず末端のステートがアクティブになる。
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
## 不足している機能
|
|
543
|
+
|
|
544
|
+
以下の機能はない:
|
|
545
|
+
|
|
546
|
+
- delays, after: 指定した秒数後に自動的に別の状態に遷移する機能
|
|
547
|
+
- 子の syncSnapshot: boolean オプション: 子のスナップショットが更新されるたびに親のスナップショットも更新する機能
|
|
548
|
+
- spawn, spawnChild
|
|
549
|
+
- stopChild
|
|
550
|
+
- raise
|
|
551
|
+
- tags
|
|
552
|
+
- .matches()
|
|
553
|
+
- ステートid (代替手段:フルパス指定での遷移)
|
|
554
|
+
- reenter (自己遷移)
|
|
555
|
+
- マシントップレベル(ルート)で以下の機能が使えない
|
|
556
|
+
- invoke, entryHook, exitHook, parallel
|
|
557
|
+
- 状態を持たないマシン
|
|
558
|
+
|
|
559
|
+
## TODO
|
|
560
|
+
|
|
561
|
+
- Arktype を独自のパーサーに置き換えてエラー箇所を分かりやすくする
|
|
562
|
+
- parallel のインターフェースの改修
|
|
563
|
+
- もう少し分かりやすく
|
|
564
|
+
- 連続で入れ子にできないのを改善する
|