@mantaq/core 0.1.0 → 0.2.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 +44 -17
- package/dist/{actor-internal-DYAL1iYK.d.mts → actor-internal-B4iAxUG_.d.mts} +16 -3
- package/dist/index.d.mts +15 -3
- package/dist/index.mjs +136 -28
- package/dist/internal.d.mts +1 -1
- package/package.json +1 -1
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.set({ ...context.get() })`
|
|
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 | `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 + 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
|
|
@@ -337,7 +338,7 @@ m.effect(workingState, ({ signal, emit, clock }) => {
|
|
|
337
338
|
|
|
338
339
|
### Snapshot & Restore
|
|
339
340
|
|
|
340
|
-
`actor.snapshot()` returns a `Snapshot` — `{ path, context, regions, done?, error? }`. Context is included. Use it to save/restore actor state across sessions.
|
|
341
|
+
`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
342
|
|
|
342
343
|
```ts
|
|
343
344
|
// Snapshot carries path, context, and regions — serialize it directly
|
|
@@ -425,17 +426,24 @@ const parent = new Actor({
|
|
|
425
426
|
|
|
426
427
|
### Error Handling
|
|
427
428
|
|
|
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
|
|
429
|
+
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
430
|
|
|
430
431
|
```ts
|
|
431
432
|
const snap = actor.snapshot();
|
|
432
433
|
if (snap.error) {
|
|
433
|
-
snap.error.reason; // "transition" | "effect" | "budget" | "output" | "internal" | "async"
|
|
434
|
-
snap.error.state.name; //
|
|
434
|
+
snap.error.reason; // "transition" | "effect" | "budget" | "output" | "internal" | "async" | "unhandled"
|
|
435
|
+
snap.error.state.name; // the state at the point of failure
|
|
435
436
|
snap.error.event.type; // the bad event
|
|
436
437
|
}
|
|
437
438
|
```
|
|
438
439
|
|
|
440
|
+
Two deliberate exceptions to "every failure is loud":
|
|
441
|
+
|
|
442
|
+
- **Subscribers only watch.** They read snapshots and never change the machine, so a throwing `on("change")`/`on("done")`/`on("transition")` callback is swallowed — the machine and its callers are unaffected.
|
|
443
|
+
- **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"`).
|
|
444
|
+
|
|
445
|
+
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.
|
|
446
|
+
|
|
439
447
|
Catch errors inside effects and emit recovery events:
|
|
440
448
|
|
|
441
449
|
```ts
|
|
@@ -662,6 +670,25 @@ vp test
|
|
|
662
670
|
vp pack
|
|
663
671
|
```
|
|
664
672
|
|
|
673
|
+
## Testing
|
|
674
|
+
|
|
675
|
+
The package ships with a full test suite you can run and audit:
|
|
676
|
+
|
|
677
|
+
```bash
|
|
678
|
+
vp test # feature + error + property tests
|
|
679
|
+
vp check # format, lint, typecheck (includes type-level contract tests)
|
|
680
|
+
vp run mutation:core # mutation score must stay ≥ 90
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
**Test contract.** Behavior is pinned by four kinds of tests, split by filename:
|
|
684
|
+
|
|
685
|
+
- `*.test.ts` — features and happy paths, hand-maintained.
|
|
686
|
+
- `*.error.test.ts` — failure paths: warnings, budgets, abort, unregistered, the `__error` state.
|
|
687
|
+
- `*.property.test.ts` — property-based invariants against a reference model (fast-check, replayable via `MANTAQ_SEED`).
|
|
688
|
+
- `*.mutation.test.ts` — directed tests that kill specific mutants; regenerable and thrown away between stryker runs.
|
|
689
|
+
|
|
690
|
+
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.
|
|
691
|
+
|
|
665
692
|
## License
|
|
666
693
|
|
|
667
694
|
MIT
|
|
@@ -88,7 +88,7 @@ interface EffectInput<ActorContext, Payload = unknown> {
|
|
|
88
88
|
clock: Clock;
|
|
89
89
|
}
|
|
90
90
|
type EffectFn<ActorContext, Payload = unknown> = (input: EffectInput<ActorContext, Payload>) => void | Promise<void>;
|
|
91
|
-
type ErrorReason = "transition" | "effect" | "budget" | "output" | "internal" | "async";
|
|
91
|
+
type ErrorReason = "transition" | "effect" | "budget" | "output" | "internal" | "async" | "unhandled";
|
|
92
92
|
interface ErrorInfo {
|
|
93
93
|
error: unknown;
|
|
94
94
|
state: AnyStateRef;
|
|
@@ -96,7 +96,13 @@ interface ErrorInfo {
|
|
|
96
96
|
event: InternalEvent;
|
|
97
97
|
reason: ErrorReason;
|
|
98
98
|
}
|
|
99
|
-
|
|
99
|
+
interface TransitionInfo {
|
|
100
|
+
event: InternalEvent;
|
|
101
|
+
from: string;
|
|
102
|
+
to: string;
|
|
103
|
+
transitioned: boolean;
|
|
104
|
+
}
|
|
105
|
+
type ErrorState = StateRef<"__error", unknown, true>;
|
|
100
106
|
type TransitionResult<AllowedState extends AnyStateRef = AnyStateRef, AllowedEmit extends string = string> = {
|
|
101
107
|
state?: AllowedState | {
|
|
102
108
|
state: AllowedState;
|
|
@@ -105,6 +111,7 @@ type TransitionResult<AllowedState extends AnyStateRef = AnyStateRef, AllowedEmi
|
|
|
105
111
|
payload?: unknown;
|
|
106
112
|
emit?: Array<{
|
|
107
113
|
type: AllowedEmit;
|
|
114
|
+
payload?: unknown;
|
|
108
115
|
}>;
|
|
109
116
|
};
|
|
110
117
|
//#endregion
|
|
@@ -112,6 +119,7 @@ type TransitionResult<AllowedState extends AnyStateRef = AnyStateRef, AllowedEmi
|
|
|
112
119
|
interface Snapshot<C = unknown> {
|
|
113
120
|
path: string[];
|
|
114
121
|
context: C;
|
|
122
|
+
payload?: unknown;
|
|
115
123
|
regions: Record<string, Snapshot<unknown>>;
|
|
116
124
|
done?: boolean;
|
|
117
125
|
error?: ErrorInfo;
|
|
@@ -124,6 +132,11 @@ interface AnyActor<C = Record<string, unknown>> {
|
|
|
124
132
|
snapshot(): Snapshot<C>;
|
|
125
133
|
on(event: "change", fn: (snapshot: Snapshot<C>, prev: Snapshot<C>) => void): () => void;
|
|
126
134
|
on(event: "done", fn: () => void): () => void;
|
|
135
|
+
on(event: "transition", fn: (info: TransitionInfo) => void): () => void;
|
|
136
|
+
recover(target: {
|
|
137
|
+
state: AnyStateRef;
|
|
138
|
+
context: C;
|
|
139
|
+
}): void;
|
|
127
140
|
settled(): Promise<void>;
|
|
128
141
|
context?: C;
|
|
129
142
|
options?: {
|
|
@@ -136,4 +149,4 @@ interface AnyActor<C = Record<string, unknown>> {
|
|
|
136
149
|
};
|
|
137
150
|
}
|
|
138
151
|
//#endregion
|
|
139
|
-
export {
|
|
152
|
+
export { InternalEvent as _, EffectInput as a, StateRef as b, ErrorState as c, TransitionResult as d, Context as f, EventRef as g, CreatedOfEvent as h, EffectFn as i, NonFinalStateRef as l, AnyEventRef as m, Snapshot as n, ErrorInfo as o, Clock as p, CreatedOf as r, ErrorReason as s, AnyActor as t, TransitionInfo as u, event as v, state as x, AnyStateRef as y };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as
|
|
1
|
+
import { _ as InternalEvent, a as EffectInput, b as StateRef, c as ErrorState, d as TransitionResult, f as Context, g as EventRef, h as CreatedOfEvent, i as EffectFn, l as NonFinalStateRef, m as AnyEventRef, n as Snapshot, o as ErrorInfo, p as Clock, r as CreatedOf, s as ErrorReason, t as AnyActor, u as TransitionInfo, v as event, x as state, y as AnyStateRef } from "./actor-internal-B4iAxUG_.mjs";
|
|
2
2
|
//#region src/real-clock.d.ts
|
|
3
3
|
declare class RealClock implements Clock {
|
|
4
4
|
#private;
|
|
@@ -101,10 +101,22 @@ declare class Actor<const States extends readonly AnyStateRef[], const Inputs ex
|
|
|
101
101
|
get regions(): Record<string, AnyActor>;
|
|
102
102
|
get options(): InternalActorOptions<States, Inputs, Internal, Outputs, ActorContext>;
|
|
103
103
|
constructor(options: ActorOptions<States, Inputs, Internal, Outputs, ActorContext>);
|
|
104
|
-
on(event: "change"
|
|
104
|
+
on(event: "change", fn: (snapshot: Snapshot<ActorContext>, prev: Snapshot<ActorContext>) => void): () => void;
|
|
105
|
+
on(event: "done", fn: () => void): () => void;
|
|
106
|
+
on(event: "transition", fn: (info: TransitionInfo) => void): () => void;
|
|
105
107
|
settled(): Promise<void>;
|
|
106
108
|
send(event: CreatedOf$1<Inputs[number]>): void;
|
|
109
|
+
/**
|
|
110
|
+
* Manually restore the actor to a live state after a fatal `__error`.
|
|
111
|
+
* DANGER: the caller supplies state and context, so the trajectory is no
|
|
112
|
+
* longer deterministic ("same inputs, same trace"). Effects are not re-run
|
|
113
|
+
* and timers are not re-armed — processing resumes on the next event.
|
|
114
|
+
*/
|
|
115
|
+
recover(target: {
|
|
116
|
+
state: States[number];
|
|
117
|
+
context: ActorContext;
|
|
118
|
+
}): void;
|
|
107
119
|
snapshot(): Snapshot<ActorContext>;
|
|
108
120
|
}
|
|
109
121
|
//#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 };
|
|
122
|
+
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
|
@@ -86,13 +86,23 @@ var VirtualClock = class {
|
|
|
86
86
|
now() {
|
|
87
87
|
return this.#now;
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Delay normalization: NaN and ±Infinity throw (programmer error); negative
|
|
91
|
+
* and 0 clamp to 0 for timeouts like the platform, to a 1ms floor for
|
|
92
|
+
* intervals (a 0ms interval would spin the synchronous advance loop
|
|
93
|
+
* forever). Finite positive values schedule at their real deadline.
|
|
94
|
+
*/
|
|
95
|
+
#delay(ms, method, interval) {
|
|
96
|
+
if (!Number.isFinite(ms)) throw new RangeError(`[VirtualClock] invalid ${method} ms value: ${ms}`);
|
|
97
|
+
return ms <= 0 ? interval ? 1 : 0 : ms;
|
|
98
|
+
}
|
|
89
99
|
setTimeout(ms, cb, options) {
|
|
90
100
|
const signal = options?.signal;
|
|
91
101
|
if (signal?.aborted) return -1;
|
|
92
102
|
const id = this.#nextId++;
|
|
93
103
|
const onAbort = trackAbort(signal, id, this.#timers);
|
|
94
104
|
this.#timers.set(id, {
|
|
95
|
-
deadline: this.#now + ms,
|
|
105
|
+
deadline: this.#now + this.#delay(ms, "setTimeout", false),
|
|
96
106
|
cb,
|
|
97
107
|
signal,
|
|
98
108
|
onAbort,
|
|
@@ -112,9 +122,10 @@ var VirtualClock = class {
|
|
|
112
122
|
if (signal?.aborted) return -1;
|
|
113
123
|
const id = this.#nextId++;
|
|
114
124
|
const onAbort = trackAbort(signal, id, this.#intervals);
|
|
125
|
+
const d = this.#delay(ms, "setInterval", true);
|
|
115
126
|
this.#intervals.set(id, {
|
|
116
|
-
ms,
|
|
117
|
-
next: this.#now +
|
|
127
|
+
ms: d,
|
|
128
|
+
next: this.#now + d,
|
|
118
129
|
cb,
|
|
119
130
|
signal,
|
|
120
131
|
onAbort
|
|
@@ -165,7 +176,8 @@ var VirtualClock = class {
|
|
|
165
176
|
}
|
|
166
177
|
}
|
|
167
178
|
advance(ms) {
|
|
168
|
-
|
|
179
|
+
if (!Number.isFinite(ms)) throw new RangeError(`[VirtualClock] invalid advance ms value: ${ms}`);
|
|
180
|
+
const target = this.#now + Math.max(0, ms);
|
|
169
181
|
while (true) {
|
|
170
182
|
const deadline = this.#findEarliestDeadline(target);
|
|
171
183
|
if (deadline === null) break;
|
|
@@ -246,6 +258,7 @@ var InternalQueue = class {
|
|
|
246
258
|
var Subscribers = class {
|
|
247
259
|
change = /* @__PURE__ */ new Set();
|
|
248
260
|
done = /* @__PURE__ */ new Set();
|
|
261
|
+
transition = /* @__PURE__ */ new Set();
|
|
249
262
|
#last = null;
|
|
250
263
|
seed(snapshot) {
|
|
251
264
|
this.#last = snapshot;
|
|
@@ -260,6 +273,10 @@ var Subscribers = class {
|
|
|
260
273
|
this.done.add(fn);
|
|
261
274
|
return () => this.done.delete(fn);
|
|
262
275
|
}
|
|
276
|
+
addTransition(fn) {
|
|
277
|
+
this.transition.add(fn);
|
|
278
|
+
return () => this.transition.delete(fn);
|
|
279
|
+
}
|
|
263
280
|
emitChange(snapshot) {
|
|
264
281
|
const prev = this.#last ?? snapshot;
|
|
265
282
|
this.#last = snapshot;
|
|
@@ -268,18 +285,21 @@ var Subscribers = class {
|
|
|
268
285
|
emitDone() {
|
|
269
286
|
for (const fn of this.done) this.#safe(fn);
|
|
270
287
|
}
|
|
288
|
+
emitTransition(info) {
|
|
289
|
+
for (const fn of this.transition) this.#safe(() => fn(info));
|
|
290
|
+
}
|
|
271
291
|
clear() {
|
|
272
292
|
this.change.clear();
|
|
273
293
|
this.done.clear();
|
|
294
|
+
this.transition.clear();
|
|
274
295
|
}
|
|
275
296
|
#safe(fn) {
|
|
276
|
-
|
|
277
|
-
if (err[0] !== void 0) console.warn(`[Actor] subscriber threw: ${err[0] instanceof Error ? err[0].message : "unknown error"}`);
|
|
297
|
+
Either.from(fn);
|
|
278
298
|
}
|
|
279
299
|
};
|
|
280
300
|
//#endregion
|
|
281
301
|
//#region src/snapshot.ts
|
|
282
|
-
function buildSnapshot(s, regions, context,
|
|
302
|
+
function buildSnapshot(s, regions, context, opts = {}) {
|
|
283
303
|
const path = [s.name];
|
|
284
304
|
const regionSnapshots = {};
|
|
285
305
|
for (const [regionName, child] of Object.entries(regions)) regionSnapshots[regionName] = child.snapshot();
|
|
@@ -288,15 +308,15 @@ function buildSnapshot(s, regions, context, error) {
|
|
|
288
308
|
context,
|
|
289
309
|
regions: regionSnapshots
|
|
290
310
|
};
|
|
311
|
+
if (opts.payload !== void 0) snap.payload = opts.payload;
|
|
291
312
|
if (s.isFinal) snap.done = true;
|
|
292
|
-
if (error !== void 0) snap.error = error;
|
|
313
|
+
if (opts.error !== void 0) snap.error = opts.error;
|
|
293
314
|
return snap;
|
|
294
315
|
}
|
|
295
316
|
//#endregion
|
|
296
317
|
//#region src/effects.ts
|
|
297
318
|
function runEffects(options) {
|
|
298
319
|
const pending = [];
|
|
299
|
-
if (options.state.isFinal) return { pending };
|
|
300
320
|
const list = options.effects[options.state.name];
|
|
301
321
|
if (!list) return { pending };
|
|
302
322
|
for (const effectFn of list) {
|
|
@@ -438,32 +458,38 @@ var Actor = class {
|
|
|
438
458
|
this.clock.setDrain?.(() => this.#drainInternal());
|
|
439
459
|
const initState = resolveInitial(options.initial);
|
|
440
460
|
const stateNames = new Set(options.states.map((s) => s.name));
|
|
441
|
-
if (!stateNames.has(initState.name))
|
|
461
|
+
if (!stateNames.has(initState.name)) throw new Error(`[Actor] initial state "${initState.name}" not found in declared states [${[...stateNames].join(", ")}]`);
|
|
442
462
|
this.state = initState;
|
|
463
|
+
this.#statePayload = options.initial instanceof StateRef ? void 0 : options.initial.payload;
|
|
443
464
|
this.#context = options.context ?? {};
|
|
444
465
|
this.#lastState = initState;
|
|
445
466
|
this.#contextHandle = new Context(() => this.#context, (value) => {
|
|
446
467
|
this.#context = value;
|
|
447
468
|
this.#contextWritten = true;
|
|
448
469
|
});
|
|
449
|
-
this.#subs.seed(this.snapshot());
|
|
450
470
|
if (options.regions) for (const [key, child] of Object.entries(options.regions)) {
|
|
451
471
|
this.#regions[key] = child;
|
|
452
472
|
const result = setOutputHandler(child, (event) => {
|
|
453
473
|
this.#queue.push(event);
|
|
454
474
|
this.#drainInternal();
|
|
455
475
|
});
|
|
456
|
-
if (result[0] !== void 0)
|
|
476
|
+
if (result[0] !== void 0) throw new Error(result[0].message);
|
|
457
477
|
this.#children.set(key, child);
|
|
458
478
|
}
|
|
479
|
+
this.#runEffects({ type: "__init" }, this.#statePayload);
|
|
480
|
+
this.#subs.seed(this.snapshot());
|
|
459
481
|
}
|
|
460
482
|
on(event, fn) {
|
|
461
483
|
if (event === "change") {
|
|
462
484
|
const cb = fn;
|
|
463
485
|
return this.#subs.addChange(cb);
|
|
464
486
|
}
|
|
487
|
+
if (event === "done") {
|
|
488
|
+
const cb = fn;
|
|
489
|
+
return this.#subs.addDone(cb);
|
|
490
|
+
}
|
|
465
491
|
const cb = fn;
|
|
466
|
-
return this.#subs.
|
|
492
|
+
return this.#subs.addTransition(cb);
|
|
467
493
|
}
|
|
468
494
|
settled() {
|
|
469
495
|
return Promise.all([this.#queue.settled(), ...this.#pendingEffects]).then(() => void 0);
|
|
@@ -472,6 +498,26 @@ var Actor = class {
|
|
|
472
498
|
this.#dispatch(event);
|
|
473
499
|
this.#emitChangeIfDirty();
|
|
474
500
|
}
|
|
501
|
+
/**
|
|
502
|
+
* Manually restore the actor to a live state after a fatal `__error`.
|
|
503
|
+
* DANGER: the caller supplies state and context, so the trajectory is no
|
|
504
|
+
* longer deterministic ("same inputs, same trace"). Effects are not re-run
|
|
505
|
+
* and timers are not re-armed — processing resumes on the next event.
|
|
506
|
+
*/
|
|
507
|
+
recover(target) {
|
|
508
|
+
if (this.#error === null) return;
|
|
509
|
+
this.#error = null;
|
|
510
|
+
this.#queue = new InternalQueue();
|
|
511
|
+
this.#effectAbort = null;
|
|
512
|
+
this.state = target.state;
|
|
513
|
+
this.#statePayload = void 0;
|
|
514
|
+
this.#context = target.context;
|
|
515
|
+
this.#lastState = target.state;
|
|
516
|
+
this.#contextWritten = false;
|
|
517
|
+
this.#entry = null;
|
|
518
|
+
this.#pendingEffects = [];
|
|
519
|
+
this.#subs.emitChange(this.snapshot());
|
|
520
|
+
}
|
|
475
521
|
#emitChangeIfDirty() {
|
|
476
522
|
if (this.state === this.#lastState && !this.#contextWritten) return;
|
|
477
523
|
this.#lastState = this.state;
|
|
@@ -480,6 +526,11 @@ var Actor = class {
|
|
|
480
526
|
}
|
|
481
527
|
#dispatch(event) {
|
|
482
528
|
if (this.#error !== null || this.state.isFinal) return;
|
|
529
|
+
const normalized = {
|
|
530
|
+
...event,
|
|
531
|
+
payload: event.payload ?? {}
|
|
532
|
+
};
|
|
533
|
+
const from = this.state.name;
|
|
483
534
|
const prev = this.#entry;
|
|
484
535
|
this.#entry = {
|
|
485
536
|
state: this.state,
|
|
@@ -489,15 +540,44 @@ var Actor = class {
|
|
|
489
540
|
const transitions = this.#options.transitions;
|
|
490
541
|
const stateTransition = transitions[this.state.name]?.[event.type];
|
|
491
542
|
const anyTransition = transitions["Any"]?.[event.type];
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
543
|
+
const result = this.#step(normalized, stateTransition, anyTransition);
|
|
544
|
+
this.#maybeEmitTransition(normalized, from, Boolean(stateTransition || anyTransition), result);
|
|
545
|
+
if (result.emitted || this.#queue.length > 0) this.#drainInternal();
|
|
546
|
+
else if (!stateTransition && !anyTransition) {
|
|
547
|
+
if (this.#internalIds.has(event.type)) this.#enterError("unhandled", event, /* @__PURE__ */ new Error(`[Actor] internal event "${event.type}" emitted but no handler in state "${this.state.name}"`));
|
|
548
|
+
}
|
|
495
549
|
} finally {
|
|
496
550
|
this.#entry = prev;
|
|
497
551
|
}
|
|
498
552
|
}
|
|
553
|
+
#step(normalized, stateTransition, anyTransition) {
|
|
554
|
+
const stateOutcome = stateTransition ? this.#applyStateStep(normalized, stateTransition) : { applied: false };
|
|
555
|
+
if (this.#error === null) {
|
|
556
|
+
const anyOutcome = this.#applyAnyStep(normalized, anyTransition, !stateOutcome.applied);
|
|
557
|
+
return {
|
|
558
|
+
emitted: anyOutcome.emitted,
|
|
559
|
+
transitioned: stateOutcome.applied || anyOutcome.transitioned,
|
|
560
|
+
target: stateOutcome.target ?? anyOutcome.target
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
return {
|
|
564
|
+
emitted: false,
|
|
565
|
+
transitioned: false
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
#maybeEmitTransition(event, from, handled, outcome) {
|
|
569
|
+
if (this.#error !== null) return;
|
|
570
|
+
if (!handled) return;
|
|
571
|
+
this.#subs.emitTransition({
|
|
572
|
+
event,
|
|
573
|
+
from,
|
|
574
|
+
to: outcome.target ?? this.state.name,
|
|
575
|
+
transitioned: outcome.transitioned
|
|
576
|
+
});
|
|
577
|
+
}
|
|
499
578
|
#applyStateStep(event, transition) {
|
|
500
579
|
let applied = false;
|
|
580
|
+
let target;
|
|
501
581
|
this.#safe("transition", event, () => {
|
|
502
582
|
const step = transition(event, {
|
|
503
583
|
context: this.#contextHandle,
|
|
@@ -505,47 +585,72 @@ var Actor = class {
|
|
|
505
585
|
});
|
|
506
586
|
if (step.emit) this.#queue.push(...step.emit);
|
|
507
587
|
if (step.state) {
|
|
508
|
-
this.#applyTransition(event, step);
|
|
588
|
+
target = this.#applyTransition(event, step);
|
|
509
589
|
applied = true;
|
|
510
590
|
}
|
|
511
591
|
});
|
|
512
|
-
return
|
|
592
|
+
return {
|
|
593
|
+
applied,
|
|
594
|
+
target
|
|
595
|
+
};
|
|
513
596
|
}
|
|
514
597
|
#applyAnyStep(event, transition, allowTransition) {
|
|
515
|
-
if (transition === void 0) return
|
|
598
|
+
if (transition === void 0) return {
|
|
599
|
+
emitted: false,
|
|
600
|
+
transitioned: false
|
|
601
|
+
};
|
|
516
602
|
let emitted = false;
|
|
603
|
+
let transitioned = false;
|
|
604
|
+
let target;
|
|
517
605
|
this.#safe("transition", event, () => {
|
|
518
606
|
const step = transition(event, {
|
|
519
607
|
context: this.#contextHandle,
|
|
520
608
|
actor: this
|
|
521
609
|
});
|
|
522
|
-
if (step.state && allowTransition)
|
|
610
|
+
if (step.state && allowTransition) {
|
|
611
|
+
target = this.#applyTransition(event, step);
|
|
612
|
+
transitioned = true;
|
|
613
|
+
}
|
|
523
614
|
if (step.emit) {
|
|
524
615
|
this.#queue.push(...step.emit);
|
|
525
616
|
emitted = true;
|
|
526
617
|
}
|
|
527
618
|
});
|
|
528
|
-
return
|
|
619
|
+
return {
|
|
620
|
+
emitted,
|
|
621
|
+
transitioned,
|
|
622
|
+
target
|
|
623
|
+
};
|
|
529
624
|
}
|
|
530
625
|
snapshot() {
|
|
531
|
-
return buildSnapshot(this.state, this.#regions, this.#context,
|
|
626
|
+
return buildSnapshot(this.state, this.#regions, this.#context, {
|
|
627
|
+
error: this.#error ?? void 0,
|
|
628
|
+
payload: this.#statePayload
|
|
629
|
+
});
|
|
532
630
|
}
|
|
533
631
|
#abortEffects() {
|
|
534
632
|
this.#effectAbort?.abort();
|
|
535
633
|
this.#subs.clear();
|
|
536
634
|
}
|
|
537
635
|
#applyTransition(event, step) {
|
|
538
|
-
if (this.#error !== null || !step.state) return;
|
|
636
|
+
if (this.#error !== null || !step.state) return void 0;
|
|
539
637
|
const resolved = parseTarget(step);
|
|
540
|
-
if (!resolved) return;
|
|
638
|
+
if (!resolved) return void 0;
|
|
541
639
|
if (!(resolved.state instanceof StateRef)) {
|
|
542
640
|
this.#enterError("transition", event, /* @__PURE__ */ new Error("invalid transition target"));
|
|
543
641
|
return;
|
|
544
642
|
}
|
|
545
643
|
this.#effectAbort?.abort();
|
|
546
644
|
this.state = resolved.state;
|
|
645
|
+
const target = this.state.name;
|
|
646
|
+
this.#statePayload = resolved.payload;
|
|
647
|
+
this.#entry = {
|
|
648
|
+
state: this.state,
|
|
649
|
+
context: this.#context
|
|
650
|
+
};
|
|
547
651
|
this.#runEffects(event, resolved.payload);
|
|
548
|
-
if (
|
|
652
|
+
if (resolved.state.isFinal && this.#error === null) this.#subs.emitDone();
|
|
653
|
+
return target;
|
|
549
654
|
}
|
|
550
655
|
#runEffects(event, statePayload) {
|
|
551
656
|
const list = this.#options.effects[this.state.name];
|
|
@@ -566,7 +671,7 @@ var Actor = class {
|
|
|
566
671
|
event,
|
|
567
672
|
context: this.#contextHandle,
|
|
568
673
|
emit: (e) => {
|
|
569
|
-
if (this.#error !== null) return;
|
|
674
|
+
if (this.#error !== null || abort.signal.aborted) return;
|
|
570
675
|
this.#queue.push(e);
|
|
571
676
|
this.#drainInternal();
|
|
572
677
|
},
|
|
@@ -604,10 +709,11 @@ var Actor = class {
|
|
|
604
709
|
this.#emitChangeIfDirty();
|
|
605
710
|
}
|
|
606
711
|
}
|
|
607
|
-
#
|
|
712
|
+
#statePayload;
|
|
713
|
+
#pendingEffects = [];
|
|
714
|
+
#errorState = new StateRef("__error", true);
|
|
608
715
|
#error = null;
|
|
609
716
|
#entry = null;
|
|
610
|
-
#pendingEffects = [];
|
|
611
717
|
#safe(reason, event, fn) {
|
|
612
718
|
const attempt = Either.from(fn);
|
|
613
719
|
if (attempt[0] === void 0) return true;
|
|
@@ -629,9 +735,11 @@ var Actor = class {
|
|
|
629
735
|
};
|
|
630
736
|
this.#effectAbort?.abort();
|
|
631
737
|
this.state = this.#errorState;
|
|
738
|
+
this.#statePayload = void 0;
|
|
632
739
|
this.#lastState = this.#errorState;
|
|
633
740
|
this.#contextWritten = false;
|
|
634
741
|
this.#subs.emitChange(this.snapshot());
|
|
742
|
+
this.#subs.emitDone();
|
|
635
743
|
}
|
|
636
744
|
};
|
|
637
745
|
//#endregion
|
package/dist/internal.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { _ as InternalEvent, t as AnyActor } from "./actor-internal-B4iAxUG_.mjs";
|
|
2
2
|
import { Either } from "@mantaq/utils";
|
|
3
3
|
//#region src/internal-registry.d.ts
|
|
4
4
|
interface ActorInternal {
|