@mantaq/sugar 0.4.0 → 2.0.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
@@ -32,7 +32,10 @@ const machine = new Actor({
32
32
  clock,
33
33
  setup: (m) => {
34
34
  m.on(s.idle, e.fetch, () => ({ state: s.loading }));
35
- m.effect(s.loading, (input) => withTimeout(2000, input, () => e.resolve.create()));
35
+ m.effect(s.loading, {
36
+ name: "startFetchTimeout",
37
+ fn: (input) => withTimeout(2000, input, () => e.resolve.create()),
38
+ });
36
39
  m.on(s.loading, e.resolve, () => ({ state: s.success }));
37
40
  m.on(s.loading, e.fail, () => ({ state: s.error }));
38
41
  },
@@ -172,7 +175,7 @@ if (matches(actor, "idle") || matches(actor, "loading")) {
172
175
  // Breaks when hierarchy changes, verbose, error-prone
173
176
  ```
174
177
 
175
- `tag()` searches recursively through parallel regions and nested hierarchies — works regardless of depth.
178
+ `tag()` searches recursively through parallel regions and nested hierarchies. Works regardless of depth.
176
179
 
177
180
  ### ActorMap patterns
178
181
 
@@ -206,7 +209,7 @@ map.spawn("child1", () => createChild());
206
209
  map.spawn("child1", () => createChild()); // warns, aborts the previous child, replaces
207
210
  ```
208
211
 
209
- `ensure()` is idempotent — safe to call repeatedly. `spawn()` always replaces.
212
+ `ensure()` is idempotent. Safe to call repeatedly. `spawn()` always replaces.
210
213
 
211
214
  ### Effect utilities: `withPromise` vs manual handling
212
215
 
@@ -279,7 +282,7 @@ if (snap && isIn(snap, "active")) {
279
282
  }
280
283
  ```
281
284
 
282
- **`ensure(key, factory)`** — spawn only if key missing:
285
+ **`ensure(key, factory)`**. Spawn only if key missing:
283
286
 
284
287
  ```ts
285
288
  map.ensure("child1", () => childActor); // no-op if exists
@@ -310,7 +313,7 @@ Works with `ActorMap` or any object implementing `{ keys(): string[]; send(key,
310
313
  ### `onOutput(actor, handler)`
311
314
 
312
315
  Route an actor's emitted outputs to a handler. `regions` auto-wire child
313
- outputs into the parent; ActorMap children do not — this is the public
316
+ outputs into the parent; ActorMap children do not. This is the public
314
317
  wrapper for that wiring seam, no internal import needed:
315
318
 
316
319
  ```ts
@@ -373,7 +376,7 @@ e.click.is(emittedEvent); // boolean
373
376
 
374
377
  ### `withPromise(promise, signal, emit, events)`
375
378
 
376
- Bridge a promise into actor events. Emits `success` on resolve, `error` on reject. Respects `AbortSignal` — skips emit if aborted before settlement.
379
+ Bridge a promise into actor events. Emits `success` on resolve, `error` on reject. Respects `AbortSignal`. Skips emit if aborted before settlement.
377
380
 
378
381
  ```ts
379
382
  import { withPromise } from "@mantaq/sugar";
@@ -434,7 +437,7 @@ const busy = tag(s.idle, s.loading);
434
437
  busy.has(actor.snapshot()); // recursive
435
438
  ```
436
439
 
437
- No migration needed. Sugar wraps core — use both side by side.
440
+ No migration needed. Sugar wraps core. Use both side by side.
438
441
 
439
442
  ## License
440
443
 
package/dist/index.d.mts CHANGED
@@ -1,48 +1,66 @@
1
1
  import { Actor, ActorBuilder, ActorOptions, AnyActor, AnyEventRef, AnyStateRef, Clock, EffectInput, EventRef, InitialState, InternalEvent, Snapshot, StateRef } from "@mantaq/core";
2
- //#region src/actors/matches.d.ts
3
- declare function matches(actor: {
2
+ interface MatchTarget {
4
3
  snapshot(): Snapshot;
5
- }, pattern: string): boolean;
6
- //#endregion
7
- //#region src/transitions/broadcast.d.ts
4
+ }
5
+ /**
6
+ * True when the actor's snapshot matches the dotted pattern. Accepts the
7
+ * pattern as one dotted string or as separate segments. A segment containing
8
+ * dots matches states whose own name contains dots.
9
+ */
10
+ export declare function matches(actor: MatchTarget, ...pattern: [pattern: string]): boolean;
11
+ /**
12
+ * Plain event shape sugar helpers accept next to core event refs.
13
+ */
8
14
  type EventLike = {
9
15
  type: string;
10
16
  payload?: unknown;
11
17
  };
18
+ /**
19
+ * Anything sendable through sugar helpers — a core ref or a plain object.
20
+ */
12
21
  type SendableEvent = AnyEventRef | EventLike;
13
22
  interface SendableMap<T extends SendableEvent = SendableEvent> {
14
23
  keys(): string[];
15
- send(key: string, event: T): void;
24
+ send(key: string, ...events: [event: T]): void;
16
25
  }
17
- declare function broadcast<const T extends SendableEvent>(map: SendableMap<T>, event: T): void;
18
- //#endregion
19
- //#region src/actors/actor-map.d.ts
20
- interface ActorMapOptions {
21
- /**
22
- * When true, a child that reaches a final state (or dies into `__error`)
23
- * is removed from the map automatically. Without it, completed children
24
- * linger until explicitly `kill`ed.
25
- */
26
- autoReap?: boolean;
26
+ /**
27
+ * Fan every given event out to all children of the map, once per key, in
28
+ * key order.
29
+ */
30
+ export declare function broadcast<const T extends SendableEvent>(map: SendableMap<T>, ...events: [event: T]): void;
31
+ /**
32
+ * Minimal actor surface the map needs. Real actors satisfy this
33
+ * structurally — tests can substitute tiny fakes.
34
+ */
35
+ interface ActorMapChild {
36
+ snapshot(): Snapshot;
37
+ on(event: "done", listener: {
38
+ fn: () => void;
39
+ }): () => void;
40
+ send(...events: [event: SendableEvent]): void;
41
+ dispose(): void;
27
42
  }
28
43
  /**
29
44
  * Keyed registry of one actor type. `spawn(key)` builds a fresh instance via
30
45
  * the factory — same actor shape, keyed by id. The factory receives the key.
46
+ *
47
+ * With autoReap enabled, a child that reaches a final state (or dies into
48
+ * `__error`) is removed from the map automatically. Without it, completed
49
+ * children linger until explicitly killed.
31
50
  */
32
- declare class ActorMap implements SendableMap<SendableEvent> {
33
- #private;
34
- constructor(factory: (id: string) => AnyActor, options?: ActorMapOptions);
51
+ interface ActorMap extends SendableMap<SendableEvent> {
35
52
  spawn(key: string): void;
36
- send(key: string, event: SendableEvent): void;
37
53
  kill(key: string): void;
38
- get size(): number;
54
+ dispose(): void;
55
+ ensure(key: string): void;
39
56
  has(key: string): boolean;
40
57
  keys(): string[];
41
- ensure(key: string): void;
42
58
  snapshot(key: string): Snapshot | undefined;
59
+ readonly size: number;
43
60
  }
44
- //#endregion
45
- //#region src/state.d.ts
61
+ export declare function createActorMap(factory: (id: string) => ActorMapChild, { autoReap }?: {
62
+ autoReap?: boolean;
63
+ }): ActorMap;
46
64
  interface StatesEntry {
47
65
  name: string;
48
66
  final?: boolean;
@@ -55,47 +73,40 @@ type FinalOf<T> = T extends {
55
73
  final: true;
56
74
  } ? true : false;
57
75
  type StatesRecord<T extends StatesArg[]> = { [K in T[number] as NameOf<K>]: StateRef<NameOf<K>, unknown, FinalOf<K>>; };
58
- declare function states<const T extends StatesArg[]>(...entries: T): StatesRecord<T>;
59
- //#endregion
60
- //#region src/event.d.ts
61
- declare function events<T extends string>(...names: T[]): { [K in T]: EventRef<K>; };
62
- //#endregion
63
- //#region src/output.d.ts
76
+ export declare function states<const T extends StatesArg[]>(...entries: T): StatesRecord<T>;
77
+ export declare function events<T extends string>(...names: T[]): { [K in T]: EventRef<K>; };
64
78
  /**
65
- * Route an actor's emitted outputs to a handler.
79
+ * Route an actor's emitted outputs to one or more handlers.
66
80
  *
67
81
  * `regions` auto-wire child outputs into the parent. ActorMap children do
68
82
  * not — this is the public wrapper for that wiring seam. One call in the
69
- * factory connects a child's declared outputs to a receiver.
83
+ * factory connects a child's declared outputs to a receiver — pass several
84
+ * handlers to feed multiple receivers.
70
85
  */
71
- declare function onOutput(actor: AnyActor, handler: (event: InternalEvent) => void): void;
72
- //#endregion
73
- //#region src/effects/promise.d.ts
74
- type EmitFn = (event: {
86
+ export declare function onOutput(actor: AnyActor, ...handlers: [handler: (event: InternalEvent) => void]): void;
87
+ type AnyEvent = {
75
88
  type: string;
76
89
  payload?: unknown;
77
- }) => void;
78
- declare function withPromise<T>(promise: Promise<T>, signal: AbortSignal, emit: EmitFn, events: {
79
- success: (data: T) => {
80
- type: string;
81
- payload?: unknown;
82
- };
83
- error: (err: unknown) => {
84
- type: string;
85
- payload?: unknown;
90
+ };
91
+ /** `Emit`: events the effect may send; follows the effect's `sends` list when declared. */
92
+ interface WithPromiseOptions<T, Emit = AnyEvent> {
93
+ promise: Promise<T>;
94
+ signal: AbortSignal;
95
+ emit: (event: Emit) => void;
96
+ events: {
97
+ success: (data: T) => NoInfer<Emit>;
98
+ error: (err: unknown) => NoInfer<Emit>;
86
99
  };
100
+ }
101
+ export declare function withPromise<T, Emit = AnyEvent>(options: WithPromiseOptions<T, Emit>): Promise<void>;
102
+ export declare function withTimeout<ActorContext, Payload = unknown, Emit = InternalEvent>(durationMs: number, options: {
103
+ input: EffectInput<ActorContext, Payload, Emit>;
104
+ event: () => NoInfer<Emit>;
87
105
  }): void;
88
- //#endregion
89
- //#region src/effects/timeout.d.ts
90
- declare function withTimeout<ActorContext>(ms: number, input: EffectInput<ActorContext>, event: () => InternalEvent): void;
91
- //#endregion
92
- //#region src/tags.d.ts
93
106
  interface Tag {
94
107
  has(snapshot: Snapshot): boolean;
95
108
  }
96
- declare function tag(...stateNames: StateRef<string, unknown>[]): Tag;
97
- //#endregion
98
- //#region src/parts.d.ts
109
+ export declare function tag(...stateRefs: StateRef<string, unknown>[]): Tag;
99
110
  interface ActorSpec {
100
111
  states: readonly AnyStateRef[];
101
112
  inputs: readonly AnyEventRef[];
@@ -115,10 +126,11 @@ type InternalOf<S extends ActorSpec> = "internal" extends keyof S ? [NotUndef<S[
115
126
  type OutputsOf<S extends ActorSpec> = "outputs" extends keyof S ? [NotUndef<S["outputs"]>] extends [never] ? readonly [] : NotUndef<S["outputs"]> : readonly [];
116
127
  type ContextOf<S extends ActorSpec> = "context" extends keyof S ? [NotUndef<S["context"]>] extends [never] ? Record<string, unknown> : NotUndef<S["context"]> : Record<string, unknown>;
117
128
  type BuilderOf<S extends ActorSpec> = ActorBuilder<S["states"], S["inputs"], InternalOf<S>, OutputsOf<S>, ContextOf<S>>;
118
- type Fragment<S extends ActorSpec> = (m: BuilderOf<S>) => void;
119
- declare function definePart<S extends ActorSpec = never>(fn: (m: BuilderOf<S>) => void): Fragment<S>;
120
- declare function use<S extends ActorSpec>(m: BuilderOf<S>, part: Fragment<S>): void;
121
- type Part<States extends readonly AnyStateRef[], Inputs extends readonly AnyEventRef[], Internal extends readonly AnyEventRef[], Outputs extends readonly AnyEventRef[], ActorContext> = (m: ActorBuilder<States, Inputs, Internal, Outputs, ActorContext>) => void;
129
+ type Fragment<S extends ActorSpec> = (builder: BuilderOf<S>) => void;
130
+ export declare function definePart<S extends ActorSpec = never>(part: (builder: BuilderOf<S>) => void): Fragment<S>;
131
+ /** Apply one or more parts to a builder, in order. */
132
+ export declare function use<S extends ActorSpec>(builder: BuilderOf<S>, ...parts: [Fragment<S>, ...Fragment<S>[]]): void;
133
+ type Part<States extends readonly AnyStateRef[], Inputs extends readonly AnyEventRef[], Internal extends readonly AnyEventRef[], Outputs extends readonly AnyEventRef[], ActorContext> = (builder: ActorBuilder<States, Inputs, Internal, Outputs, ActorContext>) => void;
122
134
  type SpecValue<States extends readonly AnyStateRef[], Inputs extends readonly AnyEventRef[], Internal extends readonly AnyEventRef[], Outputs extends readonly AnyEventRef[], ActorContext, Initial> = {
123
135
  states: States;
124
136
  inputs: Inputs;
@@ -133,14 +145,18 @@ type SpecValue<States extends readonly AnyStateRef[], Inputs extends readonly An
133
145
  /**
134
146
  * Construct the actor's static spec — states, inputs, internal/output events,
135
147
  * context, initial state, clock, regions and budget. State/event tuples and
136
- * the initial ref narrow to literals; the context stays mutable, so
148
+ * the initial ref narrow to literals while the context stays mutable, so
137
149
  * `definePart` handlers read and write it with no `as` casts.
138
150
  */
139
- declare function actorSpec<const States extends readonly AnyStateRef[], const Inputs extends readonly AnyEventRef[], const Internal extends readonly AnyEventRef[] = readonly [], const Outputs extends readonly AnyEventRef[] = readonly [], ActorContext = Record<string, unknown>, const Initial extends InitialState<States[number]> = InitialState<States[number]>>(spec: SpecValue<States, Inputs, Internal, Outputs, ActorContext, Initial>): SpecValue<States, Inputs, Internal, Outputs, ActorContext, Initial>;
140
- declare function withParts<const States extends readonly AnyStateRef[], const Inputs extends readonly AnyEventRef[], const Internal extends readonly AnyEventRef[] = readonly [], const Outputs extends readonly AnyEventRef[] = readonly [], ActorContext = Record<string, unknown>>(base: Omit<ActorOptions<States, Inputs, Internal, Outputs, ActorContext>, "setup">, parts: Part<States, Inputs, Internal, Outputs, ActorContext> | readonly Part<States, Inputs, Internal, Outputs, ActorContext>[]): Actor<States, Inputs, Internal, Outputs, ActorContext>;
141
- //#endregion
142
- //#region src/snapshot.d.ts
143
- declare function isIn(snapshot: Snapshot, stateRefName: string): boolean;
144
- declare function activeLeaves(snapshot: Snapshot): string[];
145
- //#endregion
146
- export { ActorMap, type ActorSpec, type BuilderOf, type EventLike, type Fragment, type Part, type SendableEvent, type SendableMap, activeLeaves, actorSpec, broadcast, definePart, events, isIn, matches, onOutput, states, tag, use, withParts, withPromise, withTimeout };
151
+ export declare function actorSpec<const States extends readonly AnyStateRef[], const Inputs extends readonly AnyEventRef[], const Internal extends readonly AnyEventRef[] = readonly [], const Outputs extends readonly AnyEventRef[] = readonly [], ActorContext = Record<string, unknown>, const Initial extends InitialState<States[number]> = InitialState<States[number]>>(spec: SpecValue<States, Inputs, Internal, Outputs, ActorContext, Initial>): SpecValue<States, Inputs, Internal, Outputs, ActorContext, Initial>;
152
+ /**
153
+ * Build an actor whose setup applies every given part in order. Pass parts
154
+ * as separate arguments — single part or many.
155
+ */
156
+ export declare function withParts<const States extends readonly AnyStateRef[], const Inputs extends readonly AnyEventRef[], const Internal extends readonly AnyEventRef[] = readonly [], const Outputs extends readonly AnyEventRef[] = readonly [], ActorContext = Record<string, unknown>>(base: Omit<ActorOptions<States, Inputs, Internal, Outputs, ActorContext>, "setup">, ...parts: Part<States, Inputs, Internal, Outputs, ActorContext>[]): Actor<States, Inputs, Internal, Outputs, ActorContext>;
157
+ /**
158
+ * True when any of the named states is active anywhere in the snapshot tree.
159
+ */
160
+ export declare function isIn(snapshot: Snapshot, ...stateNames: [stateName: string]): boolean;
161
+ export declare function activeLeaves(snapshot: Snapshot): string[];
162
+ export type { ActorMap, ActorMapChild, ActorSpec, BuilderOf, EventLike, Fragment, Part, SendableEvent, SendableMap, WithPromiseOptions };
package/dist/index.mjs CHANGED
@@ -1,80 +1,118 @@
1
1
  import { Actor, event, state } from "@mantaq/core";
2
- //#region src/actors/matches.ts
3
- function matchSnapshot(snapshot, parts, index) {
4
- const stateName = snapshot.path[0];
5
- for (let end = index; end < parts.length; end++) {
6
- if (parts.slice(index, end + 1).join(".") !== stateName) continue;
7
- if (end === parts.length - 1) return true;
8
- const regionSnap = snapshot.regions[parts[end + 1]];
9
- if (!regionSnap) return false;
10
- if (end + 1 === parts.length - 1) return true;
11
- return matchSnapshot(regionSnap, parts, end + 2);
2
+ // src/actors/matches.ts
3
+ function matchFrom(patternParts) {
4
+ function matchRegion(options) {
5
+ const stateName = options.snapshot.path[0];
6
+ for (let end = options.index; end < patternParts.length; end++) {
7
+ if (patternParts.slice(options.index, end + 1).join(".") !== stateName) continue;
8
+ if (end === patternParts.length - 1) return true;
9
+ const regionSnap = options.snapshot.regions[patternParts[end + 1]];
10
+ if (!regionSnap) return false;
11
+ if (end + 1 === patternParts.length - 1) return true;
12
+ return matchRegion({
13
+ snapshot: regionSnap,
14
+ index: end + 2
15
+ });
16
+ }
17
+ return false;
12
18
  }
13
- return false;
14
- }
15
- function matches(actor, pattern) {
16
- if (!pattern || pattern.endsWith(".") || pattern.startsWith(".") || pattern.includes("..")) return false;
17
- const parts = pattern.split(".");
18
- return matchSnapshot(actor.snapshot(), parts, 0);
19
+ return matchRegion;
19
20
  }
20
- //#endregion
21
- //#region src/actors/actor-map.ts
22
21
  /**
23
- * Keyed registry of one actor type. `spawn(key)` builds a fresh instance via
24
- * the factory — same actor shape, keyed by id. The factory receives the key.
22
+ * True when the actor's snapshot matches the dotted pattern. Accepts the
23
+ * pattern as one dotted string or as separate segments. A segment containing
24
+ * dots matches states whose own name contains dots.
25
25
  */
26
- var ActorMap = class {
27
- #actors = /* @__PURE__ */ new Map();
28
- #reapers = /* @__PURE__ */ new Map();
29
- #factory;
30
- #autoReap;
31
- constructor(factory, options = {}) {
32
- this.#factory = factory;
33
- this.#autoReap = options.autoReap === true;
34
- }
35
- spawn(key) {
36
- if (this.#actors.has(key)) this.kill(key);
37
- const child = this.#factory(key);
38
- this.#actors.set(key, child);
39
- if (this.#autoReap) {
40
- if (child.snapshot().done) {
41
- this.#actors.delete(key);
42
- return;
43
- }
44
- const off = child.on("done", () => {
45
- if (this.#actors.get(key) === child) this.#actors.delete(key);
46
- this.#reapers.delete(key);
47
- });
48
- this.#reapers.set(key, off);
26
+ function matches(actor, ...pattern) {
27
+ const joined = pattern.join(".");
28
+ if (!joined || joined.endsWith(".") || joined.startsWith(".") || joined.includes("..")) return false;
29
+ return matchFrom(joined.split("."))({
30
+ snapshot: actor.snapshot(),
31
+ index: 0
32
+ });
33
+ }
34
+ // src/actors/actor-map.ts
35
+ function createActorMap(factory, { autoReap } = {}) {
36
+ const actors = /* @__PURE__ */ new Map();
37
+ const reapers = /* @__PURE__ */ new Map();
38
+ const reap = autoReap === true;
39
+ function spawn(key) {
40
+ if (actors.has(key)) kill(key);
41
+ const child = factory(key);
42
+ actors.set(key, child);
43
+ if (!reap) return;
44
+ if (child.snapshot().done) {
45
+ actors.delete(key);
46
+ Promise.resolve().then(() => child.dispose());
47
+ return;
49
48
  }
49
+ const off = child.on("done", { fn: () => {
50
+ off();
51
+ if (actors.get(key) === child) actors.delete(key);
52
+ reapers.delete(key);
53
+ Promise.resolve().then(() => child.dispose());
54
+ } });
55
+ reapers.set(key, off);
50
56
  }
51
- send(key, event) {
52
- this.#actors.get(key)?.send(event);
53
- }
54
- kill(key) {
55
- this.#actors.get(key)?.dispose();
56
- this.#reapers.get(key)?.();
57
- this.#reapers.delete(key);
58
- this.#actors.delete(key);
57
+ function kill(key) {
58
+ actors.get(key)?.dispose();
59
+ reapers.get(key)?.();
60
+ reapers.delete(key);
61
+ actors.delete(key);
59
62
  }
60
- get size() {
61
- return this.#actors.size;
63
+ /**
64
+ * Tear down the map and every live child. Without this, dropping an
65
+ * actor map (e.g. when its owning actor is disposed) leaks every child
66
+ * actor whose effects/timers/reapers keep running. Idempotent.
67
+ */
68
+ function dispose() {
69
+ for (const child of actors.values()) child.dispose();
70
+ for (const off of reapers.values()) off();
71
+ reapers.clear();
72
+ actors.clear();
62
73
  }
63
- has(key) {
64
- return this.#actors.has(key);
74
+ function ensure(key) {
75
+ if (!actors.has(key)) spawn(key);
65
76
  }
66
- keys() {
67
- return [...this.#actors.keys()];
77
+ function has(key) {
78
+ return actors.has(key);
68
79
  }
69
- ensure(key) {
70
- if (!this.#actors.has(key)) this.spawn(key);
80
+ function keys() {
81
+ return [...actors.keys()];
71
82
  }
72
- snapshot(key) {
73
- return this.#actors.get(key)?.snapshot();
83
+ function snapshotOf(key) {
84
+ return actors.get(key)?.snapshot();
74
85
  }
75
- };
76
- //#endregion
77
- //#region src/state.ts
86
+ return {
87
+ spawn,
88
+ kill,
89
+ dispose,
90
+ ensure,
91
+ has,
92
+ keys,
93
+ snapshot: snapshotOf,
94
+ send(key, ...events) {
95
+ for (const event of events) actors.get(key)?.send(event);
96
+ },
97
+ get size() {
98
+ return actors.size;
99
+ }
100
+ };
101
+ }
102
+ // src/parse-refs.ts
103
+ /**
104
+ * Boundary utility for the `states()` and `events()` helpers. Ref records are
105
+ * built dynamically, keyed by a literal name tuple. TypeScript cannot express
106
+ * that assignment, so the narrowing from the runtime-shaped record to the
107
+ * mapped tuple type happens here — the only place a cast is trusted.
108
+ */
109
+ function parseStateRefs(record) {
110
+ return record;
111
+ }
112
+ function parseEventRefs(record) {
113
+ return record;
114
+ }
115
+ // src/state.ts
78
116
  function states(...entries) {
79
117
  const result = {};
80
118
  for (const entry of entries) {
@@ -82,93 +120,110 @@ function states(...entries) {
82
120
  const ref = state(name)();
83
121
  result[name] = typeof entry === "object" && entry.final ? ref.final() : ref;
84
122
  }
85
- return result;
123
+ return parseStateRefs(result);
86
124
  }
87
- //#endregion
88
- //#region src/event.ts
125
+ // src/event.ts
89
126
  function events(...names) {
90
127
  const result = {};
91
128
  for (const name of names) result[name] = event(name)();
92
- return result;
129
+ return parseEventRefs(result);
93
130
  }
94
- //#endregion
95
- //#region src/output.ts
131
+ // src/output.ts
96
132
  /**
97
- * Route an actor's emitted outputs to a handler.
133
+ * Route an actor's emitted outputs to one or more handlers.
98
134
  *
99
135
  * `regions` auto-wire child outputs into the parent. ActorMap children do
100
136
  * not — this is the public wrapper for that wiring seam. One call in the
101
- * factory connects a child's declared outputs to a receiver.
137
+ * factory connects a child's declared outputs to a receiver — pass several
138
+ * handlers to feed multiple receivers.
102
139
  */
103
- function onOutput(actor, handler) {
104
- actor.on("output", handler);
140
+ function onOutput(actor, ...handlers) {
141
+ for (const handler of handlers) actor.on("output", { fn: handler });
105
142
  }
106
- //#endregion
107
- //#region src/effects/promise.ts
108
- function withPromise(promise, signal, emit, events) {
109
- promise.then((data) => {
143
+ // src/effects/promise.ts
144
+ function withPromise(options) {
145
+ const { promise, signal, emit, events } = options;
146
+ return promise.then((data) => {
110
147
  if (!signal.aborted) emit(events.success(data));
111
- }).catch((err) => {
148
+ }, (err) => {
112
149
  if (!signal.aborted) emit(events.error(err));
113
150
  });
114
151
  }
115
- //#endregion
116
- //#region src/effects/timeout.ts
117
- function withTimeout(ms, input, event) {
118
- input.clock.setTimeout(ms, () => {
119
- if (input.signal.aborted) return;
120
- input.emit(event());
121
- }, { signal: input.signal });
122
- }
123
- //#endregion
124
- //#region src/transitions/broadcast.ts
125
- function broadcast(map, event) {
126
- for (const key of map.keys()) map.send(key, event);
127
- }
128
- //#endregion
129
- //#region src/snapshot.ts
130
- function isIn(snapshot, stateRefName) {
131
- if (snapshot.path[0] === stateRefName) return true;
132
- for (const region of Object.values(snapshot.regions)) if (isIn(region, stateRefName)) return true;
152
+ // src/effects/timeout.ts
153
+ function withTimeout(durationMs, options) {
154
+ const { input, event } = options;
155
+ input.clock.setTimeout(durationMs, {
156
+ signal: input.signal,
157
+ cb: () => {
158
+ if (input.signal.aborted) return;
159
+ input.emit(event());
160
+ }
161
+ });
162
+ }
163
+ // src/transitions/broadcast.ts
164
+ /**
165
+ * Fan every given event out to all children of the map, once per key, in
166
+ * key order.
167
+ */
168
+ function broadcast(map, ...events) {
169
+ for (const event of events) for (const key of map.keys()) map.send(key, event);
170
+ }
171
+ // src/snapshot.ts
172
+ function isInState(options) {
173
+ if (options.snapshot.path[0] === options.stateName) return true;
174
+ for (const region of Object.values(options.snapshot.regions)) if (isInState({
175
+ snapshot: region,
176
+ stateName: options.stateName
177
+ })) return true;
133
178
  return false;
134
179
  }
180
+ /**
181
+ * True when any of the named states is active anywhere in the snapshot tree.
182
+ */
183
+ function isIn(snapshot, ...stateNames) {
184
+ return stateNames.some((stateName) => isInState({
185
+ snapshot,
186
+ stateName
187
+ }));
188
+ }
135
189
  function activeLeaves(snapshot) {
136
190
  if (Object.keys(snapshot.regions).length === 0) return [snapshot.path.join(".")];
137
191
  const leaves = [];
138
192
  for (const [regionName, region] of Object.entries(snapshot.regions)) for (const leaf of activeLeaves(region)) leaves.push(`${snapshot.path[0]}.${regionName}.${leaf}`);
139
193
  return leaves;
140
194
  }
141
- //#endregion
142
- //#region src/tags.ts
143
- function tag(...stateNames) {
144
- const names = stateNames.map((s) => s.name);
195
+ // src/tags.ts
196
+ function tag(...stateRefs) {
197
+ const names = stateRefs.map((ref) => ref.name);
145
198
  return { has: (snapshot) => names.some((name) => isIn(snapshot, name)) };
146
199
  }
147
- //#endregion
148
- //#region src/parts.ts
149
- function definePart(fn) {
150
- return fn;
200
+ // src/parts.ts
201
+ function definePart(part) {
202
+ return part;
151
203
  }
152
- function use(m, part) {
153
- part(m);
204
+ /** Apply one or more parts to a builder, in order. */
205
+ function use(builder, ...parts) {
206
+ for (const part of parts) part(builder);
154
207
  }
155
208
  /**
156
209
  * Construct the actor's static spec — states, inputs, internal/output events,
157
210
  * context, initial state, clock, regions and budget. State/event tuples and
158
- * the initial ref narrow to literals; the context stays mutable, so
211
+ * the initial ref narrow to literals while the context stays mutable, so
159
212
  * `definePart` handlers read and write it with no `as` casts.
160
213
  */
161
214
  function actorSpec(spec) {
162
215
  return spec;
163
216
  }
164
- function withParts(base, parts) {
165
- const list = Array.isArray(parts) ? parts : [parts];
166
- return new Actor({
217
+ /**
218
+ * Build an actor whose setup applies every given part in order. Pass parts
219
+ * as separate arguments — single part or many.
220
+ */
221
+ function withParts(base, ...parts) {
222
+ return Actor({
167
223
  ...base,
168
- setup: (m) => {
169
- for (const part of list) part(m);
224
+ setup: (builder) => {
225
+ for (const part of parts) part(builder);
170
226
  }
171
227
  });
172
228
  }
173
- //#endregion
174
- export { ActorMap, activeLeaves, actorSpec, broadcast, definePart, events, isIn, matches, onOutput, states, tag, use, withParts, withPromise, withTimeout };
229
+ export { activeLeaves, actorSpec, broadcast, createActorMap, definePart, events, isIn, matches, onOutput, states, tag, use, withParts, withPromise, withTimeout };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mantaq/sugar",
3
- "version": "0.4.0",
3
+ "version": "2.0.0",
4
4
  "description": "Userland sugar for mantaq actors — matches, effect helpers, transition utilities.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -20,19 +20,19 @@
20
20
  "access": "public"
21
21
  },
22
22
  "dependencies": {
23
- "@mantaq/core": "0.3.0"
23
+ "@mantaq/core": "0.5.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@stryker-mutator/core": "^9",
27
- "@stryker-mutator/vitest-runner": "^9",
26
+ "@stryker-mutator/core": "^10",
27
+ "@stryker-mutator/vitest-runner": "^10",
28
28
  "@types/node": "^26.2.0",
29
29
  "@typescript/native-preview": "7.0.0-dev.20260707.2",
30
- "@vitest/coverage-v8": "4.1.10",
31
- "typescript": "^7.0.2",
32
- "vite": "npm:@voidzero-dev/vite-plus-core@0.2.8",
33
- "vite-plus": "0.2.8",
34
- "vitest": "4.1.10",
35
- "@mantaq/pbt": "0.0.2"
30
+ "@vitest/coverage-v8": "5.0.5",
31
+ "typescript": "6.0.3",
32
+ "vite": "npm:@voidzero-dev/vite-plus-core@1.4.0",
33
+ "vite-plus": "1.4.0",
34
+ "vitest": "5.0.5",
35
+ "@mantaq/pbt": "0.0.3"
36
36
  },
37
37
  "scripts": {
38
38
  "build": "vp pack",