@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 CHANGED
@@ -10,19 +10,19 @@ npm install @mantaq/core
10
10
 
11
11
  ## Quick Reference
12
12
 
13
- | Pattern | Correct | Anti-pattern |
14
- | --------------- | -------------------------------------- | ------------------------------------- |
15
- | Type context | `context: {} as MyContext` | casting in every handler |
16
- | Write context | `context.set({ ...context.get() })` | mutate without `set()` |
17
- | Type events | `event("ID")<Payload>()` | `event("ID")()` without generic |
18
- | Send events | `actor.send(signInEvent.create(data))` | raw `{ type: "ID", payload }` objects |
19
- | Effect data | Use state payload | Depend on `event` in effect |
20
- | Error handling | Emit internal event | Throw in effect or transition |
21
- | Internal events | Declare in `internal: [...]` | Put user events in internal |
22
- | Signal check | `if (signal.aborted) return` | Skip abort check in async work |
23
- | Regions | Child outputs in parent `internal` | Forget to declare child outputs |
24
- | Snapshot | `snapshot()` carries path + context | Re-serialize context by hand |
25
- | Any handler | Universal events only (CANCEL) | State-specific logic in `onAny` |
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 last known good state/context, the bad event), drops remaining events, and ignores later `send`s. Errors never escape `send()` and the machine stays deterministic — same inputs, same trace.
429
+ 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; // last known good state
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
- type ErrorState = StateRef<"__error", unknown, false>;
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 { 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 };
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 AnyStateRef, a as EffectInput, c as NonFinalStateRef, d as Clock, f as AnyEventRef, g as event, h as InternalEvent, i as EffectFn, l as TransitionResult, m as EventRef, n as Snapshot, o as ErrorInfo, p as CreatedOfEvent, r as CreatedOf, s as ErrorState, t as AnyActor, u as Context, v as StateRef, y as state } from "./actor-internal-DYAL1iYK.mjs";
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" | "done", fn: ((snapshot: Snapshot<ActorContext>, prev: Snapshot<ActorContext>) => void) | (() => void)): () => void;
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 + ms,
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
- const target = this.#now + ms;
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
- 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"}`);
297
+ Either.from(fn);
278
298
  }
279
299
  };
280
300
  //#endregion
281
301
  //#region src/snapshot.ts
282
- function buildSnapshot(s, regions, context, error) {
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)) console.warn(`[Actor] initial state "${initState.name}" not found in declared states [${[...stateNames].join(", ")}]`);
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) console.error(result[0].message);
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.addDone(cb);
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 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.`);
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 applied;
592
+ return {
593
+ applied,
594
+ target
595
+ };
513
596
  }
514
597
  #applyAnyStep(event, transition, allowTransition) {
515
- if (transition === void 0) return false;
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) this.#applyTransition(event, step);
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 emitted;
619
+ return {
620
+ emitted,
621
+ transitioned,
622
+ target
623
+ };
529
624
  }
530
625
  snapshot() {
531
- return buildSnapshot(this.state, this.#regions, this.#context, this.#error ?? void 0);
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 (this.state.isFinal) this.#subs.emitDone();
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
- #errorState = new StateRef("__error", false);
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
@@ -1,4 +1,4 @@
1
- import { h as InternalEvent, t as AnyActor } from "./actor-internal-DYAL1iYK.mjs";
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mantaq/core",
3
- "version": "0.1.0",
3
+ "version": "0.2.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": {