@mantaq/core 0.0.2 → 0.1.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 +72 -62
- package/dist/{actor-internal-CBU_EDPZ.d.mts → actor-internal-DYAL1iYK.d.mts} +63 -14
- package/dist/index.d.mts +11 -61
- package/dist/index.mjs +160 -96
- package/dist/internal.d.mts +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -10,18 +10,19 @@ npm install @mantaq/core
|
|
|
10
10
|
|
|
11
11
|
## Quick Reference
|
|
12
12
|
|
|
13
|
-
| Pattern | Correct
|
|
14
|
-
| --------------- |
|
|
15
|
-
| Type context | `context: {} as MyContext`
|
|
16
|
-
|
|
|
17
|
-
|
|
|
18
|
-
|
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
13
|
+
| Pattern | Correct | Anti-pattern |
|
|
14
|
+
| --------------- | -------------------------------------- | ------------------------------------- |
|
|
15
|
+
| Type context | `context: {} as MyContext` | casting in every handler |
|
|
16
|
+
| Write context | `context.set({ ...context.get() })` | mutate without `set()` |
|
|
17
|
+
| Type events | `event("ID")<Payload>()` | `event("ID")()` without generic |
|
|
18
|
+
| Send events | `actor.send(signInEvent.create(data))` | raw `{ type: "ID", payload }` objects |
|
|
19
|
+
| Effect data | Use state payload | Depend on `event` in effect |
|
|
20
|
+
| Error handling | Emit internal event | Throw in effect or transition |
|
|
21
|
+
| Internal events | Declare in `internal: [...]` | Put user events in internal |
|
|
22
|
+
| Signal check | `if (signal.aborted) return` | Skip abort check in async work |
|
|
23
|
+
| Regions | Child outputs in parent `internal` | Forget to declare child outputs |
|
|
24
|
+
| Snapshot | `snapshot()` carries path + context | Re-serialize context by hand |
|
|
25
|
+
| Any handler | Universal events only (CANCEL) | State-specific logic in `onAny` |
|
|
25
26
|
|
|
26
27
|
## Contents
|
|
27
28
|
|
|
@@ -94,20 +95,34 @@ const actor = new Actor({
|
|
|
94
95
|
context: {} as AuthContext,
|
|
95
96
|
setup: (m) => {
|
|
96
97
|
m.on(idleState, signInEvent, (event, opts) => {
|
|
97
|
-
|
|
98
|
+
// ✅ typed — context.get()/set() flow the constructor type
|
|
99
|
+
opts!.context.set({ ...opts!.context.get(), phoneNumber: event.payload.phoneNumber });
|
|
98
100
|
return { state: signingInState };
|
|
99
101
|
});
|
|
100
102
|
},
|
|
101
103
|
});
|
|
102
104
|
```
|
|
103
105
|
|
|
106
|
+
`set()` is the write signal — always call it to write, even when the new value is the same reference. Any `set()` triggers `change`. Spread the current value:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
opts.context.set({ ...opts.context.get(), phoneNumber: event.payload.phoneNumber });
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Never mutate in place:**
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
// ❌ mutation without set() — never signals; subscribers and persistence stay silent
|
|
116
|
+
opts!.context.phoneNumber = event.payload.phoneNumber;
|
|
117
|
+
```
|
|
118
|
+
|
|
104
119
|
**Anti-pattern — casting in every handler:**
|
|
105
120
|
|
|
106
121
|
```ts
|
|
107
122
|
setup: (m) => {
|
|
108
123
|
m.on(idleState, signInEvent, (event, opts) => {
|
|
109
124
|
const c = opts!.context as AuthContext; // ❌ unnecessary — already typed
|
|
110
|
-
c.phoneNumber
|
|
125
|
+
c.set({ ...c.get(), phoneNumber: event.payload.phoneNumber });
|
|
111
126
|
return { state: signingInState };
|
|
112
127
|
});
|
|
113
128
|
},
|
|
@@ -132,7 +147,7 @@ Handlers receive the correct payload type:
|
|
|
132
147
|
```ts
|
|
133
148
|
setup: (m) => {
|
|
134
149
|
m.on(idleState, signInEvent, (event) => {
|
|
135
|
-
event.phoneNumber; // ✅ string
|
|
150
|
+
event.payload.phoneNumber; // ✅ string
|
|
136
151
|
return { state: signingInState };
|
|
137
152
|
});
|
|
138
153
|
},
|
|
@@ -155,12 +170,12 @@ const goodEvent = event("SIGN_IN")<{ phoneNumber: string }>();
|
|
|
155
170
|
actor.send(signInEvent.create({ phoneNumber: "+1234567890" }));
|
|
156
171
|
|
|
157
172
|
// ❌ bypasses type checking
|
|
158
|
-
actor.send({
|
|
173
|
+
actor.send({ type: "SIGN_IN", payload: { phoneNumber: "+1234567890" } });
|
|
159
174
|
```
|
|
160
175
|
|
|
161
176
|
### Effects and Event Typing
|
|
162
177
|
|
|
163
|
-
Effects run when entering a state. The `event` parameter in effects is typed
|
|
178
|
+
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.
|
|
164
179
|
|
|
165
180
|
**Pass data to effects via state payload.** Set the payload in the transition return, read it from `state.payload` in the effect.
|
|
166
181
|
|
|
@@ -184,7 +199,7 @@ const actor = new Actor({
|
|
|
184
199
|
});
|
|
185
200
|
});
|
|
186
201
|
m.on(idleState, fetchEvent, (event) => {
|
|
187
|
-
return { state: loadingState.create({ url: event.url }) };
|
|
202
|
+
return { state: loadingState.create({ url: event.payload.url }) };
|
|
188
203
|
});
|
|
189
204
|
m.on(loadingState, doneEvent, () => ({ state: loadedState }));
|
|
190
205
|
},
|
|
@@ -195,10 +210,10 @@ const actor = new Actor({
|
|
|
195
210
|
|
|
196
211
|
```ts
|
|
197
212
|
// ✅ state payload — scoped to this state entry
|
|
198
|
-
return { state: loadingState.create({ url: event.url }) };
|
|
213
|
+
return { state: loadingState.create({ url: event.payload.url }) };
|
|
199
214
|
|
|
200
215
|
// ⚠️ context — works but shared across all states
|
|
201
|
-
opts!.context.url
|
|
216
|
+
opts!.context.set({ ...opts!.context.get(), url: event.payload.url });
|
|
202
217
|
return { state: loadingState };
|
|
203
218
|
```
|
|
204
219
|
|
|
@@ -206,9 +221,8 @@ return { state: loadingState };
|
|
|
206
221
|
|
|
207
222
|
```ts
|
|
208
223
|
m.effect(loadingState, ({ event, emit }) => {
|
|
209
|
-
// ❌ event is
|
|
210
|
-
// event.url
|
|
211
|
-
emit(doneEvent.create()); // fine — but event.url would be a type error
|
|
224
|
+
// ❌ event is InternalEvent — payload is unknown, not the triggering event's payload
|
|
225
|
+
emit(doneEvent.create()); // fine — but event.payload.url would be a type error
|
|
212
226
|
});
|
|
213
227
|
```
|
|
214
228
|
|
|
@@ -289,7 +303,10 @@ function createActor() {
|
|
|
289
303
|
m.on(idleState, startEvent, () => ({ state: workingState }));
|
|
290
304
|
m.on(workingState, doneEvent, () => ({ state: doneState }));
|
|
291
305
|
m.on(workingState, failedEvent, (_event, opts) => {
|
|
292
|
-
opts!.context.
|
|
306
|
+
opts!.context.set({
|
|
307
|
+
...opts!.context.get(),
|
|
308
|
+
retryCount: opts!.context.get().retryCount + 1,
|
|
309
|
+
});
|
|
293
310
|
return { state: errorState };
|
|
294
311
|
});
|
|
295
312
|
},
|
|
@@ -320,45 +337,29 @@ m.effect(workingState, ({ signal, emit, clock }) => {
|
|
|
320
337
|
|
|
321
338
|
### Snapshot & Restore
|
|
322
339
|
|
|
323
|
-
`actor.snapshot()` returns a
|
|
340
|
+
`actor.snapshot()` returns a `Snapshot` — `{ path, context, regions, done?, error? }`. Context is included. Use it to save/restore actor state across sessions.
|
|
324
341
|
|
|
325
342
|
```ts
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
regions: Record<string, unknown>;
|
|
329
|
-
context: Record<string, unknown>;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function saveActor(actor: { snapshot(): Snapshot; context: unknown }): SerializedActor {
|
|
333
|
-
return {
|
|
334
|
-
...actor.snapshot(),
|
|
335
|
-
context: { ...(actor.context as Record<string, unknown>) },
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// Usage
|
|
340
|
-
const saved = saveActor(actor);
|
|
343
|
+
// Snapshot carries path, context, and regions — serialize it directly
|
|
344
|
+
const saved = actor.snapshot();
|
|
341
345
|
localStorage.setItem("actor", JSON.stringify(saved));
|
|
342
346
|
|
|
343
347
|
const raw = localStorage.getItem("actor");
|
|
344
348
|
if (raw) {
|
|
345
|
-
const data = JSON.parse(raw) as
|
|
346
|
-
//
|
|
349
|
+
const data = JSON.parse(raw) as Snapshot;
|
|
350
|
+
// rebuild the actor from data.path (re-seed context from data.context if needed)
|
|
347
351
|
}
|
|
348
352
|
```
|
|
349
353
|
|
|
350
|
-
**Anti-pattern —
|
|
354
|
+
**Anti-pattern — re-serializing context by hand:**
|
|
351
355
|
|
|
352
356
|
```ts
|
|
353
|
-
// ❌ snapshot()
|
|
357
|
+
// ❌ snapshot() already includes context — no manual step needed
|
|
354
358
|
const snap = actor.snapshot();
|
|
355
|
-
snap.context; //
|
|
359
|
+
snap.context; // exists
|
|
356
360
|
|
|
357
|
-
// ✅
|
|
358
|
-
const stateData =
|
|
359
|
-
path: actor.snapshot().path,
|
|
360
|
-
context: { ...actor.context },
|
|
361
|
-
};
|
|
361
|
+
// ✅ save the snapshot as-is
|
|
362
|
+
const stateData = actor.snapshot();
|
|
362
363
|
```
|
|
363
364
|
|
|
364
365
|
### Dynamic Children (Regions)
|
|
@@ -374,10 +375,10 @@ const healthMonitor = new Actor({
|
|
|
374
375
|
initial: unknownState,
|
|
375
376
|
setup: (m) => {
|
|
376
377
|
m.on(unknownState, healthCheckResult, (event) => ({
|
|
377
|
-
state: event.healthy ? healthyState : degradedState,
|
|
378
|
+
state: event.payload.healthy ? healthyState : degradedState,
|
|
378
379
|
}));
|
|
379
380
|
m.on(healthyState, healthCheckResult, (event) => ({
|
|
380
|
-
state: event.healthy ? healthyState : degradedState,
|
|
381
|
+
state: event.payload.healthy ? healthyState : degradedState,
|
|
381
382
|
}));
|
|
382
383
|
},
|
|
383
384
|
});
|
|
@@ -391,7 +392,7 @@ const manager = new Actor({
|
|
|
391
392
|
setup: (m) => {
|
|
392
393
|
m.on(connectedState, healthCheckResult, (event, opts) => {
|
|
393
394
|
// access child via actor.regions
|
|
394
|
-
manager.regions.health.send(healthCheckResult.create({ healthy: event.healthy }));
|
|
395
|
+
manager.regions.health.send(healthCheckResult.create({ healthy: event.payload.healthy }));
|
|
395
396
|
return {};
|
|
396
397
|
});
|
|
397
398
|
},
|
|
@@ -424,7 +425,16 @@ const parent = new Actor({
|
|
|
424
425
|
|
|
425
426
|
### Error Handling
|
|
426
427
|
|
|
427
|
-
Never throw from effects or transitions. Emit an error as an internal event and let a transition handle it.
|
|
428
|
+
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 last known good state/context, the bad event), drops remaining events, and ignores later `send`s. Errors never escape `send()` and the machine stays deterministic — same inputs, same trace.
|
|
429
|
+
|
|
430
|
+
```ts
|
|
431
|
+
const snap = actor.snapshot();
|
|
432
|
+
if (snap.error) {
|
|
433
|
+
snap.error.reason; // "transition" | "effect" | "budget" | "output" | "internal" | "async"
|
|
434
|
+
snap.error.state.name; // last known good state
|
|
435
|
+
snap.error.event.type; // the bad event
|
|
436
|
+
}
|
|
437
|
+
```
|
|
428
438
|
|
|
429
439
|
Catch errors inside effects and emit recovery events:
|
|
430
440
|
|
|
@@ -448,7 +458,7 @@ setup: (m) => {
|
|
|
448
458
|
m.on(workingState, doneEvent, () => ({ state: doneState }));
|
|
449
459
|
m.on(workingState, failedEvent, (event) => {
|
|
450
460
|
// handle error in transition, not in effect
|
|
451
|
-
return { state: errorState.create({ error: event.error }) };
|
|
461
|
+
return { state: errorState.create({ error: event.payload.error }) };
|
|
452
462
|
});
|
|
453
463
|
},
|
|
454
464
|
```
|
|
@@ -460,7 +470,7 @@ try {
|
|
|
460
470
|
const result = riskyOperation();
|
|
461
471
|
emit(doneEvent.create());
|
|
462
472
|
} catch (err) {
|
|
463
|
-
throw err; // ❌ throw in effect —
|
|
473
|
+
throw err; // ❌ throw in effect — the machine dies into __error
|
|
464
474
|
}
|
|
465
475
|
```
|
|
466
476
|
|
|
@@ -468,7 +478,7 @@ try {
|
|
|
468
478
|
|
|
469
479
|
```ts
|
|
470
480
|
m.on(idleState, submitEvent, (event) => {
|
|
471
|
-
if (!event.data) throw new Error("missing data"); // ❌
|
|
481
|
+
if (!event.payload.data) throw new Error("missing data"); // ❌ kills the machine
|
|
472
482
|
return { state: doneState };
|
|
473
483
|
});
|
|
474
484
|
```
|
|
@@ -477,8 +487,8 @@ Prefer storing the error in context and transitioning to an error state:
|
|
|
477
487
|
|
|
478
488
|
```ts
|
|
479
489
|
m.on(idleState, submitEvent, (event, opts) => {
|
|
480
|
-
if (!event.data) {
|
|
481
|
-
opts!.context.error
|
|
490
|
+
if (!event.payload.data) {
|
|
491
|
+
opts!.context.set({ ...opts!.context.get(), error: "missing data" });
|
|
482
492
|
return { state: errorState };
|
|
483
493
|
}
|
|
484
494
|
return { state: doneState };
|
|
@@ -493,7 +503,7 @@ Use `onAny` to intercept events across all states — useful for universal error
|
|
|
493
503
|
setup: (m) => {
|
|
494
504
|
// CANCEL works from any state
|
|
495
505
|
m.onAny(cancelEvent, (_event, opts) => {
|
|
496
|
-
opts!.context.cancelled
|
|
506
|
+
opts!.context.set({ ...opts!.context.get(), cancelled: true });
|
|
497
507
|
return { state: cancelledState };
|
|
498
508
|
});
|
|
499
509
|
// State-specific handlers still run for their state
|
|
@@ -507,13 +517,13 @@ setup: (m) => {
|
|
|
507
517
|
setup: (m) => {
|
|
508
518
|
// ❌ state-specific logic in onAny defeats the purpose
|
|
509
519
|
m.onAny(submitBasicInfoEvent, (event, opts) => {
|
|
510
|
-
opts!.context.basicInfo
|
|
520
|
+
opts!.context.set({ ...opts!.context.get(), basicInfo: event }); // wrong — only makes sense in basicInfo state
|
|
511
521
|
return { state: shippingAddressState };
|
|
512
522
|
});
|
|
513
523
|
|
|
514
524
|
// ✅ keep state-specific logic in state handlers
|
|
515
525
|
m.on(basicInfoState, submitBasicInfoEvent, (event, opts) => {
|
|
516
|
-
opts!.context.basicInfo
|
|
526
|
+
opts!.context.set({ ...opts!.context.get(), basicInfo: event });
|
|
517
527
|
return { state: shippingAddressState };
|
|
518
528
|
});
|
|
519
529
|
},
|
|
@@ -20,27 +20,30 @@ declare class StateRef<T extends string, Payload = unknown, IsFinal extends bool
|
|
|
20
20
|
}
|
|
21
21
|
//#endregion
|
|
22
22
|
//#region src/event.d.ts
|
|
23
|
-
declare function event<const T extends string>(
|
|
23
|
+
declare function event<const T extends string>(type: T): <Payload extends object | void = void>() => EventRef<T, Payload>;
|
|
24
24
|
type AnyEventRef = EventRef<string, object | void>;
|
|
25
25
|
type InternalEvent = {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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;
|
|
32
34
|
};
|
|
33
35
|
declare class EventRef<const T extends string, Payload extends object | void = void> {
|
|
34
|
-
readonly
|
|
35
|
-
constructor(
|
|
36
|
+
readonly type: T;
|
|
37
|
+
constructor(type: T);
|
|
36
38
|
is(anyEvent: unknown): anyEvent is CreatedOfEvent<T, Payload>;
|
|
37
39
|
create(): Payload extends void ? {
|
|
38
|
-
|
|
40
|
+
type: T;
|
|
39
41
|
} : void;
|
|
40
42
|
create(payload: Payload): Payload extends void ? {
|
|
41
|
-
|
|
42
|
-
} :
|
|
43
|
-
|
|
43
|
+
type: T;
|
|
44
|
+
} : {
|
|
45
|
+
type: T;
|
|
46
|
+
payload: Payload;
|
|
44
47
|
};
|
|
45
48
|
}
|
|
46
49
|
//#endregion
|
|
@@ -60,12 +63,58 @@ interface Clock {
|
|
|
60
63
|
setDrain?(fn: () => void): void;
|
|
61
64
|
}
|
|
62
65
|
//#endregion
|
|
66
|
+
//#region src/context.d.ts
|
|
67
|
+
declare class Context<T> {
|
|
68
|
+
#private;
|
|
69
|
+
constructor(get: () => T, set: (value: T) => void);
|
|
70
|
+
get(): T;
|
|
71
|
+
set(value: T): void;
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/actor-types.d.ts
|
|
75
|
+
type CreatedOf<E extends AnyEventRef> = E extends EventRef<infer Type, infer P> ? CreatedOfEvent<Type, P> : never;
|
|
76
|
+
type NonFinalStateRef<States extends AnyStateRef[]> = Extract<States[number], {
|
|
77
|
+
isFinal: false;
|
|
78
|
+
}>;
|
|
79
|
+
interface EffectInput<ActorContext, Payload = unknown> {
|
|
80
|
+
signal: AbortSignal;
|
|
81
|
+
state: {
|
|
82
|
+
name: string;
|
|
83
|
+
payload: Payload;
|
|
84
|
+
};
|
|
85
|
+
event: InternalEvent;
|
|
86
|
+
context: Context<ActorContext>;
|
|
87
|
+
emit: (event: InternalEvent) => void;
|
|
88
|
+
clock: Clock;
|
|
89
|
+
}
|
|
90
|
+
type EffectFn<ActorContext, Payload = unknown> = (input: EffectInput<ActorContext, Payload>) => void | Promise<void>;
|
|
91
|
+
type ErrorReason = "transition" | "effect" | "budget" | "output" | "internal" | "async";
|
|
92
|
+
interface ErrorInfo {
|
|
93
|
+
error: unknown;
|
|
94
|
+
state: AnyStateRef;
|
|
95
|
+
context: unknown;
|
|
96
|
+
event: InternalEvent;
|
|
97
|
+
reason: ErrorReason;
|
|
98
|
+
}
|
|
99
|
+
type ErrorState = StateRef<"__error", unknown, false>;
|
|
100
|
+
type TransitionResult<AllowedState extends AnyStateRef = AnyStateRef, AllowedEmit extends string = string> = {
|
|
101
|
+
state?: AllowedState | {
|
|
102
|
+
state: AllowedState;
|
|
103
|
+
payload?: unknown;
|
|
104
|
+
};
|
|
105
|
+
payload?: unknown;
|
|
106
|
+
emit?: Array<{
|
|
107
|
+
type: AllowedEmit;
|
|
108
|
+
}>;
|
|
109
|
+
};
|
|
110
|
+
//#endregion
|
|
63
111
|
//#region src/actor-internal.d.ts
|
|
64
112
|
interface Snapshot<C = unknown> {
|
|
65
113
|
path: string[];
|
|
66
114
|
context: C;
|
|
67
115
|
regions: Record<string, Snapshot<unknown>>;
|
|
68
116
|
done?: boolean;
|
|
117
|
+
error?: ErrorInfo;
|
|
69
118
|
}
|
|
70
119
|
interface AnyActor<C = Record<string, unknown>> {
|
|
71
120
|
state: AnyStateRef;
|
|
@@ -87,4 +136,4 @@ interface AnyActor<C = Record<string, unknown>> {
|
|
|
87
136
|
};
|
|
88
137
|
}
|
|
89
138
|
//#endregion
|
|
90
|
-
export {
|
|
139
|
+
export { AnyStateRef as _, EffectInput as a, NonFinalStateRef as c, Clock as d, AnyEventRef as f, event as g, InternalEvent as h, EffectFn as i, TransitionResult as l, EventRef as m, Snapshot as n, ErrorInfo as o, CreatedOfEvent as p, CreatedOf as r, ErrorState as s, AnyActor as t, Context as u, StateRef as v, state as y };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { _ as AnyStateRef, a as EffectInput, c as NonFinalStateRef, d as Clock, f as AnyEventRef, g as event, h as InternalEvent, i as EffectFn, l as TransitionResult, m as EventRef, n as Snapshot, o as ErrorInfo, p as CreatedOfEvent, r as CreatedOf, s as ErrorState, t as AnyActor, u as Context, v as StateRef, y as state } from "./actor-internal-DYAL1iYK.mjs";
|
|
2
2
|
//#region src/real-clock.d.ts
|
|
3
3
|
declare class RealClock implements Clock {
|
|
4
4
|
#private;
|
|
@@ -38,44 +38,8 @@ declare class VirtualClock implements Clock {
|
|
|
38
38
|
setDrain(fn: () => void): void;
|
|
39
39
|
}
|
|
40
40
|
//#endregion
|
|
41
|
-
//#region src/context.d.ts
|
|
42
|
-
declare class Context<T> {
|
|
43
|
-
#private;
|
|
44
|
-
constructor(get: () => T, set: (value: T) => void);
|
|
45
|
-
get(): T;
|
|
46
|
-
set(value: T): void;
|
|
47
|
-
}
|
|
48
|
-
//#endregion
|
|
49
|
-
//#region src/actor-types.d.ts
|
|
50
|
-
type CreatedOf<E extends AnyEventRef> = E extends EventRef<infer Id, infer P> ? CreatedOfEvent<Id, P> : never;
|
|
51
|
-
type NonFinalStateRef<States extends AnyStateRef[]> = Extract<States[number], {
|
|
52
|
-
isFinal: false;
|
|
53
|
-
}>;
|
|
54
|
-
interface EffectInput<ActorContext, Payload = unknown> {
|
|
55
|
-
signal: AbortSignal;
|
|
56
|
-
state: {
|
|
57
|
-
name: string;
|
|
58
|
-
payload: Payload;
|
|
59
|
-
};
|
|
60
|
-
event: InternalEvent;
|
|
61
|
-
context: Context<ActorContext>;
|
|
62
|
-
emit: (event: InternalEvent) => void;
|
|
63
|
-
clock: Clock;
|
|
64
|
-
}
|
|
65
|
-
type EffectFn<ActorContext, Payload = unknown> = (input: EffectInput<ActorContext, Payload>) => void;
|
|
66
|
-
type TransitionResult<AllowedState extends AnyStateRef = AnyStateRef, AllowedEmit extends string = string> = {
|
|
67
|
-
state?: AllowedState | {
|
|
68
|
-
state: AllowedState;
|
|
69
|
-
payload?: unknown;
|
|
70
|
-
};
|
|
71
|
-
payload?: unknown;
|
|
72
|
-
emit?: Array<{
|
|
73
|
-
id: AllowedEmit;
|
|
74
|
-
}>;
|
|
75
|
-
};
|
|
76
|
-
//#endregion
|
|
77
41
|
//#region src/builder.d.ts
|
|
78
|
-
type
|
|
42
|
+
type EventTypeOf<E extends AnyEventRef> = E extends EventRef<infer Type, object | void> ? Type : never;
|
|
79
43
|
type PayloadOf<S extends AnyStateRef> = S extends StateRef<infer _Name, infer Payload, infer _IsFinal> ? Payload : never;
|
|
80
44
|
type TransitionHandler<States extends readonly AnyStateRef[], ActorContext> = (event: InternalEvent, opts: {
|
|
81
45
|
context: Context<ActorContext>;
|
|
@@ -87,25 +51,21 @@ interface BuiltMaps<States extends readonly AnyStateRef[], ActorContext> {
|
|
|
87
51
|
}
|
|
88
52
|
declare class ActorBuilder<States extends readonly AnyStateRef[], Inputs extends readonly AnyEventRef[], Internal extends readonly AnyEventRef[], Outputs extends readonly AnyEventRef[], ActorContext> {
|
|
89
53
|
#private;
|
|
90
|
-
on<S extends States[number], E extends Inputs[number] | Internal[number]>(stateRef: S, eventRef: E, fn: (event: E extends EventRef<infer
|
|
54
|
+
on<S extends States[number], E extends Inputs[number] | Internal[number]>(stateRef: S, eventRef: E, fn: (event: E extends EventRef<infer Type, infer P> ? CreatedOfEvent<Type, P> : never, opts: {
|
|
91
55
|
context: Context<ActorContext>;
|
|
92
56
|
actor: AnyActor;
|
|
93
|
-
}) => TransitionResult<States[number],
|
|
94
|
-
onAny<E extends Inputs[number] | Internal[number]>(eventRef: E, fn: (event: E extends EventRef<infer
|
|
57
|
+
}) => TransitionResult<States[number], EventTypeOf<Outputs[number]>>): this;
|
|
58
|
+
onAny<E extends Inputs[number] | Internal[number]>(eventRef: E, fn: (event: E extends EventRef<infer Type, infer P> ? CreatedOfEvent<Type, P> : never, opts: {
|
|
95
59
|
context: Context<ActorContext>;
|
|
96
60
|
actor: AnyActor;
|
|
97
|
-
}) => TransitionResult<States[number],
|
|
61
|
+
}) => TransitionResult<States[number], EventTypeOf<Outputs[number]>>): this;
|
|
98
62
|
effect<S extends States[number]>(stateRef: S, fn: EffectFn<ActorContext, PayloadOf<S>>): this;
|
|
99
63
|
/** @internal */ build(): BuiltMaps<States, ActorContext>;
|
|
100
64
|
}
|
|
101
65
|
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;
|
|
102
66
|
//#endregion
|
|
103
67
|
//#region src/actor.d.ts
|
|
104
|
-
type CreatedOf$1<E extends AnyEventRef> = E extends EventRef<infer
|
|
105
|
-
id: Id;
|
|
106
|
-
} : P & {
|
|
107
|
-
id: Id;
|
|
108
|
-
}) : never;
|
|
68
|
+
type CreatedOf$1<E extends AnyEventRef> = E extends EventRef<infer Type, infer P> ? CreatedOfEvent<Type, P> : never;
|
|
109
69
|
type InitialState<S extends AnyStateRef> = S extends StateRef<infer _N extends string, infer P> ? [unknown] extends [P] ? S | {
|
|
110
70
|
state: S;
|
|
111
71
|
payload?: P;
|
|
@@ -125,18 +85,9 @@ interface ActorOptions<States extends readonly AnyStateRef[], Inputs extends rea
|
|
|
125
85
|
setup: SetupFn<States, Inputs, Internal, Outputs, ActorContext>;
|
|
126
86
|
regions?: Record<string, AnyActor>;
|
|
127
87
|
}
|
|
128
|
-
interface InternalActorOptions<States extends readonly AnyStateRef[], Inputs extends readonly AnyEventRef[], Internal extends readonly AnyEventRef[], Outputs extends readonly AnyEventRef[], ActorContext = Record<string, unknown>> {
|
|
129
|
-
inputs: Inputs;
|
|
130
|
-
outputs?: Outputs;
|
|
131
|
-
internal?: Internal;
|
|
132
|
-
states: States;
|
|
133
|
-
context?: ActorContext;
|
|
134
|
-
initial: InitialState<States[number]>;
|
|
135
|
-
clock?: Clock;
|
|
136
|
-
internalBudget?: number;
|
|
88
|
+
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"> {
|
|
137
89
|
transitions: TransitionDispatch<States, ActorContext>;
|
|
138
90
|
effects: Record<string, Array<EffectFn<ActorContext>>>;
|
|
139
|
-
regions?: Record<string, AnyActor>;
|
|
140
91
|
}
|
|
141
92
|
type TransitionDispatch<States extends readonly AnyStateRef[], ActorContext> = Record<string, Record<string, ((event: InternalEvent, options: {
|
|
142
93
|
context: Context<ActorContext>;
|
|
@@ -144,17 +95,16 @@ type TransitionDispatch<States extends readonly AnyStateRef[], ActorContext> = R
|
|
|
144
95
|
}) => TransitionResult<States[number], string>) | undefined>>;
|
|
145
96
|
declare class 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>> {
|
|
146
97
|
#private;
|
|
147
|
-
state: States[number];
|
|
98
|
+
state: States[number] | ErrorState;
|
|
148
99
|
readonly clock: Clock;
|
|
149
100
|
get context(): ActorContext;
|
|
150
101
|
get regions(): Record<string, AnyActor>;
|
|
151
102
|
get options(): InternalActorOptions<States, Inputs, Internal, Outputs, ActorContext>;
|
|
152
103
|
constructor(options: ActorOptions<States, Inputs, Internal, Outputs, ActorContext>);
|
|
153
|
-
on(event: "change", fn: (snapshot: Snapshot<ActorContext>, prev: Snapshot<ActorContext>) => void): () => void;
|
|
154
|
-
on(event: "done", fn: () => void): () => void;
|
|
104
|
+
on(event: "change" | "done", fn: ((snapshot: Snapshot<ActorContext>, prev: Snapshot<ActorContext>) => void) | (() => void)): () => void;
|
|
155
105
|
settled(): Promise<void>;
|
|
156
106
|
send(event: CreatedOf$1<Inputs[number]>): void;
|
|
157
107
|
snapshot(): Snapshot<ActorContext>;
|
|
158
108
|
}
|
|
159
109
|
//#endregion
|
|
160
|
-
export { Actor, type ActorBuilder, type AnyActor, type AnyEventRef, type AnyStateRef, type Clock, Context, type CreatedOf, type CreatedOfEvent, type EffectFn, type EffectInput, EventRef, type InternalEvent, type NonFinalStateRef, RealClock, type SetupFn, type Snapshot, StateRef, VirtualClock, event, state };
|
|
110
|
+
export { Actor, type ActorBuilder, type AnyActor, type AnyEventRef, type AnyStateRef, type Clock, Context, type CreatedOf, type CreatedOfEvent, type EffectFn, type EffectInput, type ErrorInfo, type ErrorState, EventRef, type InternalEvent, type NonFinalStateRef, RealClock, type SetupFn, type Snapshot, StateRef, VirtualClock, event, state };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { registerActor, setOutputHandler } from "./internal.mjs";
|
|
2
|
+
import { Either } from "@mantaq/utils";
|
|
2
3
|
//#region src/state.ts
|
|
3
4
|
function state(id) {
|
|
4
5
|
return () => new StateRef(id, false);
|
|
@@ -203,9 +204,6 @@ var InternalQueue = class {
|
|
|
203
204
|
get length() {
|
|
204
205
|
return this.#queue.length - this.#index;
|
|
205
206
|
}
|
|
206
|
-
get isProcessing() {
|
|
207
|
-
return this.#processing;
|
|
208
|
-
}
|
|
209
207
|
push(...events) {
|
|
210
208
|
if (this.#stopped) return;
|
|
211
209
|
this.#queue.push(...events);
|
|
@@ -216,12 +214,6 @@ var InternalQueue = class {
|
|
|
216
214
|
this.#settledResolvers.push(resolve);
|
|
217
215
|
});
|
|
218
216
|
}
|
|
219
|
-
process(processEvent) {
|
|
220
|
-
this.#process((event) => {
|
|
221
|
-
processEvent(event);
|
|
222
|
-
return true;
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
217
|
processCancellable(processEvent) {
|
|
226
218
|
this.#process(processEvent);
|
|
227
219
|
}
|
|
@@ -260,7 +252,8 @@ var Subscribers = class {
|
|
|
260
252
|
}
|
|
261
253
|
addChange(fn) {
|
|
262
254
|
this.change.add(fn);
|
|
263
|
-
|
|
255
|
+
const last = this.#last;
|
|
256
|
+
if (last) this.#safe(() => fn(last, last));
|
|
264
257
|
return () => this.change.delete(fn);
|
|
265
258
|
}
|
|
266
259
|
addDone(fn) {
|
|
@@ -270,19 +263,23 @@ var Subscribers = class {
|
|
|
270
263
|
emitChange(snapshot) {
|
|
271
264
|
const prev = this.#last ?? snapshot;
|
|
272
265
|
this.#last = snapshot;
|
|
273
|
-
for (const fn of this.change) fn(snapshot, prev);
|
|
266
|
+
for (const fn of this.change) this.#safe(() => fn(snapshot, prev));
|
|
274
267
|
}
|
|
275
268
|
emitDone() {
|
|
276
|
-
for (const fn of this.done) fn
|
|
269
|
+
for (const fn of this.done) this.#safe(fn);
|
|
277
270
|
}
|
|
278
271
|
clear() {
|
|
279
272
|
this.change.clear();
|
|
280
273
|
this.done.clear();
|
|
281
274
|
}
|
|
275
|
+
#safe(fn) {
|
|
276
|
+
const err = Either.from(fn);
|
|
277
|
+
if (err[0] !== void 0) console.warn(`[Actor] subscriber threw: ${err[0] instanceof Error ? err[0].message : "unknown error"}`);
|
|
278
|
+
}
|
|
282
279
|
};
|
|
283
280
|
//#endregion
|
|
284
281
|
//#region src/snapshot.ts
|
|
285
|
-
function buildSnapshot(s, regions, context) {
|
|
282
|
+
function buildSnapshot(s, regions, context, error) {
|
|
286
283
|
const path = [s.name];
|
|
287
284
|
const regionSnapshots = {};
|
|
288
285
|
for (const [regionName, child] of Object.entries(regions)) regionSnapshots[regionName] = child.snapshot();
|
|
@@ -292,27 +289,40 @@ function buildSnapshot(s, regions, context) {
|
|
|
292
289
|
regions: regionSnapshots
|
|
293
290
|
};
|
|
294
291
|
if (s.isFinal) snap.done = true;
|
|
292
|
+
if (error !== void 0) snap.error = error;
|
|
295
293
|
return snap;
|
|
296
294
|
}
|
|
297
295
|
//#endregion
|
|
298
296
|
//#region src/effects.ts
|
|
299
297
|
function runEffects(options) {
|
|
300
|
-
|
|
298
|
+
const pending = [];
|
|
299
|
+
if (options.state.isFinal) return { pending };
|
|
301
300
|
const list = options.effects[options.state.name];
|
|
302
|
-
if (!list
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
301
|
+
if (!list) return { pending };
|
|
302
|
+
for (const effectFn of list) {
|
|
303
|
+
if (options.abort.signal.aborted) break;
|
|
304
|
+
const attempt = Either.from(() => effectFn({
|
|
305
|
+
signal: options.abort.signal,
|
|
306
|
+
state: {
|
|
307
|
+
name: options.state.name,
|
|
308
|
+
payload: options.statePayload
|
|
309
|
+
},
|
|
310
|
+
event: options.event,
|
|
311
|
+
context: options.context,
|
|
312
|
+
emit: options.emit,
|
|
313
|
+
clock: options.clock
|
|
314
|
+
}));
|
|
315
|
+
if (attempt[0] !== void 0) {
|
|
316
|
+
options.onError(attempt[0], options.lastGood);
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
const out = attempt[1];
|
|
320
|
+
if (out instanceof Promise) pending.push(out.catch((error) => {
|
|
321
|
+
if (options.abort.signal.aborted) return;
|
|
322
|
+
options.onError(error, options.lastGood);
|
|
323
|
+
}));
|
|
324
|
+
}
|
|
325
|
+
return { pending };
|
|
316
326
|
}
|
|
317
327
|
//#endregion
|
|
318
328
|
//#region src/dispatch.ts
|
|
@@ -350,13 +360,13 @@ var ActorBuilder = class {
|
|
|
350
360
|
#effects = {};
|
|
351
361
|
on(stateRef, eventRef, fn) {
|
|
352
362
|
const sName = stateRef.name;
|
|
353
|
-
const
|
|
354
|
-
(this.#transitions[sName] ??= {})[
|
|
363
|
+
const eType = eventRef.type;
|
|
364
|
+
(this.#transitions[sName] ??= {})[eType] = fn;
|
|
355
365
|
return this;
|
|
356
366
|
}
|
|
357
367
|
onAny(eventRef, fn) {
|
|
358
|
-
const
|
|
359
|
-
(this.#transitions["Any"] ??= {})[
|
|
368
|
+
const eType = eventRef.type;
|
|
369
|
+
(this.#transitions["Any"] ??= {})[eType] = fn;
|
|
360
370
|
return this;
|
|
361
371
|
}
|
|
362
372
|
effect(stateRef, fn) {
|
|
@@ -372,19 +382,16 @@ var ActorBuilder = class {
|
|
|
372
382
|
};
|
|
373
383
|
//#endregion
|
|
374
384
|
//#region src/actor.ts
|
|
375
|
-
function pickState(initial) {
|
|
376
|
-
return initial instanceof StateRef ? initial : initial.state;
|
|
377
|
-
}
|
|
378
385
|
function resolveInitial(initial) {
|
|
379
|
-
return
|
|
386
|
+
return initial instanceof StateRef ? initial : initial.state;
|
|
380
387
|
}
|
|
381
388
|
var Actor = class {
|
|
382
389
|
state;
|
|
383
390
|
clock;
|
|
384
391
|
#context;
|
|
385
392
|
#contextHandle;
|
|
393
|
+
#contextWritten = false;
|
|
386
394
|
#lastState;
|
|
387
|
-
#lastContext;
|
|
388
395
|
#options;
|
|
389
396
|
#regions = {};
|
|
390
397
|
#children = /* @__PURE__ */ new Map();
|
|
@@ -412,7 +419,7 @@ var Actor = class {
|
|
|
412
419
|
setOutputHandler: (fn) => {
|
|
413
420
|
this.#outputHandler = fn;
|
|
414
421
|
},
|
|
415
|
-
pushInternal: (event) => this.#
|
|
422
|
+
pushInternal: (event) => this.#queue.push(event),
|
|
416
423
|
drainInternal: () => this.#drainInternal(),
|
|
417
424
|
abortEffects: () => this.#abortEffects()
|
|
418
425
|
});
|
|
@@ -424,8 +431,8 @@ var Actor = class {
|
|
|
424
431
|
transitions: built.transitions,
|
|
425
432
|
effects: built.effects
|
|
426
433
|
};
|
|
427
|
-
this.#internalIds = new Set((options.internal ?? []).map((e) => e.
|
|
428
|
-
this.#inputIds = new Set(options.inputs.map((e) => e.
|
|
434
|
+
this.#internalIds = new Set((options.internal ?? []).map((e) => e.type));
|
|
435
|
+
this.#inputIds = new Set(options.inputs.map((e) => e.type));
|
|
429
436
|
this.#internalBudget = options.internalBudget ?? 1e4;
|
|
430
437
|
this.clock = options.clock ?? new RealClock();
|
|
431
438
|
this.clock.setDrain?.(() => this.#drainInternal());
|
|
@@ -435,9 +442,9 @@ var Actor = class {
|
|
|
435
442
|
this.state = initState;
|
|
436
443
|
this.#context = options.context ?? {};
|
|
437
444
|
this.#lastState = initState;
|
|
438
|
-
this.#lastContext = this.#context;
|
|
439
445
|
this.#contextHandle = new Context(() => this.#context, (value) => {
|
|
440
446
|
this.#context = value;
|
|
447
|
+
this.#contextWritten = true;
|
|
441
448
|
});
|
|
442
449
|
this.#subs.seed(this.snapshot());
|
|
443
450
|
if (options.regions) for (const [key, child] of Object.entries(options.regions)) {
|
|
@@ -459,134 +466,191 @@ var Actor = class {
|
|
|
459
466
|
return this.#subs.addDone(cb);
|
|
460
467
|
}
|
|
461
468
|
settled() {
|
|
462
|
-
return this.#queue.settled();
|
|
469
|
+
return Promise.all([this.#queue.settled(), ...this.#pendingEffects]).then(() => void 0);
|
|
463
470
|
}
|
|
464
471
|
send(event) {
|
|
465
472
|
this.#dispatch(event);
|
|
466
473
|
this.#emitChangeIfDirty();
|
|
467
474
|
}
|
|
468
475
|
#emitChangeIfDirty() {
|
|
469
|
-
if (this.state === this.#lastState && this.#
|
|
476
|
+
if (this.state === this.#lastState && !this.#contextWritten) return;
|
|
470
477
|
this.#lastState = this.state;
|
|
471
|
-
this.#
|
|
478
|
+
this.#contextWritten = false;
|
|
472
479
|
this.#subs.emitChange(this.snapshot());
|
|
473
480
|
}
|
|
474
481
|
#dispatch(event) {
|
|
475
|
-
if (this.state.isFinal) return;
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
+
if (this.#error !== null || this.state.isFinal) return;
|
|
483
|
+
const prev = this.#entry;
|
|
484
|
+
this.#entry = {
|
|
485
|
+
state: this.state,
|
|
486
|
+
context: this.#context
|
|
487
|
+
};
|
|
488
|
+
try {
|
|
489
|
+
const transitions = this.#options.transitions;
|
|
490
|
+
const stateTransition = transitions[this.state.name]?.[event.type];
|
|
491
|
+
const anyTransition = transitions["Any"]?.[event.type];
|
|
492
|
+
const transitionApplied = stateTransition ? this.#applyStateStep(event, stateTransition) : false;
|
|
493
|
+
if ((this.#error === null ? this.#applyAnyStep(event, anyTransition, !transitionApplied) : false) || this.#queue.length > 0) this.#drainInternal();
|
|
494
|
+
else if (!stateTransition && !anyTransition) console.warn(`[Actor] no transition for event "${event.type}" in state "${this.state.name}". Event dropped.`);
|
|
495
|
+
} finally {
|
|
496
|
+
this.#entry = prev;
|
|
497
|
+
}
|
|
482
498
|
}
|
|
483
499
|
#applyStateStep(event, transition) {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
500
|
+
let applied = false;
|
|
501
|
+
this.#safe("transition", event, () => {
|
|
502
|
+
const step = transition(event, {
|
|
503
|
+
context: this.#contextHandle,
|
|
504
|
+
actor: this
|
|
505
|
+
});
|
|
506
|
+
if (step.emit) this.#queue.push(...step.emit);
|
|
507
|
+
if (step.state) {
|
|
508
|
+
this.#applyTransition(event, step);
|
|
509
|
+
applied = true;
|
|
510
|
+
}
|
|
487
511
|
});
|
|
488
|
-
|
|
489
|
-
if (step.state) {
|
|
490
|
-
this.#applyTransition(event, step);
|
|
491
|
-
return true;
|
|
492
|
-
}
|
|
493
|
-
return false;
|
|
512
|
+
return applied;
|
|
494
513
|
}
|
|
495
514
|
#applyAnyStep(event, transition, allowTransition) {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
515
|
+
if (transition === void 0) return false;
|
|
516
|
+
let emitted = false;
|
|
517
|
+
this.#safe("transition", event, () => {
|
|
518
|
+
const step = transition(event, {
|
|
519
|
+
context: this.#contextHandle,
|
|
520
|
+
actor: this
|
|
521
|
+
});
|
|
522
|
+
if (step.state && allowTransition) this.#applyTransition(event, step);
|
|
523
|
+
if (step.emit) {
|
|
524
|
+
this.#queue.push(...step.emit);
|
|
525
|
+
emitted = true;
|
|
526
|
+
}
|
|
499
527
|
});
|
|
500
|
-
|
|
501
|
-
if (step.emit) {
|
|
502
|
-
this.#queue.push(...step.emit);
|
|
503
|
-
return true;
|
|
504
|
-
}
|
|
505
|
-
return false;
|
|
528
|
+
return emitted;
|
|
506
529
|
}
|
|
507
530
|
snapshot() {
|
|
508
|
-
return buildSnapshot(this.state, this.#regions, this.#context);
|
|
509
|
-
}
|
|
510
|
-
#pushEvent(event) {
|
|
511
|
-
this.#queue.push(event);
|
|
531
|
+
return buildSnapshot(this.state, this.#regions, this.#context, this.#error ?? void 0);
|
|
512
532
|
}
|
|
513
533
|
#abortEffects() {
|
|
514
534
|
this.#effectAbort?.abort();
|
|
515
535
|
this.#subs.clear();
|
|
516
536
|
}
|
|
517
537
|
#applyTransition(event, step) {
|
|
518
|
-
if (!step.state) return;
|
|
519
|
-
this.#effectAbort?.abort();
|
|
538
|
+
if (this.#error !== null || !step.state) return;
|
|
520
539
|
const resolved = parseTarget(step);
|
|
521
540
|
if (!resolved) return;
|
|
541
|
+
if (!(resolved.state instanceof StateRef)) {
|
|
542
|
+
this.#enterError("transition", event, /* @__PURE__ */ new Error("invalid transition target"));
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
this.#effectAbort?.abort();
|
|
522
546
|
this.state = resolved.state;
|
|
523
547
|
this.#runEffects(event, resolved.payload);
|
|
524
548
|
if (this.state.isFinal) this.#subs.emitDone();
|
|
525
549
|
}
|
|
526
550
|
#runEffects(event, statePayload) {
|
|
527
551
|
const list = this.#options.effects[this.state.name];
|
|
528
|
-
if (!list
|
|
552
|
+
if (!list) {
|
|
529
553
|
this.#effectAbort = null;
|
|
530
554
|
return;
|
|
531
555
|
}
|
|
532
|
-
const abort =
|
|
556
|
+
const abort = new AbortController();
|
|
557
|
+
this.#effectAbort = abort;
|
|
558
|
+
const lastGood = this.#entry ?? {
|
|
559
|
+
state: this.state,
|
|
560
|
+
context: this.#context
|
|
561
|
+
};
|
|
562
|
+
const result = runEffects({
|
|
533
563
|
effects: { [this.state.name]: list },
|
|
534
564
|
state: this.state,
|
|
535
565
|
statePayload,
|
|
536
566
|
event,
|
|
537
567
|
context: this.#contextHandle,
|
|
538
568
|
emit: (e) => {
|
|
569
|
+
if (this.#error !== null) return;
|
|
539
570
|
this.#queue.push(e);
|
|
540
571
|
this.#drainInternal();
|
|
541
572
|
},
|
|
542
|
-
clock: this.clock
|
|
573
|
+
clock: this.clock,
|
|
574
|
+
abort,
|
|
575
|
+
lastGood,
|
|
576
|
+
onError: (error) => this.#enterError("effect", event, error, lastGood)
|
|
543
577
|
});
|
|
544
|
-
this.#
|
|
578
|
+
this.#pendingEffects.push(...result.pending);
|
|
545
579
|
}
|
|
546
580
|
#drainInternal() {
|
|
547
581
|
if (this.#draining) return;
|
|
548
582
|
this.#draining = true;
|
|
549
|
-
let budgetExceeded = false;
|
|
550
583
|
try {
|
|
551
584
|
const budget = this.#internalBudget;
|
|
552
585
|
let count = 0;
|
|
553
586
|
this.#queue.processCancellable((event) => {
|
|
587
|
+
if (this.#error !== null) return false;
|
|
554
588
|
if (count >= budget) {
|
|
555
|
-
|
|
556
|
-
this.#abortEffects();
|
|
589
|
+
this.#enterError("budget", event, /* @__PURE__ */ new Error("internal event budget exceeded"));
|
|
557
590
|
return false;
|
|
558
591
|
}
|
|
559
592
|
count++;
|
|
560
|
-
if (this.#internalIds.has(event.
|
|
561
|
-
else
|
|
562
|
-
|
|
593
|
+
if (this.#internalIds.has(event.type) || this.#inputIds.has(event.type)) this.#dispatch(event);
|
|
594
|
+
else {
|
|
595
|
+
const outputHandler = this.#outputHandler;
|
|
596
|
+
if (outputHandler) {
|
|
597
|
+
if (!this.#safe("output", event, () => outputHandler(event))) return false;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
563
600
|
return true;
|
|
564
601
|
});
|
|
565
602
|
} finally {
|
|
566
603
|
this.#draining = false;
|
|
567
604
|
this.#emitChangeIfDirty();
|
|
568
605
|
}
|
|
569
|
-
|
|
606
|
+
}
|
|
607
|
+
#errorState = new StateRef("__error", false);
|
|
608
|
+
#error = null;
|
|
609
|
+
#entry = null;
|
|
610
|
+
#pendingEffects = [];
|
|
611
|
+
#safe(reason, event, fn) {
|
|
612
|
+
const attempt = Either.from(fn);
|
|
613
|
+
if (attempt[0] === void 0) return true;
|
|
614
|
+
this.#enterError(reason, event, attempt[0]);
|
|
615
|
+
return false;
|
|
616
|
+
}
|
|
617
|
+
#enterError(reason, event, error, lastGood) {
|
|
618
|
+
if (this.#error !== null) return;
|
|
619
|
+
const entry = lastGood ?? this.#entry ?? {
|
|
620
|
+
state: this.state,
|
|
621
|
+
context: this.#context
|
|
622
|
+
};
|
|
623
|
+
this.#error = {
|
|
624
|
+
error,
|
|
625
|
+
state: entry.state,
|
|
626
|
+
context: entry.context,
|
|
627
|
+
event,
|
|
628
|
+
reason
|
|
629
|
+
};
|
|
630
|
+
this.#effectAbort?.abort();
|
|
631
|
+
this.state = this.#errorState;
|
|
632
|
+
this.#lastState = this.#errorState;
|
|
633
|
+
this.#contextWritten = false;
|
|
634
|
+
this.#subs.emitChange(this.snapshot());
|
|
570
635
|
}
|
|
571
636
|
};
|
|
572
637
|
//#endregion
|
|
573
638
|
//#region src/event.ts
|
|
574
|
-
function event(
|
|
575
|
-
return () => new EventRef(
|
|
639
|
+
function event(type) {
|
|
640
|
+
return () => new EventRef(type);
|
|
576
641
|
}
|
|
577
642
|
var EventRef = class {
|
|
578
|
-
|
|
579
|
-
constructor(
|
|
580
|
-
this.
|
|
643
|
+
type;
|
|
644
|
+
constructor(type) {
|
|
645
|
+
this.type = type;
|
|
581
646
|
}
|
|
582
647
|
is(anyEvent) {
|
|
583
|
-
return !!anyEvent && typeof anyEvent === "object" && "
|
|
648
|
+
return !!anyEvent && typeof anyEvent === "object" && "type" in anyEvent && anyEvent.type === this.type;
|
|
584
649
|
}
|
|
585
650
|
create(payload) {
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
id: this.id
|
|
651
|
+
return payload === void 0 ? { type: this.type } : {
|
|
652
|
+
type: this.type,
|
|
653
|
+
payload
|
|
590
654
|
};
|
|
591
655
|
}
|
|
592
656
|
};
|
package/dist/internal.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { h as InternalEvent, t as AnyActor } from "./actor-internal-DYAL1iYK.mjs";
|
|
2
2
|
import { Either } from "@mantaq/utils";
|
|
3
3
|
//#region src/internal-registry.d.ts
|
|
4
4
|
interface ActorInternal {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mantaq/core",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.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,7 +21,7 @@
|
|
|
21
21
|
"access": "public"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@mantaq/utils": "0.0.
|
|
24
|
+
"@mantaq/utils": "0.0.2"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@stryker-mutator/core": "^9",
|