@mantaq/core 0.2.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 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 + payload | 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 | `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
 
@@ -97,20 +97,24 @@ const actor = new Actor({
97
97
  setup: (m) => {
98
98
  m.on(idleState, signInEvent, (event, opts) => {
99
99
  // ✅ typed — context.get()/set() flow the constructor type
100
- opts!.context.set({ ...opts!.context.get(), phoneNumber: event.payload.phoneNumber });
100
+ const s = opts!.context.get();
101
+ s.phoneNumber = event.payload.phoneNumber;
102
+ opts!.context.set(s);
101
103
  return { state: signingInState };
102
104
  });
103
105
  },
104
106
  });
105
107
  ```
106
108
 
107
- `set()` is the write signal — always call it to write, even when the new value is the same reference. Any `set()` triggers `change`. Spread the current value:
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:
108
110
 
109
111
  ```ts
110
- opts.context.set({ ...opts.context.get(), phoneNumber: event.payload.phoneNumber });
112
+ const s = opts.context.get();
113
+ s.phoneNumber = event.payload.phoneNumber;
114
+ opts.context.set(s);
111
115
  ```
112
116
 
113
- **Never mutate in place:**
117
+ **Never mutate without `set()`:**
114
118
 
115
119
  ```ts
116
120
  // ❌ mutation without set() — never signals; subscribers and persistence stay silent
@@ -123,7 +127,9 @@ opts!.context.phoneNumber = event.payload.phoneNumber;
123
127
  setup: (m) => {
124
128
  m.on(idleState, signInEvent, (event, opts) => {
125
129
  const c = opts!.context as AuthContext; // ❌ unnecessary — already typed
126
- c.set({ ...c.get(), phoneNumber: event.payload.phoneNumber });
130
+ const s = c.get();
131
+ s.phoneNumber = event.payload.phoneNumber;
132
+ c.set(s);
127
133
  return { state: signingInState };
128
134
  });
129
135
  },
@@ -214,7 +220,9 @@ const actor = new Actor({
214
220
  return { state: loadingState.create({ url: event.payload.url }) };
215
221
 
216
222
  // ⚠️ context — works but shared across all states
217
- opts!.context.set({ ...opts!.context.get(), url: event.payload.url });
223
+ const s = opts!.context.get();
224
+ s.url = event.payload.url;
225
+ opts!.context.set(s);
218
226
  return { state: loadingState };
219
227
  ```
220
228
 
@@ -304,10 +312,9 @@ function createActor() {
304
312
  m.on(idleState, startEvent, () => ({ state: workingState }));
305
313
  m.on(workingState, doneEvent, () => ({ state: doneState }));
306
314
  m.on(workingState, failedEvent, (_event, opts) => {
307
- opts!.context.set({
308
- ...opts!.context.get(),
309
- retryCount: opts!.context.get().retryCount + 1,
310
- });
315
+ const s = opts!.context.get();
316
+ s.retryCount += 1;
317
+ opts!.context.set(s);
311
318
  return { state: errorState };
312
319
  });
313
320
  },
@@ -439,7 +446,7 @@ if (snap.error) {
439
446
 
440
447
  Two deliberate exceptions to "every failure is loud":
441
448
 
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.
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.
443
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"`).
444
451
 
445
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.
@@ -496,7 +503,9 @@ Prefer storing the error in context and transitioning to an error state:
496
503
  ```ts
497
504
  m.on(idleState, submitEvent, (event, opts) => {
498
505
  if (!event.payload.data) {
499
- opts!.context.set({ ...opts!.context.get(), error: "missing data" });
506
+ const s = opts!.context.get();
507
+ s.error = "missing data";
508
+ opts!.context.set(s);
500
509
  return { state: errorState };
501
510
  }
502
511
  return { state: doneState };
@@ -511,7 +520,9 @@ Use `onAny` to intercept events across all states — useful for universal error
511
520
  setup: (m) => {
512
521
  // CANCEL works from any state
513
522
  m.onAny(cancelEvent, (_event, opts) => {
514
- opts!.context.set({ ...opts!.context.get(), cancelled: true });
523
+ const s = opts!.context.get();
524
+ s.cancelled = true;
525
+ opts!.context.set(s);
515
526
  return { state: cancelledState };
516
527
  });
517
528
  // State-specific handlers still run for their state
@@ -525,13 +536,17 @@ setup: (m) => {
525
536
  setup: (m) => {
526
537
  // ❌ state-specific logic in onAny defeats the purpose
527
538
  m.onAny(submitBasicInfoEvent, (event, opts) => {
528
- opts!.context.set({ ...opts!.context.get(), basicInfo: event }); // wrong — only makes sense in basicInfo state
539
+ const s = opts!.context.get();
540
+ s.basicInfo = event; // wrong — only makes sense in basicInfo state
541
+ opts!.context.set(s);
529
542
  return { state: shippingAddressState };
530
543
  });
531
544
 
532
545
  // ✅ keep state-specific logic in state handlers
533
546
  m.on(basicInfoState, submitBasicInfoEvent, (event, opts) => {
534
- opts!.context.set({ ...opts!.context.get(), basicInfo: event });
547
+ const s = opts!.context.get();
548
+ s.basicInfo = event;
549
+ opts!.context.set(s);
535
550
  return { state: shippingAddressState };
536
551
  });
537
552
  },
package/dist/index.d.mts CHANGED
@@ -1,4 +1,68 @@
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";
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;
@@ -104,19 +258,23 @@ declare class Actor<const States extends readonly AnyStateRef[], const Inputs ex
104
258
  on(event: "change", fn: (snapshot: Snapshot<ActorContext>, prev: Snapshot<ActorContext>) => void): () => void;
105
259
  on(event: "done", fn: () => void): () => void;
106
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;
107
263
  settled(): Promise<void>;
108
264
  send(event: CreatedOf$1<Inputs[number]>): void;
109
265
  /**
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.
266
+ * Restore a live state after `__error`. DANGER: caller supplies state+context,
267
+ * breaking determinism. Effects not re-run; processing resumes on next event.
114
268
  */
115
269
  recover(target: {
116
270
  state: States[number];
117
271
  context: ActorContext;
118
272
  }): void;
119
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;
120
278
  }
121
279
  //#endregion
122
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,766 +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
- /**
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
- }
99
- setTimeout(ms, cb, options) {
100
- const signal = options?.signal;
101
- if (signal?.aborted) return -1;
102
- const id = this.#nextId++;
103
- const onAbort = trackAbort(signal, id, this.#timers);
104
- this.#timers.set(id, {
105
- deadline: this.#now + this.#delay(ms, "setTimeout", false),
106
- cb,
107
- signal,
108
- onAbort,
109
- eventName: options?.eventName
110
- });
111
- return id;
112
- }
113
- clearTimeout(id) {
114
- const timer = this.#timers.get(id);
115
- if (timer) {
116
- clearAbort(timer);
117
- this.#timers.delete(id);
118
- }
119
- }
120
- setInterval(ms, cb, options) {
121
- const signal = options?.signal;
122
- if (signal?.aborted) return -1;
123
- const id = this.#nextId++;
124
- const onAbort = trackAbort(signal, id, this.#intervals);
125
- const d = this.#delay(ms, "setInterval", true);
126
- this.#intervals.set(id, {
127
- ms: d,
128
- next: this.#now + d,
129
- cb,
130
- signal,
131
- onAbort
132
- });
133
- return id;
134
- }
135
- clearInterval(id) {
136
- const interval = this.#intervals.get(id);
137
- if (interval) {
138
- clearAbort(interval);
139
- this.#intervals.delete(id);
140
- }
141
- }
142
- #findEarliestDeadline(target) {
143
- let earliest = target;
144
- let found = false;
145
- for (const t of this.#timers.values()) if (t.deadline <= target && t.deadline <= earliest) {
146
- earliest = t.deadline;
147
- found = true;
148
- }
149
- for (const t of this.#intervals.values()) if (t.next <= target && t.next <= earliest) {
150
- earliest = t.next;
151
- found = true;
152
- }
153
- return found ? earliest : null;
154
- }
155
- #fireTimersAt(deadline) {
156
- const ids = [];
157
- for (const [id, t] of this.#timers) if (t.deadline === deadline) ids.push(id);
158
- for (const id of ids) {
159
- const timer = this.#timers.get(id);
160
- if (timer) {
161
- clearAbort(timer);
162
- this.#timers.delete(id);
163
- timer.cb();
164
- }
165
- }
166
- }
167
- #fireIntervalsAt(deadline) {
168
- const ids = [];
169
- for (const [id, t] of this.#intervals) if (t.next === deadline) ids.push(id);
170
- for (const id of ids) {
171
- const interval = this.#intervals.get(id);
172
- if (interval) {
173
- interval.cb();
174
- if (this.#intervals.has(id)) interval.next = this.#now + interval.ms;
175
- }
176
- }
177
- }
178
- advance(ms) {
179
- if (!Number.isFinite(ms)) throw new RangeError(`[VirtualClock] invalid advance ms value: ${ms}`);
180
- const target = this.#now + Math.max(0, ms);
181
- while (true) {
182
- const deadline = this.#findEarliestDeadline(target);
183
- if (deadline === null) break;
184
- this.#now = deadline;
185
- this.#fireTimersAt(deadline);
186
- this.#fireIntervalsAt(deadline);
187
- }
188
- this.#now = target;
189
- this.#drain?.();
190
- }
191
- hasPending() {
192
- return this.#timers.size > 0 || this.#intervals.size > 0;
193
- }
194
- pendingTimers() {
195
- const result = [];
196
- for (const [id, t] of this.#timers) result.push({
197
- id,
198
- deadline: t.deadline,
199
- ms: t.deadline - this.#now,
200
- eventName: t.eventName
201
- });
202
- return result;
203
- }
204
- setDrain(fn) {
205
- this.#drain = fn;
206
- }
207
- };
208
- //#endregion
209
- //#region src/queue.ts
210
- var InternalQueue = class {
211
- #queue = [];
212
- #index = 0;
213
- #processing = false;
214
- #stopped = false;
215
- #settledResolvers = [];
216
- get length() {
217
- return this.#queue.length - this.#index;
218
- }
219
- push(...events) {
220
- if (this.#stopped) return;
221
- this.#queue.push(...events);
222
- }
223
- settled() {
224
- if (this.length === 0 && !this.#processing) return Promise.resolve();
225
- return new Promise((resolve) => {
226
- this.#settledResolvers.push(resolve);
227
- });
228
- }
229
- processCancellable(processEvent) {
230
- this.#process(processEvent);
231
- }
232
- #process(processEvent) {
233
- if (this.#processing) return;
234
- this.#processing = true;
235
- try {
236
- while (this.#index < this.#queue.length) {
237
- if (this.#stopped) break;
238
- const event = this.#queue[this.#index++];
239
- if (!processEvent(event)) {
240
- this.#stopped = true;
241
- break;
242
- }
243
- }
244
- } finally {
245
- this.#queue.length = 0;
246
- this.#index = 0;
247
- this.#processing = false;
248
- this.#stopped = false;
249
- if (this.#settledResolvers.length > 0) {
250
- const resolvers = this.#settledResolvers.splice(0);
251
- for (const resolve of resolvers) resolve();
252
- }
253
- }
254
- }
255
- };
256
- //#endregion
257
- //#region src/subscribers.ts
258
- var Subscribers = class {
259
- change = /* @__PURE__ */ new Set();
260
- done = /* @__PURE__ */ new Set();
261
- transition = /* @__PURE__ */ new Set();
262
- #last = null;
263
- seed(snapshot) {
264
- this.#last = snapshot;
265
- }
266
- addChange(fn) {
267
- this.change.add(fn);
268
- const last = this.#last;
269
- if (last) this.#safe(() => fn(last, last));
270
- return () => this.change.delete(fn);
271
- }
272
- addDone(fn) {
273
- this.done.add(fn);
274
- return () => this.done.delete(fn);
275
- }
276
- addTransition(fn) {
277
- this.transition.add(fn);
278
- return () => this.transition.delete(fn);
279
- }
280
- emitChange(snapshot) {
281
- const prev = this.#last ?? snapshot;
282
- this.#last = snapshot;
283
- for (const fn of this.change) this.#safe(() => fn(snapshot, prev));
284
- }
285
- emitDone() {
286
- for (const fn of this.done) this.#safe(fn);
287
- }
288
- emitTransition(info) {
289
- for (const fn of this.transition) this.#safe(() => fn(info));
290
- }
291
- clear() {
292
- this.change.clear();
293
- this.done.clear();
294
- this.transition.clear();
295
- }
296
- #safe(fn) {
297
- Either.from(fn);
298
- }
299
- };
300
- //#endregion
301
- //#region src/snapshot.ts
302
- function buildSnapshot(s, regions, context, opts = {}) {
303
- const path = [s.name];
304
- const regionSnapshots = {};
305
- for (const [regionName, child] of Object.entries(regions)) regionSnapshots[regionName] = child.snapshot();
306
- const snap = {
307
- path,
308
- context,
309
- regions: regionSnapshots
310
- };
311
- if (opts.payload !== void 0) snap.payload = opts.payload;
312
- if (s.isFinal) snap.done = true;
313
- if (opts.error !== void 0) snap.error = opts.error;
314
- return snap;
315
- }
316
- //#endregion
317
- //#region src/effects.ts
318
- function runEffects(options) {
319
- const pending = [];
320
- const list = options.effects[options.state.name];
321
- if (!list) return { pending };
322
- for (const effectFn of list) {
323
- if (options.abort.signal.aborted) break;
324
- const attempt = Either.from(() => effectFn({
325
- signal: options.abort.signal,
326
- state: {
327
- name: options.state.name,
328
- payload: options.statePayload
329
- },
330
- event: options.event,
331
- context: options.context,
332
- emit: options.emit,
333
- clock: options.clock
334
- }));
335
- if (attempt[0] !== void 0) {
336
- options.onError(attempt[0], options.lastGood);
337
- break;
338
- }
339
- const out = attempt[1];
340
- if (out instanceof Promise) pending.push(out.catch((error) => {
341
- if (options.abort.signal.aborted) return;
342
- options.onError(error, options.lastGood);
343
- }));
344
- }
345
- return { pending };
346
- }
347
- //#endregion
348
- //#region src/dispatch.ts
349
- function parseTarget(step) {
350
- if (!step.state) return void 0;
351
- if (step.state instanceof StateRef) return {
352
- state: step.state,
353
- payload: step.payload
354
- };
355
- return {
356
- state: step.state.state,
357
- payload: step.state.payload
358
- };
359
- }
360
- //#endregion
361
- //#region src/context.ts
362
- var Context = class {
363
- #get;
364
- #set;
365
- constructor(get, set) {
366
- this.#get = get;
367
- this.#set = set;
368
- }
369
- get() {
370
- return this.#get();
371
- }
372
- set(value) {
373
- this.#set(value);
374
- }
375
- };
376
- //#endregion
377
- //#region src/builder.ts
378
- var ActorBuilder = class {
379
- #transitions = {};
380
- #effects = {};
381
- on(stateRef, eventRef, fn) {
382
- const sName = stateRef.name;
383
- const eType = eventRef.type;
384
- (this.#transitions[sName] ??= {})[eType] = fn;
385
- return this;
386
- }
387
- onAny(eventRef, fn) {
388
- const eType = eventRef.type;
389
- (this.#transitions["Any"] ??= {})[eType] = fn;
390
- return this;
391
- }
392
- effect(stateRef, fn) {
393
- (this.#effects[stateRef.name] ??= []).push(fn);
394
- return this;
395
- }
396
- /** @internal */ build() {
397
- return {
398
- transitions: this.#transitions,
399
- effects: this.#effects
400
- };
401
- }
402
- };
403
- //#endregion
404
- //#region src/actor.ts
405
- function resolveInitial(initial) {
406
- return initial instanceof StateRef ? initial : initial.state;
407
- }
408
- var Actor = class {
409
- state;
410
- clock;
411
- #context;
412
- #contextHandle;
413
- #contextWritten = false;
414
- #lastState;
415
- #options;
416
- #regions = {};
417
- #children = /* @__PURE__ */ new Map();
418
- #queue = new InternalQueue();
419
- #subs = new Subscribers();
420
- #effectAbort = null;
421
- #outputHandler = null;
422
- #internalIds;
423
- #inputIds;
424
- #internalBudget;
425
- #draining = false;
426
- get context() {
427
- return this.#context;
428
- }
429
- get regions() {
430
- return this.#regions;
431
- }
432
- get options() {
433
- return this.#options;
434
- }
435
- constructor(options) {
436
- registerActor(this, {
437
- children: this.#children,
438
- getOutputHandler: () => this.#outputHandler,
439
- setOutputHandler: (fn) => {
440
- this.#outputHandler = fn;
441
- },
442
- pushInternal: (event) => this.#queue.push(event),
443
- drainInternal: () => this.#drainInternal(),
444
- abortEffects: () => this.#abortEffects()
445
- });
446
- const builder = new ActorBuilder();
447
- options.setup(builder);
448
- const built = builder.build();
449
- this.#options = {
450
- ...options,
451
- transitions: built.transitions,
452
- effects: built.effects
453
- };
454
- this.#internalIds = new Set((options.internal ?? []).map((e) => e.type));
455
- this.#inputIds = new Set(options.inputs.map((e) => e.type));
456
- this.#internalBudget = options.internalBudget ?? 1e4;
457
- this.clock = options.clock ?? new RealClock();
458
- this.clock.setDrain?.(() => this.#drainInternal());
459
- const initState = resolveInitial(options.initial);
460
- const stateNames = new Set(options.states.map((s) => s.name));
461
- if (!stateNames.has(initState.name)) throw new Error(`[Actor] initial state "${initState.name}" not found in declared states [${[...stateNames].join(", ")}]`);
462
- this.state = initState;
463
- this.#statePayload = options.initial instanceof StateRef ? void 0 : options.initial.payload;
464
- this.#context = options.context ?? {};
465
- this.#lastState = initState;
466
- this.#contextHandle = new Context(() => this.#context, (value) => {
467
- this.#context = value;
468
- this.#contextWritten = true;
469
- });
470
- if (options.regions) for (const [key, child] of Object.entries(options.regions)) {
471
- this.#regions[key] = child;
472
- const result = setOutputHandler(child, (event) => {
473
- this.#queue.push(event);
474
- this.#drainInternal();
475
- });
476
- if (result[0] !== void 0) throw new Error(result[0].message);
477
- this.#children.set(key, child);
478
- }
479
- this.#runEffects({ type: "__init" }, this.#statePayload);
480
- this.#subs.seed(this.snapshot());
481
- }
482
- on(event, fn) {
483
- if (event === "change") {
484
- const cb = fn;
485
- return this.#subs.addChange(cb);
486
- }
487
- if (event === "done") {
488
- const cb = fn;
489
- return this.#subs.addDone(cb);
490
- }
491
- const cb = fn;
492
- return this.#subs.addTransition(cb);
493
- }
494
- settled() {
495
- return Promise.all([this.#queue.settled(), ...this.#pendingEffects]).then(() => void 0);
496
- }
497
- send(event) {
498
- this.#dispatch(event);
499
- this.#emitChangeIfDirty();
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
- }
521
- #emitChangeIfDirty() {
522
- if (this.state === this.#lastState && !this.#contextWritten) return;
523
- this.#lastState = this.state;
524
- this.#contextWritten = false;
525
- this.#subs.emitChange(this.snapshot());
526
- }
527
- #dispatch(event) {
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;
534
- const prev = this.#entry;
535
- this.#entry = {
536
- state: this.state,
537
- context: this.#context
538
- };
539
- try {
540
- const transitions = this.#options.transitions;
541
- const stateTransition = transitions[this.state.name]?.[event.type];
542
- const anyTransition = transitions["Any"]?.[event.type];
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
- }
549
- } finally {
550
- this.#entry = prev;
551
- }
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
- }
578
- #applyStateStep(event, transition) {
579
- let applied = false;
580
- let target;
581
- this.#safe("transition", event, () => {
582
- const step = transition(event, {
583
- context: this.#contextHandle,
584
- actor: this
585
- });
586
- if (step.emit) this.#queue.push(...step.emit);
587
- if (step.state) {
588
- target = this.#applyTransition(event, step);
589
- applied = true;
590
- }
591
- });
592
- return {
593
- applied,
594
- target
595
- };
596
- }
597
- #applyAnyStep(event, transition, allowTransition) {
598
- if (transition === void 0) return {
599
- emitted: false,
600
- transitioned: false
601
- };
602
- let emitted = false;
603
- let transitioned = false;
604
- let target;
605
- this.#safe("transition", event, () => {
606
- const step = transition(event, {
607
- context: this.#contextHandle,
608
- actor: this
609
- });
610
- if (step.state && allowTransition) {
611
- target = this.#applyTransition(event, step);
612
- transitioned = true;
613
- }
614
- if (step.emit) {
615
- this.#queue.push(...step.emit);
616
- emitted = true;
617
- }
618
- });
619
- return {
620
- emitted,
621
- transitioned,
622
- target
623
- };
624
- }
625
- snapshot() {
626
- return buildSnapshot(this.state, this.#regions, this.#context, {
627
- error: this.#error ?? void 0,
628
- payload: this.#statePayload
629
- });
630
- }
631
- #abortEffects() {
632
- this.#effectAbort?.abort();
633
- this.#subs.clear();
634
- }
635
- #applyTransition(event, step) {
636
- if (this.#error !== null || !step.state) return void 0;
637
- const resolved = parseTarget(step);
638
- if (!resolved) return void 0;
639
- if (!(resolved.state instanceof StateRef)) {
640
- this.#enterError("transition", event, /* @__PURE__ */ new Error("invalid transition target"));
641
- return;
642
- }
643
- this.#effectAbort?.abort();
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
- };
651
- this.#runEffects(event, resolved.payload);
652
- if (resolved.state.isFinal && this.#error === null) this.#subs.emitDone();
653
- return target;
654
- }
655
- #runEffects(event, statePayload) {
656
- const list = this.#options.effects[this.state.name];
657
- if (!list) {
658
- this.#effectAbort = null;
659
- return;
660
- }
661
- const abort = new AbortController();
662
- this.#effectAbort = abort;
663
- const lastGood = this.#entry ?? {
664
- state: this.state,
665
- context: this.#context
666
- };
667
- const result = runEffects({
668
- effects: { [this.state.name]: list },
669
- state: this.state,
670
- statePayload,
671
- event,
672
- context: this.#contextHandle,
673
- emit: (e) => {
674
- if (this.#error !== null || abort.signal.aborted) return;
675
- this.#queue.push(e);
676
- this.#drainInternal();
677
- },
678
- clock: this.clock,
679
- abort,
680
- lastGood,
681
- onError: (error) => this.#enterError("effect", event, error, lastGood)
682
- });
683
- this.#pendingEffects.push(...result.pending);
684
- }
685
- #drainInternal() {
686
- if (this.#draining) return;
687
- this.#draining = true;
688
- try {
689
- const budget = this.#internalBudget;
690
- let count = 0;
691
- this.#queue.processCancellable((event) => {
692
- if (this.#error !== null) return false;
693
- if (count >= budget) {
694
- this.#enterError("budget", event, /* @__PURE__ */ new Error("internal event budget exceeded"));
695
- return false;
696
- }
697
- count++;
698
- if (this.#internalIds.has(event.type) || this.#inputIds.has(event.type)) this.#dispatch(event);
699
- else {
700
- const outputHandler = this.#outputHandler;
701
- if (outputHandler) {
702
- if (!this.#safe("output", event, () => outputHandler(event))) return false;
703
- }
704
- }
705
- return true;
706
- });
707
- } finally {
708
- this.#draining = false;
709
- this.#emitChangeIfDirty();
710
- }
711
- }
712
- #statePayload;
713
- #pendingEffects = [];
714
- #errorState = new StateRef("__error", true);
715
- #error = null;
716
- #entry = null;
717
- #safe(reason, event, fn) {
718
- const attempt = Either.from(fn);
719
- if (attempt[0] === void 0) return true;
720
- this.#enterError(reason, event, attempt[0]);
721
- return false;
722
- }
723
- #enterError(reason, event, error, lastGood) {
724
- if (this.#error !== null) return;
725
- const entry = lastGood ?? this.#entry ?? {
726
- state: this.state,
727
- context: this.#context
728
- };
729
- this.#error = {
730
- error,
731
- state: entry.state,
732
- context: entry.context,
733
- event,
734
- reason
735
- };
736
- this.#effectAbort?.abort();
737
- this.state = this.#errorState;
738
- this.#statePayload = void 0;
739
- this.#lastState = this.#errorState;
740
- this.#contextWritten = false;
741
- this.#subs.emitChange(this.snapshot());
742
- this.#subs.emitDone();
743
- }
744
- };
745
- //#endregion
746
- //#region src/event.ts
747
- function event(type) {
748
- return () => new EventRef(type);
749
- }
750
- var EventRef = class {
751
- type;
752
- constructor(type) {
753
- this.type = type;
754
- }
755
- is(anyEvent) {
756
- return !!anyEvent && typeof anyEvent === "object" && "type" in anyEvent && anyEvent.type === this.type;
757
- }
758
- create(payload) {
759
- return payload === void 0 ? { type: this.type } : {
760
- type: this.type,
761
- payload
762
- };
763
- }
764
- };
765
- //#endregion
766
- 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.2.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.2"
24
+ "@mantaq/utils": "0.0.3"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@stryker-mutator/core": "^9",
@@ -1,152 +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" | "unhandled";
92
- interface ErrorInfo {
93
- error: unknown;
94
- state: AnyStateRef;
95
- context: unknown;
96
- event: InternalEvent;
97
- reason: ErrorReason;
98
- }
99
- interface TransitionInfo {
100
- event: InternalEvent;
101
- from: string;
102
- to: string;
103
- transitioned: boolean;
104
- }
105
- type ErrorState = StateRef<"__error", unknown, true>;
106
- type TransitionResult<AllowedState extends AnyStateRef = AnyStateRef, AllowedEmit extends string = string> = {
107
- state?: AllowedState | {
108
- state: AllowedState;
109
- payload?: unknown;
110
- };
111
- payload?: unknown;
112
- emit?: Array<{
113
- type: AllowedEmit;
114
- payload?: unknown;
115
- }>;
116
- };
117
- //#endregion
118
- //#region src/actor-internal.d.ts
119
- interface Snapshot<C = unknown> {
120
- path: string[];
121
- context: C;
122
- payload?: unknown;
123
- regions: Record<string, Snapshot<unknown>>;
124
- done?: boolean;
125
- error?: ErrorInfo;
126
- }
127
- interface AnyActor<C = Record<string, unknown>> {
128
- state: AnyStateRef;
129
- clock: Clock;
130
- regions: Record<string, AnyActor>;
131
- send(event: AnyEventRef | InternalEvent): void;
132
- snapshot(): Snapshot<C>;
133
- on(event: "change", fn: (snapshot: Snapshot<C>, prev: Snapshot<C>) => void): () => void;
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;
140
- settled(): Promise<void>;
141
- context?: C;
142
- options?: {
143
- transitions?: Record<string, Record<string, unknown>>;
144
- effects?: Record<string, unknown[]>;
145
- states?: ReadonlyArray<{
146
- name: string;
147
- isFinal: boolean;
148
- }>;
149
- };
150
- }
151
- //#endregion
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 };
@@ -1,26 +0,0 @@
1
- import { _ as InternalEvent, t as AnyActor } from "./actor-internal-B4iAxUG_.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 };