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