@mantaq/core 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -31
- package/dist/index.d.mts +173 -3
- package/dist/index.mjs +1 -658
- package/package.json +3 -3
- package/dist/actor-internal-DYAL1iYK.d.mts +0 -139
- package/dist/internal.d.mts +0 -26
- package/dist/internal.mjs +0 -31
package/README.md
CHANGED
|
@@ -10,19 +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
|
-
| Write context | `context.
|
|
17
|
-
| Type events | `event("ID")<Payload>()`
|
|
18
|
-
| Send events | `actor.send(signInEvent.create(data))`
|
|
19
|
-
| Effect data | Use state payload
|
|
20
|
-
| Error handling | Emit internal event
|
|
21
|
-
| Internal events | Declare in `internal: [...]`
|
|
22
|
-
| Signal check | `if (signal.aborted) return`
|
|
23
|
-
| Regions | Child outputs in parent `internal`
|
|
24
|
-
| Snapshot | `snapshot()` carries path + context
|
|
25
|
-
| Any handler | Universal events only (CANCEL)
|
|
13
|
+
| Pattern | Correct | Anti-pattern |
|
|
14
|
+
| --------------- | -------------------------------------------------- | ------------------------------------- |
|
|
15
|
+
| Type context | `context: {} as MyContext` | casting in every handler |
|
|
16
|
+
| Write context | `const s = context.get(); s.x = y; context.set(s)` | 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 + payload | Re-serialize context by hand |
|
|
25
|
+
| Any handler | Universal events only (CANCEL) | State-specific logic in `onAny` |
|
|
26
26
|
|
|
27
27
|
## Contents
|
|
28
28
|
|
|
@@ -42,6 +42,7 @@ npm install @mantaq/core
|
|
|
42
42
|
- [Any Handler](#any-handler)
|
|
43
43
|
- [Testing with VirtualClock](#testing-with-virtualclock)
|
|
44
44
|
- [Development](#development)
|
|
45
|
+
- [Testing](#testing)
|
|
45
46
|
- [License](#license)
|
|
46
47
|
|
|
47
48
|
## Quick Start
|
|
@@ -96,20 +97,24 @@ const actor = new Actor({
|
|
|
96
97
|
setup: (m) => {
|
|
97
98
|
m.on(idleState, signInEvent, (event, opts) => {
|
|
98
99
|
// ✅ typed — context.get()/set() flow the constructor type
|
|
99
|
-
|
|
100
|
+
const s = opts!.context.get();
|
|
101
|
+
s.phoneNumber = event.payload.phoneNumber;
|
|
102
|
+
opts!.context.set(s);
|
|
100
103
|
return { state: signingInState };
|
|
101
104
|
});
|
|
102
105
|
},
|
|
103
106
|
});
|
|
104
107
|
```
|
|
105
108
|
|
|
106
|
-
|
|
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:
|
|
107
110
|
|
|
108
111
|
```ts
|
|
109
|
-
|
|
112
|
+
const s = opts.context.get();
|
|
113
|
+
s.phoneNumber = event.payload.phoneNumber;
|
|
114
|
+
opts.context.set(s);
|
|
110
115
|
```
|
|
111
116
|
|
|
112
|
-
**Never mutate
|
|
117
|
+
**Never mutate without `set()`:**
|
|
113
118
|
|
|
114
119
|
```ts
|
|
115
120
|
// ❌ mutation without set() — never signals; subscribers and persistence stay silent
|
|
@@ -122,7 +127,9 @@ opts!.context.phoneNumber = event.payload.phoneNumber;
|
|
|
122
127
|
setup: (m) => {
|
|
123
128
|
m.on(idleState, signInEvent, (event, opts) => {
|
|
124
129
|
const c = opts!.context as AuthContext; // ❌ unnecessary — already typed
|
|
125
|
-
|
|
130
|
+
const s = c.get();
|
|
131
|
+
s.phoneNumber = event.payload.phoneNumber;
|
|
132
|
+
c.set(s);
|
|
126
133
|
return { state: signingInState };
|
|
127
134
|
});
|
|
128
135
|
},
|
|
@@ -213,7 +220,9 @@ const actor = new Actor({
|
|
|
213
220
|
return { state: loadingState.create({ url: event.payload.url }) };
|
|
214
221
|
|
|
215
222
|
// ⚠️ context — works but shared across all states
|
|
216
|
-
|
|
223
|
+
const s = opts!.context.get();
|
|
224
|
+
s.url = event.payload.url;
|
|
225
|
+
opts!.context.set(s);
|
|
217
226
|
return { state: loadingState };
|
|
218
227
|
```
|
|
219
228
|
|
|
@@ -303,10 +312,9 @@ function createActor() {
|
|
|
303
312
|
m.on(idleState, startEvent, () => ({ state: workingState }));
|
|
304
313
|
m.on(workingState, doneEvent, () => ({ state: doneState }));
|
|
305
314
|
m.on(workingState, failedEvent, (_event, opts) => {
|
|
306
|
-
opts!.context.
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
});
|
|
315
|
+
const s = opts!.context.get();
|
|
316
|
+
s.retryCount += 1;
|
|
317
|
+
opts!.context.set(s);
|
|
310
318
|
return { state: errorState };
|
|
311
319
|
});
|
|
312
320
|
},
|
|
@@ -337,7 +345,7 @@ m.effect(workingState, ({ signal, emit, clock }) => {
|
|
|
337
345
|
|
|
338
346
|
### Snapshot & Restore
|
|
339
347
|
|
|
340
|
-
`actor.snapshot()` returns a `Snapshot` — `{ path, context, regions, done?, error? }`. Context is included. Use it to save/restore actor state across sessions.
|
|
348
|
+
`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.
|
|
341
349
|
|
|
342
350
|
```ts
|
|
343
351
|
// Snapshot carries path, context, and regions — serialize it directly
|
|
@@ -425,17 +433,24 @@ const parent = new Actor({
|
|
|
425
433
|
|
|
426
434
|
### Error Handling
|
|
427
435
|
|
|
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
|
|
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 — same inputs, same trace.
|
|
429
437
|
|
|
430
438
|
```ts
|
|
431
439
|
const snap = actor.snapshot();
|
|
432
440
|
if (snap.error) {
|
|
433
|
-
snap.error.reason; // "transition" | "effect" | "budget" | "output" | "internal" | "async"
|
|
434
|
-
snap.error.state.name; //
|
|
441
|
+
snap.error.reason; // "transition" | "effect" | "budget" | "output" | "internal" | "async" | "unhandled"
|
|
442
|
+
snap.error.state.name; // the state at the point of failure
|
|
435
443
|
snap.error.event.type; // the bad event
|
|
436
444
|
}
|
|
437
445
|
```
|
|
438
446
|
|
|
447
|
+
Two deliberate exceptions to "every failure is loud":
|
|
448
|
+
|
|
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 — the machine and its callers are unaffected.
|
|
450
|
+
- **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
|
+
|
|
452
|
+
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
|
+
|
|
439
454
|
Catch errors inside effects and emit recovery events:
|
|
440
455
|
|
|
441
456
|
```ts
|
|
@@ -488,7 +503,9 @@ Prefer storing the error in context and transitioning to an error state:
|
|
|
488
503
|
```ts
|
|
489
504
|
m.on(idleState, submitEvent, (event, opts) => {
|
|
490
505
|
if (!event.payload.data) {
|
|
491
|
-
|
|
506
|
+
const s = opts!.context.get();
|
|
507
|
+
s.error = "missing data";
|
|
508
|
+
opts!.context.set(s);
|
|
492
509
|
return { state: errorState };
|
|
493
510
|
}
|
|
494
511
|
return { state: doneState };
|
|
@@ -503,7 +520,9 @@ Use `onAny` to intercept events across all states — useful for universal error
|
|
|
503
520
|
setup: (m) => {
|
|
504
521
|
// CANCEL works from any state
|
|
505
522
|
m.onAny(cancelEvent, (_event, opts) => {
|
|
506
|
-
|
|
523
|
+
const s = opts!.context.get();
|
|
524
|
+
s.cancelled = true;
|
|
525
|
+
opts!.context.set(s);
|
|
507
526
|
return { state: cancelledState };
|
|
508
527
|
});
|
|
509
528
|
// State-specific handlers still run for their state
|
|
@@ -517,13 +536,17 @@ setup: (m) => {
|
|
|
517
536
|
setup: (m) => {
|
|
518
537
|
// ❌ state-specific logic in onAny defeats the purpose
|
|
519
538
|
m.onAny(submitBasicInfoEvent, (event, opts) => {
|
|
520
|
-
|
|
539
|
+
const s = opts!.context.get();
|
|
540
|
+
s.basicInfo = event; // wrong — only makes sense in basicInfo state
|
|
541
|
+
opts!.context.set(s);
|
|
521
542
|
return { state: shippingAddressState };
|
|
522
543
|
});
|
|
523
544
|
|
|
524
545
|
// ✅ keep state-specific logic in state handlers
|
|
525
546
|
m.on(basicInfoState, submitBasicInfoEvent, (event, opts) => {
|
|
526
|
-
|
|
547
|
+
const s = opts!.context.get();
|
|
548
|
+
s.basicInfo = event;
|
|
549
|
+
opts!.context.set(s);
|
|
527
550
|
return { state: shippingAddressState };
|
|
528
551
|
});
|
|
529
552
|
},
|
|
@@ -662,6 +685,25 @@ vp test
|
|
|
662
685
|
vp pack
|
|
663
686
|
```
|
|
664
687
|
|
|
688
|
+
## Testing
|
|
689
|
+
|
|
690
|
+
The package ships with a full test suite you can run and audit:
|
|
691
|
+
|
|
692
|
+
```bash
|
|
693
|
+
vp test # feature + error + property tests
|
|
694
|
+
vp check # format, lint, typecheck (includes type-level contract tests)
|
|
695
|
+
vp run mutation:core # mutation score must stay ≥ 90
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
**Test contract.** Behavior is pinned by four kinds of tests, split by filename:
|
|
699
|
+
|
|
700
|
+
- `*.test.ts` — features and happy paths, hand-maintained.
|
|
701
|
+
- `*.error.test.ts` — failure paths: warnings, budgets, abort, unregistered, the `__error` state.
|
|
702
|
+
- `*.property.test.ts` — property-based invariants against a reference model (fast-check, replayable via `MANTAQ_SEED`).
|
|
703
|
+
- `*.mutation.test.ts` — directed tests that kill specific mutants; regenerable and thrown away between stryker runs.
|
|
704
|
+
|
|
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 — a change that drops the score below 90 fails the build.
|
|
706
|
+
|
|
665
707
|
## License
|
|
666
708
|
|
|
667
709
|
MIT
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,68 @@
|
|
|
1
|
-
|
|
1
|
+
//#region src/state.d.ts
|
|
2
|
+
declare function state<const T extends string>(id: T): <Payload = unknown>() => StateRef<T, Payload, false>;
|
|
3
|
+
type AnyStateRef = StateRef<string, unknown, boolean>;
|
|
4
|
+
interface RegionOptions {
|
|
5
|
+
initial: string;
|
|
6
|
+
states: Record<string, AnyStateRef>;
|
|
7
|
+
}
|
|
8
|
+
type RegionsOptions = Record<string, RegionOptions>;
|
|
9
|
+
declare class StateRef<T extends string, Payload = unknown, IsFinal extends boolean = false> {
|
|
10
|
+
readonly name: T;
|
|
11
|
+
readonly isFinal: IsFinal;
|
|
12
|
+
/** @internal */ _regions: RegionsOptions | undefined;
|
|
13
|
+
constructor(name: T, isFinal: IsFinal);
|
|
14
|
+
regions(options: RegionsOptions): this;
|
|
15
|
+
final(): StateRef<T, Payload, true>;
|
|
16
|
+
create(payload: Payload): {
|
|
17
|
+
state: StateRef<T, Payload, IsFinal>;
|
|
18
|
+
payload: Payload;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/event.d.ts
|
|
23
|
+
declare function event<const T extends string>(type: T): <Payload extends object | void = void>() => EventRef<T, Payload>;
|
|
24
|
+
type AnyEventRef = EventRef<string, object | void>;
|
|
25
|
+
type InternalEvent = {
|
|
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
|
|
2
66
|
//#region src/real-clock.d.ts
|
|
3
67
|
declare class RealClock implements Clock {
|
|
4
68
|
#private;
|
|
@@ -38,6 +102,96 @@ declare class VirtualClock implements Clock {
|
|
|
38
102
|
setDrain(fn: () => void): void;
|
|
39
103
|
}
|
|
40
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;
|
|
115
|
+
type NonFinalStateRef<States extends AnyStateRef[]> = Extract<States[number], {
|
|
116
|
+
isFinal: false;
|
|
117
|
+
}>;
|
|
118
|
+
interface EffectInput<ActorContext, Payload = unknown> {
|
|
119
|
+
signal: AbortSignal;
|
|
120
|
+
state: {
|
|
121
|
+
name: string;
|
|
122
|
+
payload: Payload;
|
|
123
|
+
};
|
|
124
|
+
event: InternalEvent;
|
|
125
|
+
context: Context<ActorContext>;
|
|
126
|
+
emit: (event: InternalEvent) => void;
|
|
127
|
+
clock: Clock;
|
|
128
|
+
}
|
|
129
|
+
type EffectFn<ActorContext, Payload = unknown> = (input: EffectInput<ActorContext, Payload>) => void | Promise<void>;
|
|
130
|
+
type ErrorReason = "transition" | "effect" | "budget" | "output" | "internal" | "async" | "unhandled";
|
|
131
|
+
interface ErrorInfo {
|
|
132
|
+
error: unknown;
|
|
133
|
+
state: AnyStateRef;
|
|
134
|
+
context: unknown;
|
|
135
|
+
event: InternalEvent;
|
|
136
|
+
reason: ErrorReason;
|
|
137
|
+
}
|
|
138
|
+
interface TransitionInfo {
|
|
139
|
+
event: InternalEvent;
|
|
140
|
+
from: string;
|
|
141
|
+
to: string;
|
|
142
|
+
transitioned: boolean;
|
|
143
|
+
}
|
|
144
|
+
type ErrorState = StateRef<"__error", unknown, true>;
|
|
145
|
+
type TransitionResult<AllowedState extends AnyStateRef = AnyStateRef, AllowedEmit extends string = string> = {
|
|
146
|
+
state?: AllowedState | {
|
|
147
|
+
state: AllowedState;
|
|
148
|
+
payload?: unknown;
|
|
149
|
+
};
|
|
150
|
+
payload?: unknown;
|
|
151
|
+
emit?: Array<{
|
|
152
|
+
type: AllowedEmit;
|
|
153
|
+
payload?: unknown;
|
|
154
|
+
}>;
|
|
155
|
+
};
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/actor-internal.d.ts
|
|
158
|
+
interface Snapshot<C = unknown> {
|
|
159
|
+
path: string[];
|
|
160
|
+
context: C;
|
|
161
|
+
payload?: unknown;
|
|
162
|
+
regions: Record<string, Snapshot<unknown>>;
|
|
163
|
+
done?: boolean;
|
|
164
|
+
error?: ErrorInfo;
|
|
165
|
+
}
|
|
166
|
+
interface AnyActor<C = Record<string, unknown>> {
|
|
167
|
+
state: AnyStateRef;
|
|
168
|
+
clock: Clock;
|
|
169
|
+
regions: Record<string, AnyActor>;
|
|
170
|
+
send(event: AnyEventRef | InternalEvent): void;
|
|
171
|
+
snapshot(): Snapshot<C>;
|
|
172
|
+
on(event: "change", fn: (snapshot: Snapshot<C>, prev: Snapshot<C>) => void): () => void;
|
|
173
|
+
on(event: "done", fn: () => void): () => void;
|
|
174
|
+
on(event: "transition", fn: (info: TransitionInfo) => void): () => void;
|
|
175
|
+
on(event: "error", fn: (info: ErrorInfo) => void): () => void;
|
|
176
|
+
on(event: "output", fn: (event: InternalEvent) => void): () => void;
|
|
177
|
+
recover(target: {
|
|
178
|
+
state: AnyStateRef;
|
|
179
|
+
context: C;
|
|
180
|
+
}): void;
|
|
181
|
+
settled(): Promise<void>;
|
|
182
|
+
context?: C;
|
|
183
|
+
options?: {
|
|
184
|
+
transitions?: Record<string, Record<string, unknown>>;
|
|
185
|
+
effects?: Record<string, unknown[]>;
|
|
186
|
+
states?: ReadonlyArray<{
|
|
187
|
+
name: string;
|
|
188
|
+
isFinal: boolean;
|
|
189
|
+
}>;
|
|
190
|
+
};
|
|
191
|
+
inject(event: InternalEvent): void;
|
|
192
|
+
dispose(): void;
|
|
193
|
+
}
|
|
194
|
+
//#endregion
|
|
41
195
|
//#region src/builder.d.ts
|
|
42
196
|
type EventTypeOf<E extends AnyEventRef> = E extends EventRef<infer Type, object | void> ? Type : never;
|
|
43
197
|
type PayloadOf<S extends AnyStateRef> = S extends StateRef<infer _Name, infer Payload, infer _IsFinal> ? Payload : never;
|
|
@@ -101,10 +255,26 @@ declare class Actor<const States extends readonly AnyStateRef[], const Inputs ex
|
|
|
101
255
|
get regions(): Record<string, AnyActor>;
|
|
102
256
|
get options(): InternalActorOptions<States, Inputs, Internal, Outputs, ActorContext>;
|
|
103
257
|
constructor(options: ActorOptions<States, Inputs, Internal, Outputs, ActorContext>);
|
|
104
|
-
on(event: "change"
|
|
258
|
+
on(event: "change", fn: (snapshot: Snapshot<ActorContext>, prev: Snapshot<ActorContext>) => void): () => void;
|
|
259
|
+
on(event: "done", fn: () => void): () => void;
|
|
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;
|
|
105
263
|
settled(): Promise<void>;
|
|
106
264
|
send(event: CreatedOf$1<Inputs[number]>): void;
|
|
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
|
+
*/
|
|
269
|
+
recover(target: {
|
|
270
|
+
state: States[number];
|
|
271
|
+
context: ActorContext;
|
|
272
|
+
}): void;
|
|
107
273
|
snapshot(): Snapshot<ActorContext>;
|
|
274
|
+
/** Push an internal event and drain. Unknown types route to output subscribers. */
|
|
275
|
+
inject(event: InternalEvent): void;
|
|
276
|
+
/** Stop for good: abort effect, clear queue and subscribers. Later sends/injects ignored. */
|
|
277
|
+
dispose(): void;
|
|
108
278
|
}
|
|
109
279
|
//#endregion
|
|
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 };
|
|
280
|
+
export { Actor, type ActorBuilder, type ActorOptions, type AnyActor, type AnyEventRef, type AnyStateRef, type BuiltMaps, type Clock, Context, type CreatedOf, type CreatedOfEvent, type EffectFn, type EffectInput, type ErrorInfo, type ErrorReason, type ErrorState, EventRef, type EventTypeOf, type InitialState, type InternalEvent, type NonFinalStateRef, type PayloadOf, RealClock, type SetupFn, type Snapshot, StateRef, type TransitionHandler, type TransitionInfo, type TransitionResult, VirtualClock, event, state };
|
package/dist/index.mjs
CHANGED
|
@@ -1,658 +1 @@
|
|
|
1
|
-
import { registerActor, setOutputHandler } from "./internal.mjs";
|
|
2
|
-
import { Either } from "@mantaq/utils";
|
|
3
|
-
//#region src/state.ts
|
|
4
|
-
function state(id) {
|
|
5
|
-
return () => new StateRef(id, false);
|
|
6
|
-
}
|
|
7
|
-
var StateRef = class StateRef {
|
|
8
|
-
name;
|
|
9
|
-
isFinal;
|
|
10
|
-
/** @internal */ _regions;
|
|
11
|
-
constructor(name, isFinal) {
|
|
12
|
-
this.name = name;
|
|
13
|
-
this.isFinal = isFinal;
|
|
14
|
-
}
|
|
15
|
-
regions(options) {
|
|
16
|
-
this._regions = options;
|
|
17
|
-
return this;
|
|
18
|
-
}
|
|
19
|
-
final() {
|
|
20
|
-
const next = new StateRef(this.name, true);
|
|
21
|
-
next._regions = this._regions;
|
|
22
|
-
return next;
|
|
23
|
-
}
|
|
24
|
-
create(payload) {
|
|
25
|
-
return {
|
|
26
|
-
state: this,
|
|
27
|
-
payload
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
//#endregion
|
|
32
|
-
//#region src/real-clock.ts
|
|
33
|
-
var RealClock = class {
|
|
34
|
-
#start = Date.now();
|
|
35
|
-
now() {
|
|
36
|
-
return Date.now() - this.#start;
|
|
37
|
-
}
|
|
38
|
-
setTimeout(ms, cb, options) {
|
|
39
|
-
if (options?.signal?.aborted) return -1;
|
|
40
|
-
let onAbort;
|
|
41
|
-
const id = Number(globalThis.setTimeout(() => {
|
|
42
|
-
if (onAbort && options?.signal) options.signal.removeEventListener("abort", onAbort);
|
|
43
|
-
cb();
|
|
44
|
-
}, ms));
|
|
45
|
-
if (options?.signal) {
|
|
46
|
-
onAbort = () => globalThis.clearTimeout(id);
|
|
47
|
-
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
48
|
-
}
|
|
49
|
-
return id;
|
|
50
|
-
}
|
|
51
|
-
clearTimeout(id) {
|
|
52
|
-
globalThis.clearTimeout(id);
|
|
53
|
-
}
|
|
54
|
-
setInterval(ms, cb, options) {
|
|
55
|
-
const id = Number(globalThis.setInterval(cb, ms));
|
|
56
|
-
if (options?.signal) {
|
|
57
|
-
const onAbort = () => globalThis.clearInterval(id);
|
|
58
|
-
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
59
|
-
}
|
|
60
|
-
return id;
|
|
61
|
-
}
|
|
62
|
-
clearInterval(id) {
|
|
63
|
-
globalThis.clearInterval(id);
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
//#endregion
|
|
67
|
-
//#region src/abort-tracker.ts
|
|
68
|
-
function trackAbort(signal, id, map) {
|
|
69
|
-
const onAbort = signal ? () => {
|
|
70
|
-
map.delete(id);
|
|
71
|
-
} : void 0;
|
|
72
|
-
if (signal && onAbort) signal.addEventListener("abort", onAbort, { once: true });
|
|
73
|
-
return onAbort;
|
|
74
|
-
}
|
|
75
|
-
function clearAbort(timer) {
|
|
76
|
-
if (timer.signal && timer.onAbort) timer.signal.removeEventListener("abort", timer.onAbort);
|
|
77
|
-
}
|
|
78
|
-
//#endregion
|
|
79
|
-
//#region src/virtual-clock.ts
|
|
80
|
-
var VirtualClock = class {
|
|
81
|
-
#now = 0;
|
|
82
|
-
#timers = /* @__PURE__ */ new Map();
|
|
83
|
-
#intervals = /* @__PURE__ */ new Map();
|
|
84
|
-
#nextId = 1;
|
|
85
|
-
#drain = null;
|
|
86
|
-
now() {
|
|
87
|
-
return this.#now;
|
|
88
|
-
}
|
|
89
|
-
setTimeout(ms, cb, options) {
|
|
90
|
-
const signal = options?.signal;
|
|
91
|
-
if (signal?.aborted) return -1;
|
|
92
|
-
const id = this.#nextId++;
|
|
93
|
-
const onAbort = trackAbort(signal, id, this.#timers);
|
|
94
|
-
this.#timers.set(id, {
|
|
95
|
-
deadline: this.#now + ms,
|
|
96
|
-
cb,
|
|
97
|
-
signal,
|
|
98
|
-
onAbort,
|
|
99
|
-
eventName: options?.eventName
|
|
100
|
-
});
|
|
101
|
-
return id;
|
|
102
|
-
}
|
|
103
|
-
clearTimeout(id) {
|
|
104
|
-
const timer = this.#timers.get(id);
|
|
105
|
-
if (timer) {
|
|
106
|
-
clearAbort(timer);
|
|
107
|
-
this.#timers.delete(id);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
setInterval(ms, cb, options) {
|
|
111
|
-
const signal = options?.signal;
|
|
112
|
-
if (signal?.aborted) return -1;
|
|
113
|
-
const id = this.#nextId++;
|
|
114
|
-
const onAbort = trackAbort(signal, id, this.#intervals);
|
|
115
|
-
this.#intervals.set(id, {
|
|
116
|
-
ms,
|
|
117
|
-
next: this.#now + ms,
|
|
118
|
-
cb,
|
|
119
|
-
signal,
|
|
120
|
-
onAbort
|
|
121
|
-
});
|
|
122
|
-
return id;
|
|
123
|
-
}
|
|
124
|
-
clearInterval(id) {
|
|
125
|
-
const interval = this.#intervals.get(id);
|
|
126
|
-
if (interval) {
|
|
127
|
-
clearAbort(interval);
|
|
128
|
-
this.#intervals.delete(id);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
#findEarliestDeadline(target) {
|
|
132
|
-
let earliest = target;
|
|
133
|
-
let found = false;
|
|
134
|
-
for (const t of this.#timers.values()) if (t.deadline <= target && t.deadline <= earliest) {
|
|
135
|
-
earliest = t.deadline;
|
|
136
|
-
found = true;
|
|
137
|
-
}
|
|
138
|
-
for (const t of this.#intervals.values()) if (t.next <= target && t.next <= earliest) {
|
|
139
|
-
earliest = t.next;
|
|
140
|
-
found = true;
|
|
141
|
-
}
|
|
142
|
-
return found ? earliest : null;
|
|
143
|
-
}
|
|
144
|
-
#fireTimersAt(deadline) {
|
|
145
|
-
const ids = [];
|
|
146
|
-
for (const [id, t] of this.#timers) if (t.deadline === deadline) ids.push(id);
|
|
147
|
-
for (const id of ids) {
|
|
148
|
-
const timer = this.#timers.get(id);
|
|
149
|
-
if (timer) {
|
|
150
|
-
clearAbort(timer);
|
|
151
|
-
this.#timers.delete(id);
|
|
152
|
-
timer.cb();
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
#fireIntervalsAt(deadline) {
|
|
157
|
-
const ids = [];
|
|
158
|
-
for (const [id, t] of this.#intervals) if (t.next === deadline) ids.push(id);
|
|
159
|
-
for (const id of ids) {
|
|
160
|
-
const interval = this.#intervals.get(id);
|
|
161
|
-
if (interval) {
|
|
162
|
-
interval.cb();
|
|
163
|
-
if (this.#intervals.has(id)) interval.next = this.#now + interval.ms;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
advance(ms) {
|
|
168
|
-
const target = this.#now + ms;
|
|
169
|
-
while (true) {
|
|
170
|
-
const deadline = this.#findEarliestDeadline(target);
|
|
171
|
-
if (deadline === null) break;
|
|
172
|
-
this.#now = deadline;
|
|
173
|
-
this.#fireTimersAt(deadline);
|
|
174
|
-
this.#fireIntervalsAt(deadline);
|
|
175
|
-
}
|
|
176
|
-
this.#now = target;
|
|
177
|
-
this.#drain?.();
|
|
178
|
-
}
|
|
179
|
-
hasPending() {
|
|
180
|
-
return this.#timers.size > 0 || this.#intervals.size > 0;
|
|
181
|
-
}
|
|
182
|
-
pendingTimers() {
|
|
183
|
-
const result = [];
|
|
184
|
-
for (const [id, t] of this.#timers) result.push({
|
|
185
|
-
id,
|
|
186
|
-
deadline: t.deadline,
|
|
187
|
-
ms: t.deadline - this.#now,
|
|
188
|
-
eventName: t.eventName
|
|
189
|
-
});
|
|
190
|
-
return result;
|
|
191
|
-
}
|
|
192
|
-
setDrain(fn) {
|
|
193
|
-
this.#drain = fn;
|
|
194
|
-
}
|
|
195
|
-
};
|
|
196
|
-
//#endregion
|
|
197
|
-
//#region src/queue.ts
|
|
198
|
-
var InternalQueue = class {
|
|
199
|
-
#queue = [];
|
|
200
|
-
#index = 0;
|
|
201
|
-
#processing = false;
|
|
202
|
-
#stopped = false;
|
|
203
|
-
#settledResolvers = [];
|
|
204
|
-
get length() {
|
|
205
|
-
return this.#queue.length - this.#index;
|
|
206
|
-
}
|
|
207
|
-
push(...events) {
|
|
208
|
-
if (this.#stopped) return;
|
|
209
|
-
this.#queue.push(...events);
|
|
210
|
-
}
|
|
211
|
-
settled() {
|
|
212
|
-
if (this.length === 0 && !this.#processing) return Promise.resolve();
|
|
213
|
-
return new Promise((resolve) => {
|
|
214
|
-
this.#settledResolvers.push(resolve);
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
processCancellable(processEvent) {
|
|
218
|
-
this.#process(processEvent);
|
|
219
|
-
}
|
|
220
|
-
#process(processEvent) {
|
|
221
|
-
if (this.#processing) return;
|
|
222
|
-
this.#processing = true;
|
|
223
|
-
try {
|
|
224
|
-
while (this.#index < this.#queue.length) {
|
|
225
|
-
if (this.#stopped) break;
|
|
226
|
-
const event = this.#queue[this.#index++];
|
|
227
|
-
if (!processEvent(event)) {
|
|
228
|
-
this.#stopped = true;
|
|
229
|
-
break;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
} finally {
|
|
233
|
-
this.#queue.length = 0;
|
|
234
|
-
this.#index = 0;
|
|
235
|
-
this.#processing = false;
|
|
236
|
-
this.#stopped = false;
|
|
237
|
-
if (this.#settledResolvers.length > 0) {
|
|
238
|
-
const resolvers = this.#settledResolvers.splice(0);
|
|
239
|
-
for (const resolve of resolvers) resolve();
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
};
|
|
244
|
-
//#endregion
|
|
245
|
-
//#region src/subscribers.ts
|
|
246
|
-
var Subscribers = class {
|
|
247
|
-
change = /* @__PURE__ */ new Set();
|
|
248
|
-
done = /* @__PURE__ */ new Set();
|
|
249
|
-
#last = null;
|
|
250
|
-
seed(snapshot) {
|
|
251
|
-
this.#last = snapshot;
|
|
252
|
-
}
|
|
253
|
-
addChange(fn) {
|
|
254
|
-
this.change.add(fn);
|
|
255
|
-
const last = this.#last;
|
|
256
|
-
if (last) this.#safe(() => fn(last, last));
|
|
257
|
-
return () => this.change.delete(fn);
|
|
258
|
-
}
|
|
259
|
-
addDone(fn) {
|
|
260
|
-
this.done.add(fn);
|
|
261
|
-
return () => this.done.delete(fn);
|
|
262
|
-
}
|
|
263
|
-
emitChange(snapshot) {
|
|
264
|
-
const prev = this.#last ?? snapshot;
|
|
265
|
-
this.#last = snapshot;
|
|
266
|
-
for (const fn of this.change) this.#safe(() => fn(snapshot, prev));
|
|
267
|
-
}
|
|
268
|
-
emitDone() {
|
|
269
|
-
for (const fn of this.done) this.#safe(fn);
|
|
270
|
-
}
|
|
271
|
-
clear() {
|
|
272
|
-
this.change.clear();
|
|
273
|
-
this.done.clear();
|
|
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
|
-
}
|
|
279
|
-
};
|
|
280
|
-
//#endregion
|
|
281
|
-
//#region src/snapshot.ts
|
|
282
|
-
function buildSnapshot(s, regions, context, error) {
|
|
283
|
-
const path = [s.name];
|
|
284
|
-
const regionSnapshots = {};
|
|
285
|
-
for (const [regionName, child] of Object.entries(regions)) regionSnapshots[regionName] = child.snapshot();
|
|
286
|
-
const snap = {
|
|
287
|
-
path,
|
|
288
|
-
context,
|
|
289
|
-
regions: regionSnapshots
|
|
290
|
-
};
|
|
291
|
-
if (s.isFinal) snap.done = true;
|
|
292
|
-
if (error !== void 0) snap.error = error;
|
|
293
|
-
return snap;
|
|
294
|
-
}
|
|
295
|
-
//#endregion
|
|
296
|
-
//#region src/effects.ts
|
|
297
|
-
function runEffects(options) {
|
|
298
|
-
const pending = [];
|
|
299
|
-
if (options.state.isFinal) return { pending };
|
|
300
|
-
const list = options.effects[options.state.name];
|
|
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 };
|
|
326
|
-
}
|
|
327
|
-
//#endregion
|
|
328
|
-
//#region src/dispatch.ts
|
|
329
|
-
function parseTarget(step) {
|
|
330
|
-
if (!step.state) return void 0;
|
|
331
|
-
if (step.state instanceof StateRef) return {
|
|
332
|
-
state: step.state,
|
|
333
|
-
payload: step.payload
|
|
334
|
-
};
|
|
335
|
-
return {
|
|
336
|
-
state: step.state.state,
|
|
337
|
-
payload: step.state.payload
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
//#endregion
|
|
341
|
-
//#region src/context.ts
|
|
342
|
-
var Context = class {
|
|
343
|
-
#get;
|
|
344
|
-
#set;
|
|
345
|
-
constructor(get, set) {
|
|
346
|
-
this.#get = get;
|
|
347
|
-
this.#set = set;
|
|
348
|
-
}
|
|
349
|
-
get() {
|
|
350
|
-
return this.#get();
|
|
351
|
-
}
|
|
352
|
-
set(value) {
|
|
353
|
-
this.#set(value);
|
|
354
|
-
}
|
|
355
|
-
};
|
|
356
|
-
//#endregion
|
|
357
|
-
//#region src/builder.ts
|
|
358
|
-
var ActorBuilder = class {
|
|
359
|
-
#transitions = {};
|
|
360
|
-
#effects = {};
|
|
361
|
-
on(stateRef, eventRef, fn) {
|
|
362
|
-
const sName = stateRef.name;
|
|
363
|
-
const eType = eventRef.type;
|
|
364
|
-
(this.#transitions[sName] ??= {})[eType] = fn;
|
|
365
|
-
return this;
|
|
366
|
-
}
|
|
367
|
-
onAny(eventRef, fn) {
|
|
368
|
-
const eType = eventRef.type;
|
|
369
|
-
(this.#transitions["Any"] ??= {})[eType] = fn;
|
|
370
|
-
return this;
|
|
371
|
-
}
|
|
372
|
-
effect(stateRef, fn) {
|
|
373
|
-
(this.#effects[stateRef.name] ??= []).push(fn);
|
|
374
|
-
return this;
|
|
375
|
-
}
|
|
376
|
-
/** @internal */ build() {
|
|
377
|
-
return {
|
|
378
|
-
transitions: this.#transitions,
|
|
379
|
-
effects: this.#effects
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
};
|
|
383
|
-
//#endregion
|
|
384
|
-
//#region src/actor.ts
|
|
385
|
-
function resolveInitial(initial) {
|
|
386
|
-
return initial instanceof StateRef ? initial : initial.state;
|
|
387
|
-
}
|
|
388
|
-
var Actor = class {
|
|
389
|
-
state;
|
|
390
|
-
clock;
|
|
391
|
-
#context;
|
|
392
|
-
#contextHandle;
|
|
393
|
-
#contextWritten = false;
|
|
394
|
-
#lastState;
|
|
395
|
-
#options;
|
|
396
|
-
#regions = {};
|
|
397
|
-
#children = /* @__PURE__ */ new Map();
|
|
398
|
-
#queue = new InternalQueue();
|
|
399
|
-
#subs = new Subscribers();
|
|
400
|
-
#effectAbort = null;
|
|
401
|
-
#outputHandler = null;
|
|
402
|
-
#internalIds;
|
|
403
|
-
#inputIds;
|
|
404
|
-
#internalBudget;
|
|
405
|
-
#draining = false;
|
|
406
|
-
get context() {
|
|
407
|
-
return this.#context;
|
|
408
|
-
}
|
|
409
|
-
get regions() {
|
|
410
|
-
return this.#regions;
|
|
411
|
-
}
|
|
412
|
-
get options() {
|
|
413
|
-
return this.#options;
|
|
414
|
-
}
|
|
415
|
-
constructor(options) {
|
|
416
|
-
registerActor(this, {
|
|
417
|
-
children: this.#children,
|
|
418
|
-
getOutputHandler: () => this.#outputHandler,
|
|
419
|
-
setOutputHandler: (fn) => {
|
|
420
|
-
this.#outputHandler = fn;
|
|
421
|
-
},
|
|
422
|
-
pushInternal: (event) => this.#queue.push(event),
|
|
423
|
-
drainInternal: () => this.#drainInternal(),
|
|
424
|
-
abortEffects: () => this.#abortEffects()
|
|
425
|
-
});
|
|
426
|
-
const builder = new ActorBuilder();
|
|
427
|
-
options.setup(builder);
|
|
428
|
-
const built = builder.build();
|
|
429
|
-
this.#options = {
|
|
430
|
-
...options,
|
|
431
|
-
transitions: built.transitions,
|
|
432
|
-
effects: built.effects
|
|
433
|
-
};
|
|
434
|
-
this.#internalIds = new Set((options.internal ?? []).map((e) => e.type));
|
|
435
|
-
this.#inputIds = new Set(options.inputs.map((e) => e.type));
|
|
436
|
-
this.#internalBudget = options.internalBudget ?? 1e4;
|
|
437
|
-
this.clock = options.clock ?? new RealClock();
|
|
438
|
-
this.clock.setDrain?.(() => this.#drainInternal());
|
|
439
|
-
const initState = resolveInitial(options.initial);
|
|
440
|
-
const stateNames = new Set(options.states.map((s) => s.name));
|
|
441
|
-
if (!stateNames.has(initState.name)) console.warn(`[Actor] initial state "${initState.name}" not found in declared states [${[...stateNames].join(", ")}]`);
|
|
442
|
-
this.state = initState;
|
|
443
|
-
this.#context = options.context ?? {};
|
|
444
|
-
this.#lastState = initState;
|
|
445
|
-
this.#contextHandle = new Context(() => this.#context, (value) => {
|
|
446
|
-
this.#context = value;
|
|
447
|
-
this.#contextWritten = true;
|
|
448
|
-
});
|
|
449
|
-
this.#subs.seed(this.snapshot());
|
|
450
|
-
if (options.regions) for (const [key, child] of Object.entries(options.regions)) {
|
|
451
|
-
this.#regions[key] = child;
|
|
452
|
-
const result = setOutputHandler(child, (event) => {
|
|
453
|
-
this.#queue.push(event);
|
|
454
|
-
this.#drainInternal();
|
|
455
|
-
});
|
|
456
|
-
if (result[0] !== void 0) console.error(result[0].message);
|
|
457
|
-
this.#children.set(key, child);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
on(event, fn) {
|
|
461
|
-
if (event === "change") {
|
|
462
|
-
const cb = fn;
|
|
463
|
-
return this.#subs.addChange(cb);
|
|
464
|
-
}
|
|
465
|
-
const cb = fn;
|
|
466
|
-
return this.#subs.addDone(cb);
|
|
467
|
-
}
|
|
468
|
-
settled() {
|
|
469
|
-
return Promise.all([this.#queue.settled(), ...this.#pendingEffects]).then(() => void 0);
|
|
470
|
-
}
|
|
471
|
-
send(event) {
|
|
472
|
-
this.#dispatch(event);
|
|
473
|
-
this.#emitChangeIfDirty();
|
|
474
|
-
}
|
|
475
|
-
#emitChangeIfDirty() {
|
|
476
|
-
if (this.state === this.#lastState && !this.#contextWritten) return;
|
|
477
|
-
this.#lastState = this.state;
|
|
478
|
-
this.#contextWritten = false;
|
|
479
|
-
this.#subs.emitChange(this.snapshot());
|
|
480
|
-
}
|
|
481
|
-
#dispatch(event) {
|
|
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
|
-
}
|
|
498
|
-
}
|
|
499
|
-
#applyStateStep(event, transition) {
|
|
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
|
-
}
|
|
511
|
-
});
|
|
512
|
-
return applied;
|
|
513
|
-
}
|
|
514
|
-
#applyAnyStep(event, transition, allowTransition) {
|
|
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
|
-
}
|
|
527
|
-
});
|
|
528
|
-
return emitted;
|
|
529
|
-
}
|
|
530
|
-
snapshot() {
|
|
531
|
-
return buildSnapshot(this.state, this.#regions, this.#context, this.#error ?? void 0);
|
|
532
|
-
}
|
|
533
|
-
#abortEffects() {
|
|
534
|
-
this.#effectAbort?.abort();
|
|
535
|
-
this.#subs.clear();
|
|
536
|
-
}
|
|
537
|
-
#applyTransition(event, step) {
|
|
538
|
-
if (this.#error !== null || !step.state) return;
|
|
539
|
-
const resolved = parseTarget(step);
|
|
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();
|
|
546
|
-
this.state = resolved.state;
|
|
547
|
-
this.#runEffects(event, resolved.payload);
|
|
548
|
-
if (this.state.isFinal) this.#subs.emitDone();
|
|
549
|
-
}
|
|
550
|
-
#runEffects(event, statePayload) {
|
|
551
|
-
const list = this.#options.effects[this.state.name];
|
|
552
|
-
if (!list) {
|
|
553
|
-
this.#effectAbort = null;
|
|
554
|
-
return;
|
|
555
|
-
}
|
|
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({
|
|
563
|
-
effects: { [this.state.name]: list },
|
|
564
|
-
state: this.state,
|
|
565
|
-
statePayload,
|
|
566
|
-
event,
|
|
567
|
-
context: this.#contextHandle,
|
|
568
|
-
emit: (e) => {
|
|
569
|
-
if (this.#error !== null) return;
|
|
570
|
-
this.#queue.push(e);
|
|
571
|
-
this.#drainInternal();
|
|
572
|
-
},
|
|
573
|
-
clock: this.clock,
|
|
574
|
-
abort,
|
|
575
|
-
lastGood,
|
|
576
|
-
onError: (error) => this.#enterError("effect", event, error, lastGood)
|
|
577
|
-
});
|
|
578
|
-
this.#pendingEffects.push(...result.pending);
|
|
579
|
-
}
|
|
580
|
-
#drainInternal() {
|
|
581
|
-
if (this.#draining) return;
|
|
582
|
-
this.#draining = true;
|
|
583
|
-
try {
|
|
584
|
-
const budget = this.#internalBudget;
|
|
585
|
-
let count = 0;
|
|
586
|
-
this.#queue.processCancellable((event) => {
|
|
587
|
-
if (this.#error !== null) return false;
|
|
588
|
-
if (count >= budget) {
|
|
589
|
-
this.#enterError("budget", event, /* @__PURE__ */ new Error("internal event budget exceeded"));
|
|
590
|
-
return false;
|
|
591
|
-
}
|
|
592
|
-
count++;
|
|
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
|
-
}
|
|
600
|
-
return true;
|
|
601
|
-
});
|
|
602
|
-
} finally {
|
|
603
|
-
this.#draining = false;
|
|
604
|
-
this.#emitChangeIfDirty();
|
|
605
|
-
}
|
|
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());
|
|
635
|
-
}
|
|
636
|
-
};
|
|
637
|
-
//#endregion
|
|
638
|
-
//#region src/event.ts
|
|
639
|
-
function event(type) {
|
|
640
|
-
return () => new EventRef(type);
|
|
641
|
-
}
|
|
642
|
-
var EventRef = class {
|
|
643
|
-
type;
|
|
644
|
-
constructor(type) {
|
|
645
|
-
this.type = type;
|
|
646
|
-
}
|
|
647
|
-
is(anyEvent) {
|
|
648
|
-
return !!anyEvent && typeof anyEvent === "object" && "type" in anyEvent && anyEvent.type === this.type;
|
|
649
|
-
}
|
|
650
|
-
create(payload) {
|
|
651
|
-
return payload === void 0 ? { type: this.type } : {
|
|
652
|
-
type: this.type,
|
|
653
|
-
payload
|
|
654
|
-
};
|
|
655
|
-
}
|
|
656
|
-
};
|
|
657
|
-
//#endregion
|
|
658
|
-
export { Actor, Context, EventRef, RealClock, StateRef, VirtualClock, event, state };
|
|
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};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mantaq/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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": {
|
|
@@ -12,16 +12,16 @@
|
|
|
12
12
|
"dist"
|
|
13
13
|
],
|
|
14
14
|
"type": "module",
|
|
15
|
+
"sideEffects": false,
|
|
15
16
|
"exports": {
|
|
16
17
|
".": "./dist/index.mjs",
|
|
17
|
-
"./internal": "./dist/internal.mjs",
|
|
18
18
|
"./package.json": "./package.json"
|
|
19
19
|
},
|
|
20
20
|
"publishConfig": {
|
|
21
21
|
"access": "public"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@mantaq/utils": "0.0.
|
|
24
|
+
"@mantaq/utils": "0.0.3"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@stryker-mutator/core": "^9",
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
//#region src/state.d.ts
|
|
2
|
-
declare function state<const T extends string>(id: T): <Payload = unknown>() => StateRef<T, Payload, false>;
|
|
3
|
-
type AnyStateRef = StateRef<string, unknown, boolean>;
|
|
4
|
-
interface RegionOptions {
|
|
5
|
-
initial: string;
|
|
6
|
-
states: Record<string, AnyStateRef>;
|
|
7
|
-
}
|
|
8
|
-
type RegionsOptions = Record<string, RegionOptions>;
|
|
9
|
-
declare class StateRef<T extends string, Payload = unknown, IsFinal extends boolean = false> {
|
|
10
|
-
readonly name: T;
|
|
11
|
-
readonly isFinal: IsFinal;
|
|
12
|
-
/** @internal */ _regions: RegionsOptions | undefined;
|
|
13
|
-
constructor(name: T, isFinal: IsFinal);
|
|
14
|
-
regions(options: RegionsOptions): this;
|
|
15
|
-
final(): StateRef<T, Payload, true>;
|
|
16
|
-
create(payload: Payload): {
|
|
17
|
-
state: StateRef<T, Payload, IsFinal>;
|
|
18
|
-
payload: Payload;
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
//#endregion
|
|
22
|
-
//#region src/event.d.ts
|
|
23
|
-
declare function event<const T extends string>(type: T): <Payload extends object | void = void>() => EventRef<T, Payload>;
|
|
24
|
-
type AnyEventRef = EventRef<string, object | void>;
|
|
25
|
-
type InternalEvent = {
|
|
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/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
|
|
111
|
-
//#region src/actor-internal.d.ts
|
|
112
|
-
interface Snapshot<C = unknown> {
|
|
113
|
-
path: string[];
|
|
114
|
-
context: C;
|
|
115
|
-
regions: Record<string, Snapshot<unknown>>;
|
|
116
|
-
done?: boolean;
|
|
117
|
-
error?: ErrorInfo;
|
|
118
|
-
}
|
|
119
|
-
interface AnyActor<C = Record<string, unknown>> {
|
|
120
|
-
state: AnyStateRef;
|
|
121
|
-
clock: Clock;
|
|
122
|
-
regions: Record<string, AnyActor>;
|
|
123
|
-
send(event: AnyEventRef | InternalEvent): void;
|
|
124
|
-
snapshot(): Snapshot<C>;
|
|
125
|
-
on(event: "change", fn: (snapshot: Snapshot<C>, prev: Snapshot<C>) => void): () => void;
|
|
126
|
-
on(event: "done", fn: () => void): () => void;
|
|
127
|
-
settled(): Promise<void>;
|
|
128
|
-
context?: C;
|
|
129
|
-
options?: {
|
|
130
|
-
transitions?: Record<string, Record<string, unknown>>;
|
|
131
|
-
effects?: Record<string, unknown[]>;
|
|
132
|
-
states?: ReadonlyArray<{
|
|
133
|
-
name: string;
|
|
134
|
-
isFinal: boolean;
|
|
135
|
-
}>;
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
//#endregion
|
|
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/internal.d.mts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { h as InternalEvent, t as AnyActor } from "./actor-internal-DYAL1iYK.mjs";
|
|
2
|
-
import { Either } from "@mantaq/utils";
|
|
3
|
-
//#region src/internal-registry.d.ts
|
|
4
|
-
interface ActorInternal {
|
|
5
|
-
children: Map<string, AnyActor>;
|
|
6
|
-
getOutputHandler(): ((event: InternalEvent) => void) | null;
|
|
7
|
-
setOutputHandler(fn: ((event: InternalEvent) => void) | null): void;
|
|
8
|
-
pushInternal(event: InternalEvent): void;
|
|
9
|
-
drainInternal(): void;
|
|
10
|
-
abortEffects(): void;
|
|
11
|
-
}
|
|
12
|
-
interface RegistryError {
|
|
13
|
-
message: string;
|
|
14
|
-
}
|
|
15
|
-
declare global {
|
|
16
|
-
var __mantaqCoreInternalRegistry: WeakMap<object, ActorInternal> | undefined;
|
|
17
|
-
}
|
|
18
|
-
declare function registerActor(actor: object, internal: ActorInternal): void;
|
|
19
|
-
declare function getChildren(actor: object): Either<RegistryError, Map<string, AnyActor>>;
|
|
20
|
-
declare function getOutputHandler(actor: object): Either<RegistryError, ((event: InternalEvent) => void) | null>;
|
|
21
|
-
declare function setOutputHandler(actor: object, fn: ((event: InternalEvent) => void) | null): Either<RegistryError, void>;
|
|
22
|
-
declare function pushInternal(actor: object, event: InternalEvent): Either<RegistryError, void>;
|
|
23
|
-
declare function drainInternal(actor: object): Either<RegistryError, void>;
|
|
24
|
-
declare function abortEffects(actor: object): Either<RegistryError, void>;
|
|
25
|
-
//#endregion
|
|
26
|
-
export { ActorInternal, RegistryError, abortEffects, drainInternal, getChildren, getOutputHandler, pushInternal, registerActor, setOutputHandler };
|
package/dist/internal.mjs
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { Either } from "@mantaq/utils";
|
|
2
|
-
//#region src/internal-registry.ts
|
|
3
|
-
const registry = globalThis.__mantaqCoreInternalRegistry ??= /* @__PURE__ */ new WeakMap();
|
|
4
|
-
const UNREGISTERED = { message: "[mantaq] actor is not registered with the internal registry" };
|
|
5
|
-
function withInternal(actor, fn) {
|
|
6
|
-
const internal = registry.get(actor);
|
|
7
|
-
return internal === void 0 ? Either.left(UNREGISTERED) : [void 0, fn(internal)];
|
|
8
|
-
}
|
|
9
|
-
function registerActor(actor, internal) {
|
|
10
|
-
registry.set(actor, internal);
|
|
11
|
-
}
|
|
12
|
-
function getChildren(actor) {
|
|
13
|
-
return withInternal(actor, (internal) => internal.children);
|
|
14
|
-
}
|
|
15
|
-
function getOutputHandler(actor) {
|
|
16
|
-
return withInternal(actor, (internal) => internal.getOutputHandler());
|
|
17
|
-
}
|
|
18
|
-
function setOutputHandler(actor, fn) {
|
|
19
|
-
return withInternal(actor, (internal) => internal.setOutputHandler(fn));
|
|
20
|
-
}
|
|
21
|
-
function pushInternal(actor, event) {
|
|
22
|
-
return withInternal(actor, (internal) => internal.pushInternal(event));
|
|
23
|
-
}
|
|
24
|
-
function drainInternal(actor) {
|
|
25
|
-
return withInternal(actor, (internal) => internal.drainInternal());
|
|
26
|
-
}
|
|
27
|
-
function abortEffects(actor) {
|
|
28
|
-
return withInternal(actor, (internal) => internal.abortEffects());
|
|
29
|
-
}
|
|
30
|
-
//#endregion
|
|
31
|
-
export { abortEffects, drainInternal, getChildren, getOutputHandler, pushInternal, registerActor, setOutputHandler };
|