@mantaq/core 0.1.0 → 0.3.0

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