@mantaq/core 0.3.0 → 0.5.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/README.md +117 -87
- package/dist/index.d.mts +148 -157
- package/dist/index.mjs +1 -1
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @mantaq/core
|
|
2
2
|
|
|
3
|
-
State machine library. Small API surface
|
|
3
|
+
State machine library. Small API surface. Cook the primitives, not the framework.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -81,7 +81,7 @@ See [anderscan.github.io/mantaq](https://anderscan.github.io/mantaq) for full do
|
|
|
81
81
|
|
|
82
82
|
### Typed Actor Context
|
|
83
83
|
|
|
84
|
-
Cast the context type once at the constructor level. Handlers receive the typed context automatically
|
|
84
|
+
Cast the context type once at the constructor level. Handlers receive the typed context automatically. No casting needed per-handler.
|
|
85
85
|
|
|
86
86
|
```ts
|
|
87
87
|
interface AuthContext {
|
|
@@ -106,7 +106,7 @@ const actor = new Actor({
|
|
|
106
106
|
});
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
-
Context is user land
|
|
109
|
+
Context is user land. Mutate it freely. `set()` is the write signal: any `set()` triggers `change`, even with the same reference. Call it after you mutate:
|
|
110
110
|
|
|
111
111
|
```ts
|
|
112
112
|
const s = opts.context.get();
|
|
@@ -121,7 +121,7 @@ opts.context.set(s);
|
|
|
121
121
|
opts!.context.phoneNumber = event.payload.phoneNumber;
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
-
**Anti-pattern
|
|
124
|
+
**Anti-pattern. Casting in every handler:**
|
|
125
125
|
|
|
126
126
|
```ts
|
|
127
127
|
setup: (m) => {
|
|
@@ -160,7 +160,7 @@ setup: (m) => {
|
|
|
160
160
|
},
|
|
161
161
|
```
|
|
162
162
|
|
|
163
|
-
**Anti-pattern
|
|
163
|
+
**Anti-pattern. Skipping the generic parameter:**
|
|
164
164
|
|
|
165
165
|
```ts
|
|
166
166
|
// ❌ payload is `unknown`, no type safety
|
|
@@ -170,7 +170,7 @@ const badEvent = event("SIGN_IN")();
|
|
|
170
170
|
const goodEvent = event("SIGN_IN")<{ phoneNumber: string }>();
|
|
171
171
|
```
|
|
172
172
|
|
|
173
|
-
**Anti-pattern
|
|
173
|
+
**Anti-pattern. Sending raw objects instead of using `.create()`:**
|
|
174
174
|
|
|
175
175
|
```ts
|
|
176
176
|
// ✅ typed
|
|
@@ -182,7 +182,7 @@ actor.send({ type: "SIGN_IN", payload: { phoneNumber: "+1234567890" } });
|
|
|
182
182
|
|
|
183
183
|
### Effects and Event Typing
|
|
184
184
|
|
|
185
|
-
Effects run when entering a state. The `event` parameter in effects is typed `InternalEvent
|
|
185
|
+
Effects run when entering a state. The `event` parameter in effects is typed `InternalEvent`, `{ type, payload? }` with `payload: unknown`, not the specific event that triggered the transition. This is by design: effects live on states, not transitions. Multiple transitions can lead to the same state, so the effect cannot know which event caused entry.
|
|
186
186
|
|
|
187
187
|
**Pass data to effects via state payload.** Set the payload in the transition return, read it from `state.payload` in the effect.
|
|
188
188
|
|
|
@@ -198,12 +198,15 @@ const actor = new Actor({
|
|
|
198
198
|
states: [idleState, loadingState, loadedState],
|
|
199
199
|
initial: idleState,
|
|
200
200
|
setup: (m) => {
|
|
201
|
-
m.effect(loadingState,
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
201
|
+
m.effect(loadingState, {
|
|
202
|
+
name: "fetchUrl",
|
|
203
|
+
fn: ({ state, emit, clock }) => {
|
|
204
|
+
// state.payload is typed to the state's declared payload: { url: string }
|
|
205
|
+
const url = state.payload.url;
|
|
206
|
+
clock.setTimeout(1000, () => {
|
|
207
|
+
emit(doneEvent.create());
|
|
208
|
+
});
|
|
209
|
+
},
|
|
207
210
|
});
|
|
208
211
|
m.on(idleState, fetchEvent, (event) => {
|
|
209
212
|
return { state: loadingState.create({ url: event.payload.url }) };
|
|
@@ -213,7 +216,7 @@ const actor = new Actor({
|
|
|
213
216
|
});
|
|
214
217
|
```
|
|
215
218
|
|
|
216
|
-
**Context is also possible but less precise.** Context is shared across all states
|
|
219
|
+
**Context is also possible but less precise.** Context is shared across all states. Writing to context in one state leaks into others. Prefer state payload for data that belongs to a specific state entry.
|
|
217
220
|
|
|
218
221
|
```ts
|
|
219
222
|
// ✅ state payload — scoped to this state entry
|
|
@@ -226,12 +229,15 @@ opts!.context.set(s);
|
|
|
226
229
|
return { state: loadingState };
|
|
227
230
|
```
|
|
228
231
|
|
|
229
|
-
**Anti-pattern
|
|
232
|
+
**Anti-pattern. Depending on `event` in effects:**
|
|
230
233
|
|
|
231
234
|
```ts
|
|
232
|
-
m.effect(loadingState,
|
|
233
|
-
|
|
234
|
-
|
|
235
|
+
m.effect(loadingState, {
|
|
236
|
+
name: "fetchUrl",
|
|
237
|
+
fn: ({ event, emit }) => {
|
|
238
|
+
// ❌ event is InternalEvent — payload is unknown, not the triggering event's payload
|
|
239
|
+
emit(doneEvent.create()); // fine — but event.payload.url would be a type error
|
|
240
|
+
},
|
|
235
241
|
});
|
|
236
242
|
```
|
|
237
243
|
|
|
@@ -241,9 +247,9 @@ The `event` parameter exists for convenience (e.g., logging), not for business l
|
|
|
241
247
|
|
|
242
248
|
Mantaq uses two event queues: **external** (user-sent) and **internal** (effect-emitted). Understanding the difference prevents subtle bugs.
|
|
243
249
|
|
|
244
|
-
**External events** are sent via `actor.send()
|
|
250
|
+
**External events** are sent via `actor.send()`. They trigger transitions on the current state.
|
|
245
251
|
|
|
246
|
-
**Internal events** are emitted from effects via `emit()
|
|
252
|
+
**Internal events** are emitted from effects via `emit()`. They are queued and processed after the current transition completes.
|
|
247
253
|
|
|
248
254
|
```ts
|
|
249
255
|
const tickEvent = event("TICK")();
|
|
@@ -255,10 +261,13 @@ const actor = new Actor({
|
|
|
255
261
|
states: [idleState, connectedState],
|
|
256
262
|
initial: idleState,
|
|
257
263
|
setup: (m) => {
|
|
258
|
-
m.effect(idleState,
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
264
|
+
m.effect(idleState, {
|
|
265
|
+
name: "scheduleTick",
|
|
266
|
+
fn: ({ emit, clock }) => {
|
|
267
|
+
clock.setTimeout(1000, () => {
|
|
268
|
+
emit(tickEvent.create()); // goes to internal queue
|
|
269
|
+
});
|
|
270
|
+
},
|
|
262
271
|
});
|
|
263
272
|
m.on(idleState, connectEvent, () => ({ state: connectedState })); // external handler
|
|
264
273
|
m.on(idleState, tickEvent, () => ({})); // internal handler
|
|
@@ -266,9 +275,9 @@ const actor = new Actor({
|
|
|
266
275
|
});
|
|
267
276
|
```
|
|
268
277
|
|
|
269
|
-
**Ordering:** External events process one at a time. Internal events emitted during a transition process depth-first after that transition. Internal events from effects process after the effect completes
|
|
278
|
+
**Ordering:** External events process one at a time. Internal events emitted during a transition process depth-first after that transition. Internal events from effects process after the effect completes. This is a common source of bugs. If an effect emits an internal event and the actor is in a different state by the time that event processes, the handler may not exist or may behave unexpectedly.
|
|
270
279
|
|
|
271
|
-
**Anti-pattern
|
|
280
|
+
**Anti-pattern. Mixing internal/external incorrectly:**
|
|
272
281
|
|
|
273
282
|
```ts
|
|
274
283
|
// ❌ declaring a user-sent event as internal
|
|
@@ -280,7 +289,7 @@ inputs: [connectEvent],
|
|
|
280
289
|
|
|
281
290
|
### Effect Pattern
|
|
282
291
|
|
|
283
|
-
Effects run on state entry. They receive typed context, an AbortSignal, and an emit function.
|
|
292
|
+
Effects run on state entry. They receive typed context, an AbortSignal, and an emit function. Each effect carries a required camelCase `name` describing what it does — tests can assert on it (`harness.assertEffectRan(stateName, effectName)`) and executed effects appear in history as `{ stateName, effectName }`.
|
|
284
293
|
|
|
285
294
|
```ts
|
|
286
295
|
type MyContext = { retryCount: number; maxRetries: number };
|
|
@@ -297,17 +306,20 @@ function createActor() {
|
|
|
297
306
|
initial: idleState,
|
|
298
307
|
context: { retryCount: 0, maxRetries: 3 } as MyContext,
|
|
299
308
|
setup: (m) => {
|
|
300
|
-
m.effect(workingState,
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
309
|
+
m.effect(workingState, {
|
|
310
|
+
name: "attemptWork",
|
|
311
|
+
fn: ({ signal, context, emit, clock }) => {
|
|
312
|
+
// context is MyContext — typed from the constructor generic
|
|
313
|
+
if (context.retryCount >= context.maxRetries) {
|
|
314
|
+
emit(failedEvent.create({ error: "Max retries exceeded" }));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
clock.setTimeout(2000, () => {
|
|
319
|
+
if (signal.aborted) return; // ✅ check signal before emitting
|
|
320
|
+
emit(doneEvent.create());
|
|
321
|
+
});
|
|
322
|
+
},
|
|
311
323
|
});
|
|
312
324
|
m.on(idleState, startEvent, () => ({ state: workingState }));
|
|
313
325
|
m.on(workingState, doneEvent, () => ({ state: doneState }));
|
|
@@ -322,30 +334,36 @@ function createActor() {
|
|
|
322
334
|
}
|
|
323
335
|
```
|
|
324
336
|
|
|
325
|
-
**Anti-pattern
|
|
337
|
+
**Anti-pattern. Not checking `signal.aborted`:**
|
|
326
338
|
|
|
327
339
|
```ts
|
|
328
|
-
m.effect(workingState,
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
340
|
+
m.effect(workingState, {
|
|
341
|
+
name: "attemptWork",
|
|
342
|
+
fn: ({ signal, emit, clock }) => {
|
|
343
|
+
// ❌ if state changes before timeout, this still fires
|
|
344
|
+
clock.setTimeout(2000, () => {
|
|
345
|
+
emit(doneEvent.create());
|
|
346
|
+
});
|
|
347
|
+
},
|
|
333
348
|
});
|
|
334
349
|
```
|
|
335
350
|
|
|
336
351
|
```ts
|
|
337
|
-
m.effect(workingState,
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
352
|
+
m.effect(workingState, {
|
|
353
|
+
name: "attemptWork",
|
|
354
|
+
fn: ({ signal, emit, clock }) => {
|
|
355
|
+
// ✅ guard with abort check
|
|
356
|
+
clock.setTimeout(2000, () => {
|
|
357
|
+
if (signal.aborted) return;
|
|
358
|
+
emit(doneEvent.create());
|
|
359
|
+
});
|
|
360
|
+
},
|
|
343
361
|
});
|
|
344
362
|
```
|
|
345
363
|
|
|
346
364
|
### Snapshot & Restore
|
|
347
365
|
|
|
348
|
-
`actor.snapshot()` returns a `Snapshot
|
|
366
|
+
`actor.snapshot()` returns a `Snapshot`: `{ path, context, payload?, regions, done?, error? }`. Context is included; `payload` is the payload the current state was entered with (present only when the transition carried one). Use it to save/restore actor state across sessions.
|
|
349
367
|
|
|
350
368
|
```ts
|
|
351
369
|
// Snapshot carries path, context, and regions — serialize it directly
|
|
@@ -359,7 +377,7 @@ if (raw) {
|
|
|
359
377
|
}
|
|
360
378
|
```
|
|
361
379
|
|
|
362
|
-
**Anti-pattern
|
|
380
|
+
**Anti-pattern. Re-serializing context by hand:**
|
|
363
381
|
|
|
364
382
|
```ts
|
|
365
383
|
// ❌ snapshot() already includes context — no manual step needed
|
|
@@ -410,7 +428,7 @@ const manager = new Actor({
|
|
|
410
428
|
matches(manager, "connected.health.healthy"); // ✅
|
|
411
429
|
```
|
|
412
430
|
|
|
413
|
-
**Anti-pattern
|
|
431
|
+
**Anti-pattern. Child outputs not declared as parent internal events:**
|
|
414
432
|
|
|
415
433
|
```ts
|
|
416
434
|
const child = new Actor({
|
|
@@ -433,7 +451,7 @@ const parent = new Actor({
|
|
|
433
451
|
|
|
434
452
|
### Error Handling
|
|
435
453
|
|
|
436
|
-
Never throw from effects or transitions. Emit an error as an internal event and let a transition handle it. If user code throws anyway, the machine does **not** misbehave silently: it dies into a built-in terminal `__error` state, records `snapshot().error` (the thrown value, the state/context at the point of failure, the bad event), drops remaining events, and ignores later `send`s. Errors never escape `send()` and the machine stays deterministic
|
|
454
|
+
Never throw from effects or transitions. Emit an error as an internal event and let a transition handle it. If user code throws anyway, the machine does **not** misbehave silently: it dies into a built-in terminal `__error` state, records `snapshot().error` (the thrown value, the state/context at the point of failure, the bad event), drops remaining events, and ignores later `send`s. Errors never escape `send()` and the machine stays deterministic. Same inputs, same trace.
|
|
437
455
|
|
|
438
456
|
```ts
|
|
439
457
|
const snap = actor.snapshot();
|
|
@@ -446,10 +464,10 @@ if (snap.error) {
|
|
|
446
464
|
|
|
447
465
|
Two deliberate exceptions to "every failure is loud":
|
|
448
466
|
|
|
449
|
-
- **Subscribers only watch.** They read snapshots and never change the machine, so a throwing `on("change")`/`on("done")`/`on("transition")`/`on("error")` callback is swallowed
|
|
467
|
+
- **Subscribers only watch.** They read snapshots and never change the machine, so a throwing `on("change")`/`on("done")`/`on("transition")`/`on("error")` callback is swallowed. The machine and its callers are unaffected.
|
|
450
468
|
- **Unhandled external events are ignored.** An event with no handler in the current state is dropped by design (broadcast fan-out, cross-state sends). An **internal** event with no handler, however, is a machine-authoring bug and routes to the error state (`reason: "unhandled"`).
|
|
451
469
|
|
|
452
|
-
A dead machine can be manually resumed with `actor.recover({ state, context })
|
|
470
|
+
A dead machine can be manually resumed with `actor.recover({ state, context })`. An explicit, **inherently dangerous** escape hatch (the caller injects state and context, so determinism no longer holds). Effects are not re-run and timers are not re-armed; processing resumes on the next event. Prefer fixing the root cause and recreating the actor.
|
|
453
471
|
|
|
454
472
|
Catch errors inside effects and emit recovery events:
|
|
455
473
|
|
|
@@ -458,17 +476,20 @@ const doneEvent = event("WORK_DONE")();
|
|
|
458
476
|
const failedEvent = event("WORK_FAILED")<{ error: string }>();
|
|
459
477
|
|
|
460
478
|
setup: (m) => {
|
|
461
|
-
m.effect(workingState,
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
479
|
+
m.effect(workingState, {
|
|
480
|
+
name: "runRiskyOperation",
|
|
481
|
+
fn: ({ signal, emit, clock }) => {
|
|
482
|
+
clock.setTimeout(100, () => {
|
|
483
|
+
if (signal.aborted) return;
|
|
484
|
+
try {
|
|
485
|
+
const result = riskyOperation();
|
|
486
|
+
emit(doneEvent.create());
|
|
487
|
+
} catch (err) {
|
|
488
|
+
// ✅ emit error as internal event — lets transition handle it
|
|
489
|
+
emit(failedEvent.create({ error: String(err) }));
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
},
|
|
472
493
|
});
|
|
473
494
|
m.on(workingState, doneEvent, () => ({ state: doneState }));
|
|
474
495
|
m.on(workingState, failedEvent, (event) => {
|
|
@@ -478,7 +499,7 @@ setup: (m) => {
|
|
|
478
499
|
},
|
|
479
500
|
```
|
|
480
501
|
|
|
481
|
-
**Anti-pattern
|
|
502
|
+
**Anti-pattern. Re-throwing in effects:**
|
|
482
503
|
|
|
483
504
|
```ts
|
|
484
505
|
try {
|
|
@@ -489,7 +510,7 @@ try {
|
|
|
489
510
|
}
|
|
490
511
|
```
|
|
491
512
|
|
|
492
|
-
**Anti-pattern
|
|
513
|
+
**Anti-pattern. Throwing in transition handlers:**
|
|
493
514
|
|
|
494
515
|
```ts
|
|
495
516
|
m.on(idleState, submitEvent, (event) => {
|
|
@@ -514,7 +535,7 @@ m.on(idleState, submitEvent, (event, opts) => {
|
|
|
514
535
|
|
|
515
536
|
### Any Handler
|
|
516
537
|
|
|
517
|
-
Use `onAny` to intercept events across all states
|
|
538
|
+
Use `onAny` to intercept events across all states. Useful for universal error handling, cleanup, or logging.
|
|
518
539
|
|
|
519
540
|
```ts
|
|
520
541
|
setup: (m) => {
|
|
@@ -530,7 +551,7 @@ setup: (m) => {
|
|
|
530
551
|
},
|
|
531
552
|
```
|
|
532
553
|
|
|
533
|
-
**Anti-pattern
|
|
554
|
+
**Anti-pattern. Using `onAny` for everything:**
|
|
534
555
|
|
|
535
556
|
```ts
|
|
536
557
|
setup: (m) => {
|
|
@@ -574,8 +595,11 @@ const actor = new Actor({
|
|
|
574
595
|
initial: idle,
|
|
575
596
|
clock,
|
|
576
597
|
setup: (m) => {
|
|
577
|
-
m.effect(idle,
|
|
578
|
-
|
|
598
|
+
m.effect(idle, {
|
|
599
|
+
name: "armIdleTimeout",
|
|
600
|
+
fn: ({ emit, clock }) => {
|
|
601
|
+
clock.setTimeout(5000, () => emit(timer.create()));
|
|
602
|
+
},
|
|
579
603
|
});
|
|
580
604
|
m.on(idle, timer, () => ({ state: timedOut }));
|
|
581
605
|
},
|
|
@@ -588,7 +612,7 @@ expect(actor.state.name).toBe("timedOut");
|
|
|
588
612
|
expect(clock.hasPending()).toBe(false);
|
|
589
613
|
```
|
|
590
614
|
|
|
591
|
-
**Anti-pattern
|
|
615
|
+
**Anti-pattern. Using `new Date()` or `setTimeout` directly:**
|
|
592
616
|
|
|
593
617
|
```ts
|
|
594
618
|
// ❌ real timers — slow, flaky, non-deterministic
|
|
@@ -618,11 +642,14 @@ const actor = new Actor({
|
|
|
618
642
|
initial: loading,
|
|
619
643
|
clock,
|
|
620
644
|
setup: (m) => {
|
|
621
|
-
m.effect(loading,
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
645
|
+
m.effect(loading, {
|
|
646
|
+
name: "scheduleWork",
|
|
647
|
+
fn: ({ signal, clock }) => {
|
|
648
|
+
const id = clock.setTimeout(5000, () => {
|
|
649
|
+
/* ... */
|
|
650
|
+
});
|
|
651
|
+
signal.addEventListener("abort", () => clock.clearTimeout(id));
|
|
652
|
+
},
|
|
626
653
|
});
|
|
627
654
|
m.on(loading, cancel, () => ({ state: success }));
|
|
628
655
|
},
|
|
@@ -651,8 +678,11 @@ const actor = new Actor({
|
|
|
651
678
|
initial: active,
|
|
652
679
|
clock,
|
|
653
680
|
setup: (m) => {
|
|
654
|
-
m.effect(active,
|
|
655
|
-
|
|
681
|
+
m.effect(active, {
|
|
682
|
+
name: "startTicker",
|
|
683
|
+
fn: ({ emit, clock }) => {
|
|
684
|
+
clock.setInterval(1000, () => emit(tick.create()));
|
|
685
|
+
},
|
|
656
686
|
});
|
|
657
687
|
m.on(active, tick, () => {
|
|
658
688
|
count++;
|
|
@@ -665,7 +695,7 @@ clock.advance(3500);
|
|
|
665
695
|
expect(count).toBe(3); // fired at 1000, 2000, 3000
|
|
666
696
|
```
|
|
667
697
|
|
|
668
|
-
**Anti-pattern
|
|
698
|
+
**Anti-pattern. Expecting single `advance` to fire interval once:**
|
|
669
699
|
|
|
670
700
|
```ts
|
|
671
701
|
// ❌ advance(5000) fires interval at 1000, 2000, 3000, 4000, 5000
|
|
@@ -697,12 +727,12 @@ vp run mutation:core # mutation score must stay ≥ 90
|
|
|
697
727
|
|
|
698
728
|
**Test contract.** Behavior is pinned by four kinds of tests, split by filename:
|
|
699
729
|
|
|
700
|
-
- `*.test.ts
|
|
701
|
-
- `*.error.test.ts
|
|
702
|
-
- `*.property.test.ts
|
|
703
|
-
- `*.mutation.test.ts
|
|
730
|
+
- `*.test.ts`. Features and happy paths, hand-maintained.
|
|
731
|
+
- `*.error.test.ts`. Failure paths: warnings, budgets, abort, unregistered, the `__error` state.
|
|
732
|
+
- `*.property.test.ts`. Property-based invariants against a reference model (fast-check, replayable via `MANTAQ_SEED`).
|
|
733
|
+
- `*.mutation.test.ts`. Directed tests that kill specific mutants. Regenerable and thrown away between stryker runs.
|
|
704
734
|
|
|
705
|
-
Coverage is analyzed per test (stryker `perTest` coverage analysis) so every mutant is attributed to the test that pins it. The mutation break threshold is 90
|
|
735
|
+
Coverage is analyzed per test (stryker `perTest` coverage analysis) so every mutant is attributed to the test that pins it. The mutation break threshold is 90. A change that drops the score below 90 fails the build.
|
|
706
736
|
|
|
707
737
|
## License
|
|
708
738
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,16 +1,41 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
interface Clock {
|
|
2
|
+
setTimeout(ms: number, options: ClockTimerOptions): number;
|
|
3
|
+
clearTimeout(id: number): void;
|
|
4
|
+
setInterval(ms: number, options: ClockIntervalOptions): number;
|
|
5
|
+
clearInterval(id: number): void;
|
|
6
|
+
now(): number;
|
|
7
|
+
/** Optional hook for draining queues after clock advances (VirtualClock). */
|
|
8
|
+
setDrain?(fn: () => void): void;
|
|
9
|
+
}
|
|
10
|
+
interface ClockTimerOptions {
|
|
11
|
+
cb: () => void;
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
eventName?: string;
|
|
14
|
+
}
|
|
15
|
+
interface ClockIntervalOptions {
|
|
16
|
+
cb: () => void;
|
|
17
|
+
signal?: AbortSignal;
|
|
18
|
+
}
|
|
19
|
+
export interface Context<T> {
|
|
20
|
+
get(): T;
|
|
21
|
+
set(value: T): void;
|
|
22
|
+
}
|
|
23
|
+
/** Two-way handle over an owner's context: reads see live values, writes flag dirty state upstream. */
|
|
24
|
+
export declare function Context<T>(options: {
|
|
25
|
+
get: () => T;
|
|
26
|
+
set: (value: T) => void;
|
|
27
|
+
}): Context<T>;
|
|
28
|
+
export declare function state<const T extends string>(name: T): <Payload = unknown>() => StateRef<T, Payload, false>;
|
|
3
29
|
type AnyStateRef = StateRef<string, unknown, boolean>;
|
|
4
30
|
interface RegionOptions {
|
|
5
31
|
initial: string;
|
|
6
32
|
states: Record<string, AnyStateRef>;
|
|
7
33
|
}
|
|
8
34
|
type RegionsOptions = Record<string, RegionOptions>;
|
|
9
|
-
|
|
35
|
+
export interface StateRef<T extends string = string, Payload = unknown, IsFinal extends boolean = false> {
|
|
10
36
|
readonly name: T;
|
|
11
37
|
readonly isFinal: IsFinal;
|
|
12
|
-
/** @internal */ _regions
|
|
13
|
-
constructor(name: T, isFinal: IsFinal);
|
|
38
|
+
/** @internal */ _regions?: RegionsOptions;
|
|
14
39
|
regions(options: RegionsOptions): this;
|
|
15
40
|
final(): StateRef<T, Payload, true>;
|
|
16
41
|
create(payload: Payload): {
|
|
@@ -18,104 +43,15 @@ declare class StateRef<T extends string, Payload = unknown, IsFinal extends bool
|
|
|
18
43
|
payload: Payload;
|
|
19
44
|
};
|
|
20
45
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
type
|
|
26
|
-
type: string;
|
|
27
|
-
payload?: unknown;
|
|
28
|
-
};
|
|
29
|
-
type CreatedOfEvent<T extends string, P> = P extends void ? {
|
|
30
|
-
type: T;
|
|
31
|
-
} : {
|
|
32
|
-
type: T;
|
|
33
|
-
payload: P;
|
|
34
|
-
};
|
|
35
|
-
declare class EventRef<const T extends string, Payload extends object | void = void> {
|
|
36
|
-
readonly type: T;
|
|
37
|
-
constructor(type: T);
|
|
38
|
-
is(anyEvent: unknown): anyEvent is CreatedOfEvent<T, Payload>;
|
|
39
|
-
create(): Payload extends void ? {
|
|
40
|
-
type: T;
|
|
41
|
-
} : void;
|
|
42
|
-
create(payload: Payload): Payload extends void ? {
|
|
43
|
-
type: T;
|
|
44
|
-
} : {
|
|
45
|
-
type: T;
|
|
46
|
-
payload: Payload;
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
//#endregion
|
|
50
|
-
//#region src/clock.d.ts
|
|
51
|
-
interface Clock {
|
|
52
|
-
setTimeout(ms: number, cb: () => void, options?: {
|
|
53
|
-
signal?: AbortSignal;
|
|
54
|
-
eventName?: string;
|
|
55
|
-
}): number;
|
|
56
|
-
clearTimeout(id: number): void;
|
|
57
|
-
setInterval(ms: number, cb: () => void, options?: {
|
|
58
|
-
signal?: AbortSignal;
|
|
59
|
-
}): number;
|
|
60
|
-
clearInterval(id: number): void;
|
|
61
|
-
now(): number;
|
|
62
|
-
/** Optional hook for draining queues after clock advances (VirtualClock). */
|
|
63
|
-
setDrain?(fn: () => void): void;
|
|
64
|
-
}
|
|
65
|
-
//#endregion
|
|
66
|
-
//#region src/real-clock.d.ts
|
|
67
|
-
declare class RealClock implements Clock {
|
|
68
|
-
#private;
|
|
69
|
-
now(): number;
|
|
70
|
-
setTimeout(ms: number, cb: () => void, options?: {
|
|
71
|
-
signal?: AbortSignal;
|
|
72
|
-
eventName?: string;
|
|
73
|
-
}): number;
|
|
74
|
-
clearTimeout(id: number): void;
|
|
75
|
-
setInterval(ms: number, cb: () => void, options?: {
|
|
76
|
-
signal?: AbortSignal;
|
|
77
|
-
}): number;
|
|
78
|
-
clearInterval(id: number): void;
|
|
79
|
-
}
|
|
80
|
-
//#endregion
|
|
81
|
-
//#region src/virtual-clock.d.ts
|
|
82
|
-
declare class VirtualClock implements Clock {
|
|
83
|
-
#private;
|
|
84
|
-
now(): number;
|
|
85
|
-
setTimeout(ms: number, cb: () => void, options?: {
|
|
86
|
-
signal?: AbortSignal;
|
|
87
|
-
eventName?: string;
|
|
88
|
-
}): number;
|
|
89
|
-
clearTimeout(id: number): void;
|
|
90
|
-
setInterval(ms: number, cb: () => void, options?: {
|
|
91
|
-
signal?: AbortSignal;
|
|
92
|
-
}): number;
|
|
93
|
-
clearInterval(id: number): void;
|
|
94
|
-
advance(ms: number): void;
|
|
95
|
-
hasPending(): boolean;
|
|
96
|
-
pendingTimers(): Array<{
|
|
97
|
-
id: number;
|
|
98
|
-
deadline: number;
|
|
99
|
-
ms: number;
|
|
100
|
-
eventName?: string;
|
|
101
|
-
}>;
|
|
102
|
-
setDrain(fn: () => void): void;
|
|
103
|
-
}
|
|
104
|
-
//#endregion
|
|
105
|
-
//#region src/context.d.ts
|
|
106
|
-
declare class Context<T> {
|
|
107
|
-
#private;
|
|
108
|
-
constructor(get: () => T, set: (value: T) => void);
|
|
109
|
-
get(): T;
|
|
110
|
-
set(value: T): void;
|
|
111
|
-
}
|
|
112
|
-
//#endregion
|
|
113
|
-
//#region src/actor-types.d.ts
|
|
114
|
-
type CreatedOf<E extends AnyEventRef> = E extends EventRef<infer Type, infer P> ? CreatedOfEvent<Type, P> : never;
|
|
46
|
+
export declare function isStateRef(value: unknown): value is StateRef;
|
|
47
|
+
export declare function StateRef<T extends string, Payload = unknown, IsFinal extends boolean = false>(name: T, options: {
|
|
48
|
+
isFinal: IsFinal;
|
|
49
|
+
}): StateRef<T, Payload, IsFinal>;
|
|
50
|
+
type CreatedOf<E extends EventRef<string, object | void>> = E extends EventRef<infer Type, infer P> ? CreatedOfEvent<Type, P> : never;
|
|
115
51
|
type NonFinalStateRef<States extends AnyStateRef[]> = Extract<States[number], {
|
|
116
52
|
isFinal: false;
|
|
117
53
|
}>;
|
|
118
|
-
interface EffectInput<ActorContext, Payload = unknown> {
|
|
54
|
+
interface EffectInput<ActorContext, Payload = unknown, Emit = InternalEvent> {
|
|
119
55
|
signal: AbortSignal;
|
|
120
56
|
state: {
|
|
121
57
|
name: string;
|
|
@@ -123,10 +59,12 @@ interface EffectInput<ActorContext, Payload = unknown> {
|
|
|
123
59
|
};
|
|
124
60
|
event: InternalEvent;
|
|
125
61
|
context: Context<ActorContext>;
|
|
126
|
-
emit: (event:
|
|
62
|
+
emit: (event: Emit) => void;
|
|
127
63
|
clock: Clock;
|
|
128
64
|
}
|
|
129
|
-
type EffectFn<ActorContext, Payload = unknown> = (input: EffectInput<ActorContext, Payload>) => void | Promise<void
|
|
65
|
+
type EffectFn<ActorContext, Payload = unknown, Emit = InternalEvent> = (input: EffectInput<ActorContext, Payload, Emit>) => void | Promise<void> | {
|
|
66
|
+
then(onFulfilled: unknown, onRejected: unknown): unknown;
|
|
67
|
+
};
|
|
130
68
|
type ErrorReason = "transition" | "effect" | "budget" | "output" | "internal" | "async" | "unhandled";
|
|
131
69
|
interface ErrorInfo {
|
|
132
70
|
error: unknown;
|
|
@@ -140,6 +78,8 @@ interface TransitionInfo {
|
|
|
140
78
|
from: string;
|
|
141
79
|
to: string;
|
|
142
80
|
transitioned: boolean;
|
|
81
|
+
/** Names of effects invoked on entry into `to`, in registration order. */
|
|
82
|
+
effects: string[];
|
|
143
83
|
}
|
|
144
84
|
type ErrorState = StateRef<"__error", unknown, true>;
|
|
145
85
|
type TransitionResult<AllowedState extends AnyStateRef = AnyStateRef, AllowedEmit extends string = string> = {
|
|
@@ -153,8 +93,6 @@ type TransitionResult<AllowedState extends AnyStateRef = AnyStateRef, AllowedEmi
|
|
|
153
93
|
payload?: unknown;
|
|
154
94
|
}>;
|
|
155
95
|
};
|
|
156
|
-
//#endregion
|
|
157
|
-
//#region src/actor-internal.d.ts
|
|
158
96
|
interface Snapshot<C = unknown> {
|
|
159
97
|
path: string[];
|
|
160
98
|
context: C;
|
|
@@ -163,17 +101,17 @@ interface Snapshot<C = unknown> {
|
|
|
163
101
|
done?: boolean;
|
|
164
102
|
error?: ErrorInfo;
|
|
165
103
|
}
|
|
104
|
+
type SubscriberEventName = "change" | "done" | "transition" | "error" | "output";
|
|
105
|
+
type SubscriberHandler<C, E extends SubscriberEventName> = E extends "change" ? (snapshot: Snapshot<C>, prev: Snapshot<C>) => void : E extends "done" ? () => void : E extends "transition" ? (info: TransitionInfo) => void : E extends "error" ? (info: ErrorInfo) => void : E extends "output" ? (event: InternalEvent) => void : never;
|
|
166
106
|
interface AnyActor<C = Record<string, unknown>> {
|
|
167
107
|
state: AnyStateRef;
|
|
168
108
|
clock: Clock;
|
|
169
109
|
regions: Record<string, AnyActor>;
|
|
170
|
-
send(event:
|
|
110
|
+
send(event: InternalEvent): void;
|
|
171
111
|
snapshot(): Snapshot<C>;
|
|
172
|
-
on(event:
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
on(event: "error", fn: (info: ErrorInfo) => void): () => void;
|
|
176
|
-
on(event: "output", fn: (event: InternalEvent) => void): () => void;
|
|
112
|
+
on<E extends SubscriberEventName>(event: E, options: {
|
|
113
|
+
fn: SubscriberHandler<C, E>;
|
|
114
|
+
}): () => void;
|
|
177
115
|
recover(target: {
|
|
178
116
|
state: AnyStateRef;
|
|
179
117
|
context: C;
|
|
@@ -182,6 +120,7 @@ interface AnyActor<C = Record<string, unknown>> {
|
|
|
182
120
|
context?: C;
|
|
183
121
|
options?: {
|
|
184
122
|
transitions?: Record<string, Record<string, unknown>>;
|
|
123
|
+
targets?: Record<string, Record<string, string[]>>;
|
|
185
124
|
effects?: Record<string, unknown[]>;
|
|
186
125
|
states?: ReadonlyArray<{
|
|
187
126
|
name: string;
|
|
@@ -191,36 +130,91 @@ interface AnyActor<C = Record<string, unknown>> {
|
|
|
191
130
|
inject(event: InternalEvent): void;
|
|
192
131
|
dispose(): void;
|
|
193
132
|
}
|
|
194
|
-
|
|
195
|
-
//#region src/builder.d.ts
|
|
196
|
-
type EventTypeOf<E extends AnyEventRef> = E extends EventRef<infer Type, object | void> ? Type : never;
|
|
197
|
-
type PayloadOf<S extends AnyStateRef> = S extends StateRef<infer _Name, infer Payload, infer _IsFinal> ? Payload : never;
|
|
198
|
-
type TransitionHandler<States extends readonly AnyStateRef[], ActorContext> = (event: InternalEvent, opts: {
|
|
133
|
+
type RuntimeTransitionHandler<States extends readonly AnyStateRef[], ActorContext> = (event: InternalEvent, options: {
|
|
199
134
|
context: Context<ActorContext>;
|
|
200
|
-
actor: AnyActor
|
|
135
|
+
actor: AnyActor<ActorContext>;
|
|
201
136
|
}) => TransitionResult<States[number], string>;
|
|
137
|
+
type EventTypeOf<E extends AnyEventRef> = E extends EventRef<infer Type, object | void> ? Type : never;
|
|
138
|
+
type PayloadOf<S extends AnyStateRef> = S extends StateRef<infer _Name, infer Payload, infer _IsFinal> ? Payload : never;
|
|
139
|
+
type TransitionHandler<States extends readonly AnyStateRef[], ActorContext> = RuntimeTransitionHandler<States, ActorContext>;
|
|
140
|
+
interface EffectEntry<ActorContext> {
|
|
141
|
+
name: string;
|
|
142
|
+
sends?: string[];
|
|
143
|
+
fn: EffectFn<ActorContext>;
|
|
144
|
+
}
|
|
145
|
+
type EmitOf<E> = E extends readonly AnyEventRef[] ? CreatedOf<E[number]> : InternalEvent;
|
|
146
|
+
type SendList<I extends readonly AnyEventRef[]> = readonly I[number][] | undefined;
|
|
202
147
|
interface BuiltMaps<States extends readonly AnyStateRef[], ActorContext> {
|
|
203
148
|
transitions: Record<string, Record<string, TransitionHandler<States, ActorContext>>>;
|
|
204
|
-
|
|
149
|
+
/** Declared `to` state names per `[state][event]`; `Any` keys `onAny`. Static graph for tooling. */
|
|
150
|
+
targets: Record<string, Record<string, string[]>>;
|
|
151
|
+
effects: Record<string, Array<EffectEntry<ActorContext>>>;
|
|
152
|
+
}
|
|
153
|
+
/** Handler may only return states listed in `to`; no `to` means it can only stay. */
|
|
154
|
+
type HandlerFn<To extends AnyStateRef, Outputs extends readonly AnyEventRef[], ActorContext, E extends AnyEventRef> = (event: E extends EventRef<infer Type, infer P> ? CreatedOfEvent<Type, P> : never, opts: {
|
|
155
|
+
context: Context<ActorContext>;
|
|
156
|
+
actor: AnyActor<ActorContext>;
|
|
157
|
+
}) => TransitionResult<To, EventTypeOf<Outputs[number]>>;
|
|
158
|
+
interface HandlerOptions<To extends readonly AnyStateRef[], Outputs extends readonly AnyEventRef[], ActorContext, E extends AnyEventRef> {
|
|
159
|
+
event: E;
|
|
160
|
+
to?: To;
|
|
161
|
+
handler: HandlerFn<NoInfer<To[number]>, Outputs, ActorContext, E>;
|
|
205
162
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
/** @internal */ build(): BuiltMaps<States, ActorContext>;
|
|
163
|
+
interface ActorBuilder<States extends readonly AnyStateRef[], Inputs extends readonly AnyEventRef[], Internal extends readonly AnyEventRef[], Outputs extends readonly AnyEventRef[], ActorContext> {
|
|
164
|
+
on<S extends States[number], E extends Inputs[number] | Internal[number], const To extends readonly States[number][] = readonly []>(stateRef: S, options: HandlerOptions<To, Outputs, ActorContext, E>): ActorBuilder<States, Inputs, Internal, Outputs, ActorContext>;
|
|
165
|
+
onAny<E extends Inputs[number] | Internal[number], const To extends readonly States[number][] = readonly []>(options: HandlerOptions<To, Outputs, ActorContext, E>): ActorBuilder<States, Inputs, Internal, Outputs, ActorContext>;
|
|
166
|
+
/** `sends`: internal events `fn` may emit; `emit` only accepts those. Omit for untyped `emit`. */
|
|
167
|
+
effect<S extends States[number], const E extends SendList<Internal> = undefined>(stateRef: S, definition: {
|
|
168
|
+
name: string;
|
|
169
|
+
sends?: E;
|
|
170
|
+
fn: EffectFn<ActorContext, PayloadOf<S>, EmitOf<E>>;
|
|
171
|
+
}): ActorBuilder<States, Inputs, Internal, Outputs, ActorContext>;
|
|
172
|
+
/** @internal */
|
|
173
|
+
build(): BuiltMaps<States, ActorContext>;
|
|
218
174
|
}
|
|
175
|
+
declare function ActorBuilder<States extends readonly AnyStateRef[], Inputs extends readonly AnyEventRef[], Internal extends readonly AnyEventRef[], Outputs extends readonly AnyEventRef[], ActorContext>(): ActorBuilder<States, Inputs, Internal, Outputs, ActorContext>;
|
|
219
176
|
type SetupFn<States extends readonly AnyStateRef[], Inputs extends readonly AnyEventRef[], Internal extends readonly AnyEventRef[], Outputs extends readonly AnyEventRef[], ActorContext> = (m: ActorBuilder<States, Inputs, Internal, Outputs, ActorContext>) => void;
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
type
|
|
223
|
-
|
|
177
|
+
export declare function event<const T extends string>(type: T): <Payload extends object | void = void>() => EventRef<T, Payload>;
|
|
178
|
+
export interface EventRef<T extends string = string, Payload extends object | void = void> {
|
|
179
|
+
readonly type: T;
|
|
180
|
+
is(anyEvent: unknown): anyEvent is CreatedOfEvent<T, Payload>;
|
|
181
|
+
create(): Payload extends void ? {
|
|
182
|
+
type: T;
|
|
183
|
+
} : void;
|
|
184
|
+
create(payload: Payload): Payload extends void ? {
|
|
185
|
+
type: T;
|
|
186
|
+
} : {
|
|
187
|
+
type: T;
|
|
188
|
+
payload: Payload;
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
type AnyEventRef = EventRef<string, object | void>;
|
|
192
|
+
type CreatedOfEvent<T extends string, P> = P extends void ? {
|
|
193
|
+
type: T;
|
|
194
|
+
} : {
|
|
195
|
+
type: T;
|
|
196
|
+
payload: P;
|
|
197
|
+
};
|
|
198
|
+
export declare function EventRef<const T extends string, Payload extends object | void = void>(type: T): EventRef<T, Payload>;
|
|
199
|
+
export declare function RealClock(): Clock;
|
|
200
|
+
export interface VirtualClock {
|
|
201
|
+
now(): number;
|
|
202
|
+
setTimeout(ms: number, options: ClockTimerOptions): number;
|
|
203
|
+
clearTimeout(id: number): void;
|
|
204
|
+
setInterval(ms: number, options: ClockIntervalOptions): number;
|
|
205
|
+
clearInterval(id: number): void;
|
|
206
|
+
advance(ms: number): void;
|
|
207
|
+
hasPending(): boolean;
|
|
208
|
+
pendingTimers(): Array<{
|
|
209
|
+
id: number;
|
|
210
|
+
deadline: number;
|
|
211
|
+
ms: number;
|
|
212
|
+
eventName?: string;
|
|
213
|
+
}>;
|
|
214
|
+
setDrain(fn: () => void): void;
|
|
215
|
+
}
|
|
216
|
+
export declare function VirtualClock(): VirtualClock;
|
|
217
|
+
type InitialState<S extends AnyStateRef> = S extends StateRef<infer _N extends string, infer P, infer _I extends boolean> ? [unknown] extends [P] ? S | {
|
|
224
218
|
state: S;
|
|
225
219
|
payload?: P;
|
|
226
220
|
} : {
|
|
@@ -241,40 +235,37 @@ interface ActorOptions<States extends readonly AnyStateRef[], Inputs extends rea
|
|
|
241
235
|
}
|
|
242
236
|
interface InternalActorOptions<States extends readonly AnyStateRef[], Inputs extends readonly AnyEventRef[], Internal extends readonly AnyEventRef[], Outputs extends readonly AnyEventRef[], ActorContext = Record<string, unknown>> extends Omit<ActorOptions<States, Inputs, Internal, Outputs, ActorContext>, "setup"> {
|
|
243
237
|
transitions: TransitionDispatch<States, ActorContext>;
|
|
244
|
-
|
|
238
|
+
targets: Record<string, Record<string, string[]>>;
|
|
239
|
+
effects: Record<string, Array<EffectEntry<ActorContext>>>;
|
|
245
240
|
}
|
|
246
|
-
type TransitionDispatch<States extends readonly AnyStateRef[], ActorContext> = Record<string, Record<string,
|
|
241
|
+
type TransitionDispatch<States extends readonly AnyStateRef[], ActorContext> = Record<string, Record<string, TransitionStepFn<States, ActorContext> | undefined>>;
|
|
242
|
+
type TransitionStepFn<States extends readonly AnyStateRef[], ActorContext> = (event: InternalEvent, options: {
|
|
247
243
|
context: Context<ActorContext>;
|
|
248
|
-
actor: AnyActor
|
|
249
|
-
}) => TransitionResult<States[number], string
|
|
250
|
-
|
|
251
|
-
#private;
|
|
244
|
+
actor: AnyActor<ActorContext>;
|
|
245
|
+
}) => TransitionResult<States[number], string>;
|
|
246
|
+
export interface Actor<States extends readonly AnyStateRef[], Inputs extends readonly AnyEventRef[], Internal extends readonly AnyEventRef[] = readonly [], Outputs extends readonly AnyEventRef[] = readonly [], ActorContext = Record<string, unknown>> extends AnyActor<ActorContext> {
|
|
252
247
|
state: States[number] | ErrorState;
|
|
253
248
|
readonly clock: Clock;
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
on(event: "transition", fn: (info: TransitionInfo) => void): () => void;
|
|
261
|
-
on(event: "error", fn: (info: ErrorInfo) => void): () => void;
|
|
262
|
-
on(event: "output", fn: (event: InternalEvent) => void): () => void;
|
|
249
|
+
readonly context: ActorContext;
|
|
250
|
+
readonly regions: Record<string, AnyActor>;
|
|
251
|
+
readonly options: InternalActorOptions<States, Inputs, Internal, Outputs, ActorContext>;
|
|
252
|
+
on<E extends SubscriberEventName>(event: E, options: {
|
|
253
|
+
fn: SubscriberHandler<ActorContext, E>;
|
|
254
|
+
}): () => void;
|
|
263
255
|
settled(): Promise<void>;
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
* Restore a live state after `__error`. DANGER: caller supplies state+context,
|
|
267
|
-
* breaking determinism. Effects not re-run; processing resumes on next event.
|
|
268
|
-
*/
|
|
256
|
+
pendingEffectCount(): number;
|
|
257
|
+
send(event: CreatedOf<Inputs[number]>): void;
|
|
269
258
|
recover(target: {
|
|
270
259
|
state: States[number];
|
|
271
260
|
context: ActorContext;
|
|
272
261
|
}): void;
|
|
273
262
|
snapshot(): Snapshot<ActorContext>;
|
|
274
|
-
/** Push an internal event and drain. Unknown types route to output subscribers. */
|
|
275
263
|
inject(event: InternalEvent): void;
|
|
276
|
-
/** Stop for good: abort effect, clear queue and subscribers. Later sends/injects ignored. */
|
|
277
264
|
dispose(): void;
|
|
278
265
|
}
|
|
279
|
-
|
|
280
|
-
export
|
|
266
|
+
export declare function Actor<const States extends readonly AnyStateRef[], const Inputs extends readonly AnyEventRef[], const Internal extends readonly AnyEventRef[] = readonly [], const Outputs extends readonly AnyEventRef[] = readonly [], ActorContext = Record<string, unknown>>(options: ActorOptions<States, Inputs, Internal, Outputs, ActorContext>): Actor<States, Inputs, Internal, Outputs, ActorContext>;
|
|
267
|
+
export type InternalEvent = {
|
|
268
|
+
type: string;
|
|
269
|
+
payload?: unknown;
|
|
270
|
+
};
|
|
271
|
+
export type { ActorBuilder, ActorOptions, AnyActor, AnyEventRef, AnyStateRef, BuiltMaps, Clock, CreatedOf, CreatedOfEvent, EffectFn, EffectInput, ErrorInfo, ErrorReason, ErrorState, EventTypeOf, InitialState, NonFinalStateRef, PayloadOf, SetupFn, Snapshot, TransitionHandler, TransitionInfo, TransitionResult };
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Either as e}from"@mantaq/utils";function t(e){return()=>new n(e,!1)}var n=class e{name;isFinal;_regions;constructor(e,t){this.name=e,this.isFinal=t}regions(e){return this._regions=e,this}final(){let t=new e(this.name,!0);return t._regions=this._regions,t}create(e){return{state:this,payload:e}}},r=class{#e=Date.now();now(){return Date.now()-this.#e}setTimeout(e,t,n){if(n?.signal?.aborted)return-1;let r,i=Number(globalThis.setTimeout(()=>{r&&n?.signal&&n.signal.removeEventListener(`abort`,r),t()},e));return n?.signal&&(r=()=>globalThis.clearTimeout(i),n.signal.addEventListener(`abort`,r,{once:!0})),i}clearTimeout(e){globalThis.clearTimeout(e)}setInterval(e,t,n){let r=Number(globalThis.setInterval(t,e));return n?.signal&&n.signal.addEventListener(`abort`,()=>globalThis.clearInterval(r),{once:!0}),r}clearInterval(e){globalThis.clearInterval(e)}};function i(e,t,n){let r=e?()=>{n.delete(t)}:void 0;return e&&r&&e.addEventListener(`abort`,r,{once:!0}),r}function a(e){e.signal&&e.onAbort&&e.signal.removeEventListener(`abort`,e.onAbort)}var o=class{#e=0;#t=new Map;#n=new Map;#r=1;#i=null;now(){return this.#e}#a(e,t,n){if(!Number.isFinite(e))throw RangeError(`[VirtualClock] invalid ${t} ms value: ${e}`);return e<=0?+!!n:e}setTimeout(e,t,n){let r=n?.signal;if(r?.aborted)return-1;let a=this.#r++,o=i(r,a,this.#t);return this.#t.set(a,{deadline:this.#e+this.#a(e,`setTimeout`,!1),cb:t,signal:r,onAbort:o,eventName:n?.eventName}),a}clearTimeout(e){let t=this.#t.get(e);t&&(a(t),this.#t.delete(e))}setInterval(e,t,n){let r=n?.signal;if(r?.aborted)return-1;let a=this.#r++,o=i(r,a,this.#n),s=this.#a(e,`setInterval`,!0);return this.#n.set(a,{ms:s,next:this.#e+s,cb:t,signal:r,onAbort:o}),a}clearInterval(e){let t=this.#n.get(e);t&&(a(t),this.#n.delete(e))}#o(e){let t=e,n=!1;for(let r of this.#t.values())r.deadline<=e&&r.deadline<=t&&(t=r.deadline,n=!0);for(let r of this.#n.values())r.next<=e&&r.next<=t&&(t=r.next,n=!0);return n?t:null}#s(e){let t=[];for(let[n,r]of this.#t)r.deadline===e&&t.push(n);for(let e of t){let t=this.#t.get(e);t&&(a(t),this.#t.delete(e),t.cb())}}#c(e){let t=[];for(let[n,r]of this.#n)r.next===e&&t.push(n);for(let e of t){let t=this.#n.get(e);t&&(t.cb(),this.#n.has(e)&&(t.next=this.#e+t.ms))}}advance(e){if(!Number.isFinite(e))throw RangeError(`[VirtualClock] invalid advance ms value: ${e}`);let t=this.#e+Math.max(0,e);for(;;){let e=this.#o(t);if(e===null)break;this.#e=e,this.#s(e),this.#c(e)}this.#e=t,this.#i?.()}hasPending(){return this.#t.size>0||this.#n.size>0}pendingTimers(){let e=[];for(let[t,n]of this.#t)e.push({id:t,deadline:n.deadline,ms:n.deadline-this.#e,eventName:n.eventName});return e}setDrain(e){this.#i=e}},s=class{#e=[];#t=0;#n=!1;#r=!1;#i=[];get length(){return this.#e.length-this.#t}push(...e){this.#r||this.#e.push(...e)}clear(){let e=this.#i.splice(0);this.#e.length=0,this.#t=0;for(let t of e)t()}settled(){return this.length===0&&!this.#n?Promise.resolve():new Promise(e=>{this.#i.push(e)})}processCancellable(e){this.#a(e)}#a(e){if(!this.#n){this.#n=!0;try{for(;this.#t<this.#e.length&&!this.#r;){let t=this.#e[this.#t++];if(!e(t)){this.#r=!0;break}}}finally{this.#e.length=0,this.#t=0,this.#n=!1,this.#r=!1;let e=this.#i.splice(0);for(let t of e)t()}}}},c=class{change=new Set;done=new Set;transition=new Set;error=new Set;#e=null;seed(e){this.#e=e}addChange(e){this.change.add(e);let t=this.#e;return t&&this.#t(()=>e(t,t)),()=>this.change.delete(e)}addDone(e){return this.done.add(e),()=>this.done.delete(e)}addTransition(e){return this.transition.add(e),()=>this.transition.delete(e)}addError(e){this.error.add(e);let t=this.#e?.error;return t&&this.#t(()=>e(t)),()=>this.error.delete(e)}emitChange(e){let t=this.#e??e;this.#e=e;for(let n of this.change)this.#t(()=>n(e,t))}emitDone(){for(let e of this.done)this.#t(e)}emitTransition(e){for(let t of this.transition)this.#t(()=>t(e))}emitError(e){for(let t of this.error)this.#t(()=>t(e))}clear(){this.change.clear(),this.done.clear(),this.transition.clear(),this.error.clear(),this.output.clear()}#t(t){e.from(t)}output=new Set;addOutput(e){return this.output.add(e),()=>this.output.delete(e)}emitOutput(e){for(let t of this.output)t(e)}};function l(e,t,n,r={}){let i=[e.name],a={};for(let[e,n]of Object.entries(t))a[e]=n.snapshot();let o={path:i,context:n,regions:a};return r.payload!==void 0&&(o.payload=r.payload),e.isFinal&&(o.done=!0),r.error!==void 0&&(o.error=r.error),o}function u(t){let n=[],r=t.effects[t.state.name];if(!r)return{pending:n};for(let i of r){if(t.abort.signal.aborted)break;let r=e.from(()=>i({signal:t.abort.signal,state:{name:t.state.name,payload:t.statePayload},event:t.event,context:t.context,emit:t.emit,clock:t.clock}));if(r[0]!==void 0){t.onError(r[0],t.lastGood);break}let a=r[1];a instanceof Promise&&n.push(a.catch(e=>{t.abort.signal.aborted||t.onError(e,t.lastGood)}))}return{pending:n}}function d(e){if(e.state)return e.state instanceof n?{state:e.state,payload:e.payload}:{state:e.state.state,payload:e.state.payload}}var f=class{#e;#t;constructor(e,t){this.#e=e,this.#t=t}get(){return this.#e()}set(e){this.#t(e)}},p=class{#e={};#t={};on(e,t,n){let r=e.name,i=t.type;return(this.#e[r]??={})[i]=n,this}onAny(e,t){let n=e.type;return(this.#e.Any??={})[n]=t,this}effect(e,t){return(this.#t[e.name]??=[]).push(t),this}build(){return{transitions:this.#e,effects:this.#t}}};function m(e){return e instanceof n?e:e.state}var h=class{state;clock;#e;#t;#n=!1;#r;#i;#a={};#o=new s;#s=new c;#c=null;#l;#u;#d;#f=!1;get context(){return this.#e}get regions(){return this.#a}get options(){return this.#i}constructor(e){let t=new p;e.setup(t);let i=t.build();this.#i={...e,transitions:i.transitions,effects:i.effects},this.#l=new Set((e.internal??[]).map(e=>e.type)),this.#u=new Set(e.inputs.map(e=>e.type)),this.#d=e.internalBudget??1e4,this.clock=e.clock??new r,this.clock.setDrain?.(()=>this.#x());let a=m(e.initial),o=new Set(e.states.map(e=>e.name));if(!o.has(a.name))throw Error(`[Actor] initial state "${a.name}" not found in declared states [${[...o].join(`, `)}]`);if(this.state=a,this.#S=e.initial instanceof n?void 0:e.initial.payload,this.#e=e.context??{},this.#r=a,this.#t=new f(()=>this.#e,e=>{this.#e=e,this.#n=!0}),e.regions)for(let[t,n]of Object.entries(e.regions))this.#a[t]=n,n.on(`output`,e=>{this.#o.push(e),this.#x()});this.#b({type:`__init`},this.#S),this.#s.seed(this.snapshot())}on(e,t){return{change:e=>this.#s.addChange(e),done:e=>this.#s.addDone(e),transition:e=>this.#s.addTransition(e),error:e=>this.#s.addError(e),output:e=>this.#s.addOutput(e)}[e](t)}settled(){return Promise.all([this.#o.settled(),...this.#C]).then(()=>void 0)}send(e){this.#k||(this.#m(e),this.#p())}recover(e){this.#T===null||this.#k||(this.#T=null,this.#o=new s,this.#c=null,this.state=e.state,this.#S=void 0,this.#e=e.context,this.#r=e.state,this.#n=!1,this.#E=null,this.#C=[],this.#s.emitChange(this.snapshot()))}#p(){this.state===this.#r&&!this.#n||(this.#r=this.state,this.#n=!1,this.#s.emitChange(this.snapshot()))}#m(e){if(this.#T!==null||this.state.isFinal)return;let t={...e,payload:e.payload??{}},n=this.state.name,r=this.#E;this.#E={state:this.state,context:this.#e};try{let r=this.#i.transitions,i=r[this.state.name]?.[e.type],a=r.Any?.[e.type],o=this.#h(t,i,a);this.#g(t,n,!!(i||a),o),o.emitted||this.#o.length>0?this.#x():!i&&!a&&this.#l.has(e.type)&&this.#O(`unhandled`,e,Error(`[Actor] internal event "${e.type}" emitted but no handler in state "${this.state.name}"`))}finally{this.#E=r}}#h(e,t,n){let r=t?this.#_(e,t):{applied:!1};if(this.#T===null){let t=this.#v(e,n,!r.applied);return{emitted:t.emitted,transitioned:r.applied||t.transitioned,target:r.target??t.target}}return{emitted:!1,transitioned:!1}}#g(e,t,n,r){this.#T===null&&n&&this.#s.emitTransition({event:e,from:t,to:r.target??this.state.name,transitioned:r.transitioned})}#_(e,t){let n=!1,r;return this.#D(`transition`,e,()=>{let i=t(e,{context:this.#t,actor:this});i.emit&&this.#o.push(...i.emit),i.state&&(r=this.#y(e,i),n=!0)}),{applied:n,target:r}}#v(e,t,n){if(t===void 0)return{emitted:!1,transitioned:!1};let r=!1,i=!1,a;return this.#D(`transition`,e,()=>{let o=t(e,{context:this.#t,actor:this});o.state&&n&&(a=this.#y(e,o),i=!0),o.emit&&(this.#o.push(...o.emit),r=!0)}),{emitted:r,transitioned:i,target:a}}snapshot(){return l(this.state,this.#a,this.#e,{error:this.#T??void 0,payload:this.#S})}#y(e,t){if(this.#T!==null||!t.state)return;let r=d(t);if(!r)return;if(!(r.state instanceof n)){this.#O(`transition`,e,Error(`invalid transition target`));return}this.#c?.abort(),this.state=r.state;let i=this.state.name;return this.#S=r.payload,this.#E={state:this.state,context:this.#e},this.#b(e,r.payload),r.state.isFinal&&this.#T===null&&this.#s.emitDone(),i}#b(e,t){if(this.#k)return;let n=this.#i.effects[this.state.name];if(!n){this.#c=null;return}let r=new AbortController;this.#c=r;let i=this.#E??{state:this.state,context:this.#e},a=u({effects:{[this.state.name]:n},state:this.state,statePayload:t,event:e,context:this.#t,emit:e=>{this.#T!==null||r.signal.aborted||(this.#o.push(e),this.#x())},clock:this.clock,abort:r,lastGood:i,onError:t=>this.#O(`effect`,e,t,i)});this.#C.push(...a.pending)}#x(){if(!(this.#f||this.#k)){this.#f=!0;try{let e=this.#d,t=0;this.#o.processCancellable(n=>{if(this.#T!==null)return!1;if(t>=e)return this.#O(`budget`,n,Error(`internal event budget exceeded`)),!1;if(t++,this.#l.has(n.type)||this.#u.has(n.type))this.#m(n);else if(!this.#D(`output`,n,()=>this.#s.emitOutput(n)))return!1;return!0})}finally{this.#f=!1,this.#p()}}}#S;#C=[];#w=new n(`__error`,!0);#T=null;#E=null;#D(t,n,r){let i=e.from(r);return i[0]===void 0||(this.#O(t,n,i[0]),!1)}#O(e,t,n,r){if(this.#T!==null)return;let i=r??this.#E??{state:this.state,context:this.#e};this.#T={error:n,state:i.state,context:i.context,event:t,reason:e},this.#c?.abort(),this.state=this.#w,this.#S=void 0,this.#r=this.#w,this.#n=!1,this.#s.emitError(this.#T),this.#s.emitChange(this.snapshot()),this.#s.emitDone()}inject(e){this.#k||(this.#o.push(e),this.#x())}dispose(){this.#k||(this.#k=!0,this.#c?.abort(),this.#c=null,this.#o.clear(),this.#s.clear())}#k=!1};function g(e){return()=>new _(e)}var _=class{type;constructor(e){this.type=e}is(e){return!!e&&typeof e==`object`&&`type`in e&&e.type===this.type}create(e){return e===void 0?{type:this.type}:{type:this.type,payload:e}}};export{h as Actor,f as Context,_ as EventRef,r as RealClock,n as StateRef,o as VirtualClock,g as event,t as state};
|
|
1
|
+
import{Either as e}from"@mantaq/utils";function t(e){let t=e;return typeof t==`function`?t:()=>({})}function n(e){return e}function r(){let e={},r={},i={};function a(n,i){let a=i.event.type;(e[n]??={})[a]=t(i.handler),(r[n]??={})[a]=(i.to??[]).map(e=>e.name)}let o={on(e,t){return a(e.name,t),o},onAny(e){return a(`Any`,e),o},effect(e,{name:t,sends:r,fn:a}){return(i[e.name]??=[]).push({name:t,...r&&{sends:r.map(e=>e.type)},fn:n(a)}),o},build(){return{transitions:e,targets:r,effects:i}}};return o}function i(e){return{get:e.get,set:e.set}}function a(e){return()=>c(e,{isFinal:!1})}const o=/* @__PURE__ */ new WeakSet;function s(e){return typeof e==`object`&&!!e&&o.has(e)}function c(e,t){let n={name:e,isFinal:t.isFinal,regions(e){return n._regions=e,n},final(){let e=c(n.name,{isFinal:!0});return e._regions=n._regions,e},create(e){return{state:n,payload:e}}};return o.add(n),n}function l(e){let t=e.state;if(t)return`state`in t?{state:t.state,payload:t.payload}:{state:t,payload:e.payload}}function u(e){if(e instanceof Promise)return!0;let t=e;return typeof t==`object`&&!!t&&typeof t.then==`function`}function ee(t){let n=[],r=[],i=t.effects[t.state.name];if(!i)return{pending:n,ran:r};for(let{name:a,fn:o}of i){if(t.abort.signal.aborted)break;let i,s=e.from(()=>(i=o({signal:t.abort.signal,state:{name:t.state.name,payload:t.statePayload},event:t.event,context:t.context,emit:t.emit,clock:t.clock}),!0));if(s[0]!==void 0){t.onError(s[0],t.lastGood);break}if(r.push(a),u(i)){let e=Promise.resolve(i).then(()=>void 0,e=>{t.abort.signal.aborted||t.onError(e,t.lastGood)});n.push(e)}}return{pending:n,ran:r}}function d(e){return e??{}}function f(){let e=[],t=0,n=!1,r=!1,i=[];function a(){let e=i.splice(0);for(let t of e)t()}function o(i){if(!n){n=!0;try{for(;t<e.length&&!r;){let n=e[t];if(t++,!i(n)){r=!0;break}}}finally{e.length=0,t=0,n=!1,r=!1,a()}}}return{get length(){return e.length-t},push(...t){r||e.push(...t)},clear(){e.length=0,t=0,a()},settled(){return e.length-t===0&&!n?Promise.resolve():new Promise(e=>{i.push(e)})},processCancellable(e){o(e)}}}function p(){let e=Date.now(),t=/* @__PURE__ */ new Map;function n(e){let n=t.get(e);n&&(n.signal.removeEventListener(`abort`,n.onAbort),t.delete(e))}return{now(){return Date.now()-e},setTimeout(e,{signal:r,cb:i}){if(r?.aborted)return-1;let a=Number(globalThis.setTimeout(()=>{n(a),i()},e));if(r){function e(){globalThis.clearTimeout(a),n(a)}t.set(a,{signal:r,onAbort:e}),r.addEventListener(`abort`,e,{once:!0})}return a},clearTimeout(e){globalThis.clearTimeout(e),n(e)},setInterval(e,{signal:r,cb:i}){if(r?.aborted)return-1;let a=Number(globalThis.setInterval(i,e));if(r){function e(){globalThis.clearInterval(a),n(a)}t.set(a,{signal:r,onAbort:e}),r.addEventListener(`abort`,e,{once:!0})}return a},clearInterval(e){globalThis.clearInterval(e),n(e)}}}function te(e){let{stateRef:t,regions:n,context:r}=e,i=[t.name],a={};for(let[e,t]of Object.entries(n))a[e]=t.snapshot();let o={path:i,context:r,regions:a};return e.payload!==void 0&&(o.payload=e.payload),t.isFinal&&(o.done=!0),e.error!==void 0&&(o.error={...e.error,context:m(e.error.context)}),o}function m(e){return typeof structuredClone==`function`?structuredClone(e):JSON.parse(JSON.stringify(e))}function h(){let t=/* @__PURE__ */ new Set,n=/* @__PURE__ */ new Set,r=/* @__PURE__ */ new Set,i=/* @__PURE__ */ new Set,a=/* @__PURE__ */ new Set,o;function s(t){e.from(()=>(t(),!0))}return{change:t,done:n,transition:r,error:i,output:a,seed(e){o=e},addChange(e){t.add(e);let n=o;return n&&s(()=>e(n,n)),()=>t.delete(e)},addDone(e){return n.add(e),()=>n.delete(e)},addTransition(e){return r.add(e),()=>r.delete(e)},addError(e){i.add(e);let t=o?.error;return t&&s(()=>e(t)),()=>i.delete(e)},addOutput(e){return a.add(e),()=>a.delete(e)},emitChange(e){let n=o??e;o=e;for(let r of t)s(()=>r(e,n))},emitDone(){for(let e of n)s(e)},emitTransition(e){for(let t of r)s(()=>t(e))},emitError(e){for(let t of i)s(()=>t(e))},emitOutput(e){for(let t of a)t(e)},clear(){t.clear(),n.clear(),r.clear(),i.clear(),a.clear()}}}function g(e,t){let n=e?()=>{t.entries.delete(t.timerId)}:void 0;return e&&n&&e.addEventListener(`abort`,n,{once:!0}),n}function _(e){e.signal&&e.onAbort&&e.signal.removeEventListener(`abort`,e.onAbort)}function v(){let e=0,t=/* @__PURE__ */ new Map,n=/* @__PURE__ */ new Map,r=1,i=/* @__PURE__ */ new Set;function a(e,t){if(!Number.isFinite(e))throw RangeError(`[VirtualClock] invalid ${t.method} ms value: ${e}`);return!0}function o(e,t){return a(e,{method:t.method}),e<=0?+!!t.interval:e}function s(e){let r=e,i=!1;for(let n of t.values())n.deadline<=e&&n.deadline<=r&&(r=n.deadline,i=!0);for(let t of n.values())t.next<=e&&t.next<=r&&(r=t.next,i=!0);return i?r:void 0}function c(e){let n=[];for(let[r,i]of t)i.deadline===e&&n.push(r);for(let e of n){let n=t.get(e);n&&(_(n),t.delete(e),n.cb())}}function l(t){let r=[];for(let[e,i]of n)i.next===t&&r.push(e);for(let t of r){let r=n.get(t);r&&(r.cb(),n.has(t)&&(r.next=e+r.ms))}}return{now(){return e},setTimeout(n,{signal:i,cb:a,eventName:s}){if(i?.aborted)return-1;let c=r++,l=g(i,{timerId:c,entries:t});return t.set(c,{deadline:e+o(n,{method:`setTimeout`,interval:!1}),cb:a,signal:i,onAbort:l,eventName:s}),c},clearTimeout(e){let n=t.get(e);n&&(_(n),t.delete(e))},setInterval(t,{signal:i,cb:a}){if(i?.aborted)return-1;let s=r++,c=g(i,{timerId:s,entries:n}),l=o(t,{method:`setInterval`,interval:!0});return n.set(s,{ms:l,next:e+l,cb:a,signal:i,onAbort:c}),s},clearInterval(e){let t=n.get(e);t&&(_(t),n.delete(e))},advance(t){a(t,{method:`advance`});let n=e+Math.max(0,t),r=-1,o=0;for(;;){let t=s(n);if(t===void 0)break;if(t===r){if(++o>=1e6)break}else o=0,r=t;e=t,c(t),l(t)}e=n;for(let e of i)e()},hasPending(){return t.size>0||n.size>0},pendingTimers(){let n=[];for(let[r,i]of t)n.push({id:r,deadline:i.deadline,ms:i.deadline-e,eventName:i.eventName});return n},setDrain(e){i.add(e)}}}function y(e){return`state`in e?e.state:e}function b(e,t){if(!e.has(t.candidate.name))throw Error(`[Actor] initial state "${t.candidate.name}" not found in declared states [${[...e].join(`, `)}]`);return!0}function x(t){let n=r();t.setup(n);let a=n.build(),o={...t,transitions:a.transitions,targets:a.targets,effects:a.effects},u=new Set((t.internal??[]).map(e=>e.type)),g=new Set(t.inputs.map(e=>e.type)),_=t.internalBudget??1e4,v=t.clock??p();v.setDrain?.(Y);let x,S,C=d(t.context),w,T=!0,E,D={},O=f(),k=h(),A,j=!1,M=[],N,P,F=!1,I=!1,L=c(`__error`,{isFinal:!0}),R=i({get:()=>C,set:e=>{C=e,I=!0,T=!0}}),z=y(t.initial);b(new Set(t.states.map(e=>e.name)),{candidate:z}),x=z;let B=t.initial;if(S=!s(B)&&`payload`in B?B.payload:void 0,E=z,t.regions)for(let[e,n]of Object.entries(t.regions))D[e]=n,n.on(`output`,{fn:e=>{O.push(e),Y()}});let V={get state(){return x},clock:v,get regions(){return D},get context(){return C},get options(){return o},on(e,t){return{change:e=>k.addChange(e),done:e=>k.addDone(e),transition:e=>k.addTransition(e),error:e=>k.addError(e),output:e=>k.addOutput(e)}[e](t.fn)},settled(){return Q()},pendingEffectCount(){return M.length},send(e){F||(W(e),H())},recover:U,snapshot(){return K()},inject(e){ae(e)},dispose(){oe()}};J({type:`__init`},{payloadForState:S}),k.seed(K());function H(){(x!==E||I)&&(E=x,I=!1,k.emitChange(K()))}function U(e){N===void 0||F||(A?.abort(),A=void 0,O.clear(),O=f(),N=void 0,x=e.state,S=void 0,C=e.context,E=e.state,I=!1,T=!0,w=void 0,P=void 0,M=[],k.emitChange(K()))}function W(e){if(N!==void 0||x.isFinal)return;let t={...e,payload:e.payload??{}},n=x.name,r=P;P={state:x,context:C};try{let r=o.transitions,i=r[x.name]?.[e.type],a=r.Any?.[e.type],s=G(t,{stateTransition:i,anyTransition:a});ne(t,{from:n,handled:!!(i||a),outcome:s}),s.emitted||O.length>0?Y():!i&&!a&&u.has(e.type)&&Z(`unhandled`,{event:e,error:/* @__PURE__ */ Error(`[Actor] internal event "${e.type}" emitted but no handler in state "${x.name}"`)})}finally{P=r}}function G(e,t){let n=t.stateTransition?re(e,{transition:t.stateTransition}):{applied:!1,target:void 0,effects:void 0};if(N===void 0){let r=ie(e,{transition:t.anyTransition,allowTransition:!n.applied});return{emitted:r.emitted,transitioned:n.applied||r.transitioned,target:n.target??r.target,effects:n.effects??r.effects}}return{emitted:!1,transitioned:!1}}function ne(e,t){N===void 0&&t.handled&&k.emitTransition({event:e,from:t.from,to:t.outcome.target??x.name,transitioned:t.outcome.transitioned,effects:t.outcome.effects??[]})}function re(e,t){let n=!1,r,i;return X(`transition`,{event:e,operation:()=>{let a=t.transition(e,{context:R,actor:$()});if(a.emit&&O.push(...a.emit),a.state){let t=q(e,{step:a});r=t.target,i=t.effects,n=!0}}}),{applied:n,target:r,effects:i}}function ie(e,t){if(t.transition===void 0)return{emitted:!1,transitioned:!1};let n=t.transition,r=!1,i=!1,a,o;return X(`transition`,{event:e,operation:()=>{let s=n(e,{context:R,actor:$()});if(s.state&&t.allowTransition){let t=q(e,{step:s});a=t.target,o=t.effects,i=!0}s.emit&&(O.push(...s.emit),r=!0)}}),{emitted:r,transitioned:i,target:a,effects:o}}function K(){return(w===void 0||T)&&(w=m(C),T=!1),te({stateRef:x,regions:D,context:w,error:N,payload:S})}function q(e,t){if(N!==void 0||!t.step.state)return{};let n=l(t.step);if(!n)return{};if(!s(n.state))return Z(`transition`,{event:e,error:/* @__PURE__ */ Error(`invalid transition target`)}),{};A?.abort(),x=n.state;let r=x.name;S=n.payload,P={state:x,context:C};let i=J(e,{payloadForState:n.payload});return n.state.isFinal&&N===void 0&&k.emitDone(),{target:r,effects:i}}function J(e,t){if(F)return[];let n=o.effects[x.name];if(!n)return A=void 0,[];let r=new AbortController;A=r;let i=P??{state:x,context:C},a=ee({effects:{[x.name]:n},state:x,statePayload:t.payloadForState,event:e,context:R,emit:e=>{N!==void 0||r.signal.aborted||(O.push(e),Y())},clock:v,abort:r,lastGood:i,onError:t=>Z(`effect`,{event:e,error:t})});for(let e of a.pending)M.push(e),e.finally(()=>{let t=M.indexOf(e);t!==-1&&M.splice(t,1)}).catch(()=>{});return a.ran}function Y(){if(!(j||F)){j=!0;try{let e=0;O.processCancellable(t=>{if(N!==void 0)return!1;if(e>=_)return Z(`budget`,{event:t,error:/* @__PURE__ */ Error(`internal event budget exceeded`)}),!1;if(e++,u.has(t.type)||g.has(t.type))W(t);else if(!X(`output`,{event:t,operation:()=>k.emitOutput(t)}))return!1;return!0})}finally{j=!1,H()}}}function X(t,n){let r=e.from(()=>(n.operation(),!0));return r[0]===void 0||(Z(t,{event:n.event,error:r[0]}),!1)}function Z(e,t){if(N!==void 0)return;let n=t.lastGood??P??{state:x,context:C};N={error:t.error,state:n.state,context:n.context,event:t.event,reason:e},A?.abort(),x=L,S=void 0,E=L,I=!1,k.emitError(N),k.emitChange(K()),k.emitDone()}function ae(e){F||(O.push(e),Y())}function oe(){if(!F){F=!0,A?.abort(),A=void 0;for(let e of Object.values(D))e.dispose();D={},O.clear(),k.clear(),M=[]}}function Q(){return O.settled().then(()=>{if(M.length!==0)return Promise.all(M).then(()=>Q())})}function $(){return V}return V}function S(e){return Symbol.for(`mantaq:event:${e}`)}function C(e){return()=>w(e)}function w(e){let t=S(e),n=function(e){return typeof e==`object`&&!!e&&Object.hasOwn(e,t)};function r(n){return Object.defineProperty(n===void 0?{type:e}:{type:e,payload:n},t,{value:!0})}return{type:e,is:n,create:r}}export{x as Actor,i as Context,w as EventRef,p as RealClock,c as StateRef,v as VirtualClock,C as event,s as isStateRef,a as state};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mantaq/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "State machine runtime with actor model, event system, state hierarchy, effects, and virtual clock.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -21,19 +21,19 @@
|
|
|
21
21
|
"access": "public"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@mantaq/utils": "0.0.
|
|
24
|
+
"@mantaq/utils": "0.0.4"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@stryker-mutator/core": "^
|
|
28
|
-
"@stryker-mutator/vitest-runner": "^
|
|
27
|
+
"@stryker-mutator/core": "^10",
|
|
28
|
+
"@stryker-mutator/vitest-runner": "^10",
|
|
29
29
|
"@types/node": "^26.2.0",
|
|
30
30
|
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
|
31
|
-
"@vitest/coverage-v8": "
|
|
32
|
-
"typescript": "
|
|
33
|
-
"vite": "npm:@voidzero-dev/vite-plus-core@
|
|
34
|
-
"vite-plus": "
|
|
35
|
-
"vitest": "
|
|
36
|
-
"@mantaq/pbt": "0.0.
|
|
31
|
+
"@vitest/coverage-v8": "5.0.5",
|
|
32
|
+
"typescript": "6.0.3",
|
|
33
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@1.4.0",
|
|
34
|
+
"vite-plus": "1.4.0",
|
|
35
|
+
"vitest": "5.0.5",
|
|
36
|
+
"@mantaq/pbt": "0.0.3"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
39
|
"build": "vp pack",
|