@mantaq/sugar 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
@@ -307,6 +307,29 @@ broadcast(map, { type: "ping" });
307
307
 
308
308
  Works with `ActorMap` or any object implementing `{ keys(): string[]; send(key, event): void }`.
309
309
 
310
+ ### `onOutput(actor, handler)`
311
+
312
+ 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
314
+ wrapper for that wiring seam, no internal import needed:
315
+
316
+ ```ts
317
+ import { ActorMap, onOutput } from "@mantaq/sugar";
318
+
319
+ const requests = new ActorMap(
320
+ (id) => {
321
+ const child = createHandler(id);
322
+ onOutput(child, (e) => {
323
+ if (result.is(e)) parent.send(e);
324
+ });
325
+ return child;
326
+ },
327
+ { autoReap: true },
328
+ );
329
+ ```
330
+
331
+ The `is()` guard narrows the output to the receiver's declared input.
332
+
310
333
  ### `tag(...stateRefs)`
311
334
 
312
335
  Group multiple `StateRef`s and test if a snapshot matches any of them. Useful for UI state grouping.
package/dist/index.d.mts CHANGED
@@ -17,25 +17,59 @@ interface SendableMap<T extends SendableEvent = SendableEvent> {
17
17
  declare function broadcast<const T extends SendableEvent>(map: SendableMap<T>, event: T): void;
18
18
  //#endregion
19
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;
27
+ }
28
+ /**
29
+ * Keyed registry of one actor type. `spawn(key)` builds a fresh instance via
30
+ * the factory — same actor shape, keyed by id. The factory receives the key.
31
+ */
20
32
  declare class ActorMap implements SendableMap<SendableEvent> {
21
33
  #private;
22
- constructor(parent?: AnyActor);
23
- spawn(key: string, factory: () => AnyActor): void;
34
+ constructor(factory: (id: string) => AnyActor, options?: ActorMapOptions);
35
+ spawn(key: string): void;
24
36
  send(key: string, event: SendableEvent): void;
25
37
  kill(key: string): void;
26
38
  get size(): number;
27
39
  has(key: string): boolean;
28
40
  keys(): string[];
29
- ensure(key: string, factory: () => AnyActor): void;
41
+ ensure(key: string): void;
30
42
  snapshot(key: string): Snapshot | undefined;
31
43
  }
32
44
  //#endregion
33
45
  //#region src/state.d.ts
34
- declare function states<T extends string>(...names: T[]): { [K in T]: StateRef<K>; };
46
+ interface StatesEntry {
47
+ name: string;
48
+ final?: boolean;
49
+ }
50
+ type StatesArg = string | StatesEntry;
51
+ type NameOf<T> = T extends string ? T : T extends {
52
+ name: infer N extends string;
53
+ } ? N : never;
54
+ type FinalOf<T> = T extends {
55
+ final: true;
56
+ } ? true : false;
57
+ 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>;
35
59
  //#endregion
36
60
  //#region src/event.d.ts
37
61
  declare function events<T extends string>(...names: T[]): { [K in T]: EventRef<K>; };
38
62
  //#endregion
63
+ //#region src/output.d.ts
64
+ /**
65
+ * Route an actor's emitted outputs to a handler.
66
+ *
67
+ * `regions` auto-wire child outputs into the parent. ActorMap children do
68
+ * 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.
70
+ */
71
+ declare function onOutput(actor: AnyActor, handler: (event: InternalEvent) => void): void;
72
+ //#endregion
39
73
  //#region src/effects/promise.d.ts
40
74
  type EmitFn = (event: {
41
75
  type: string;
@@ -65,4 +99,4 @@ declare function tag(...stateNames: StateRef<string, unknown>[]): Tag;
65
99
  declare function isIn(snapshot: Snapshot, stateRefName: string): boolean;
66
100
  declare function activeLeaves(snapshot: Snapshot): string[];
67
101
  //#endregion
68
- export { ActorMap, type EventLike, type SendableEvent, type SendableMap, activeLeaves, broadcast, events, isIn, matches, states, tag, withPromise, withTimeout };
102
+ export { ActorMap, type EventLike, type SendableEvent, type SendableMap, activeLeaves, broadcast, events, isIn, matches, onOutput, states, tag, withPromise, withTimeout };
package/dist/index.mjs CHANGED
@@ -21,29 +21,45 @@ function matches(actor, pattern) {
21
21
  }
22
22
  //#endregion
23
23
  //#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
+ */
24
28
  var ActorMap = class {
25
29
  #actors = /* @__PURE__ */ new Map();
26
- #parent;
27
- constructor(parent) {
28
- this.#parent = parent ?? null;
30
+ #reapers = /* @__PURE__ */ new Map();
31
+ #factory;
32
+ #autoReap;
33
+ constructor(factory, options = {}) {
34
+ this.#factory = factory;
35
+ this.#autoReap = options.autoReap === true;
29
36
  }
30
- spawn(key, factory) {
31
- if (this.#actors.has(key)) {
32
- console.warn(`[ActorMap] spawning over existing key "${key}". Old actor will be aborted.`);
33
- this.kill(key);
34
- }
35
- const child = factory();
36
- if (this.#parent) Either.match(setOutputHandler(child, (event) => {
37
- this.#parent.send(event);
38
- }), (err) => console.error(err.message), () => {});
37
+ spawn(key) {
38
+ if (this.#actors.has(key)) this.kill(key);
39
+ const child = this.#factory(key);
39
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);
51
+ }
40
52
  }
41
53
  send(key, event) {
42
54
  this.#actors.get(key)?.send(event);
43
55
  }
44
56
  kill(key) {
45
57
  const actor = this.#actors.get(key);
46
- if (actor) Either.match(abortEffects(actor), (err) => console.error(err.message), () => {});
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);
47
63
  this.#actors.delete(key);
48
64
  }
49
65
  get size() {
@@ -55,8 +71,8 @@ var ActorMap = class {
55
71
  keys() {
56
72
  return [...this.#actors.keys()];
57
73
  }
58
- ensure(key, factory) {
59
- if (!this.#actors.has(key)) this.spawn(key, factory);
74
+ ensure(key) {
75
+ if (!this.#actors.has(key)) this.spawn(key);
60
76
  }
61
77
  snapshot(key) {
62
78
  return this.#actors.get(key)?.snapshot();
@@ -64,9 +80,13 @@ var ActorMap = class {
64
80
  };
65
81
  //#endregion
66
82
  //#region src/state.ts
67
- function states(...names) {
83
+ function states(...entries) {
68
84
  const result = {};
69
- for (const name of names) result[name] = state(name)();
85
+ for (const entry of entries) {
86
+ const name = typeof entry === "string" ? entry : entry.name;
87
+ const ref = state(name)();
88
+ result[name] = typeof entry === "object" && entry.final ? ref.final() : ref;
89
+ }
70
90
  return result;
71
91
  }
72
92
  //#endregion
@@ -77,6 +97,18 @@ function events(...names) {
77
97
  return result;
78
98
  }
79
99
  //#endregion
100
+ //#region src/output.ts
101
+ /**
102
+ * Route an actor's emitted outputs to a handler.
103
+ *
104
+ * `regions` auto-wire child outputs into the parent. ActorMap children do
105
+ * 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.
107
+ */
108
+ function onOutput(actor, handler) {
109
+ setOutputHandler(actor, handler);
110
+ }
111
+ //#endregion
80
112
  //#region src/effects/promise.ts
81
113
  function withPromise(promise, signal, emit, events) {
82
114
  promise.then((data) => {
@@ -88,11 +120,10 @@ function withPromise(promise, signal, emit, events) {
88
120
  //#endregion
89
121
  //#region src/effects/timeout.ts
90
122
  function withTimeout(ms, input, event) {
91
- if (typeof ms !== "number" || ms < 0 || !Number.isFinite(ms)) console.warn(`[withTimeout] invalid ms value: ${ms}. Timeout may not fire.`);
92
123
  input.clock.setTimeout(ms, () => {
93
124
  if (input.signal.aborted) return;
94
125
  input.emit(event());
95
- });
126
+ }, { signal: input.signal });
96
127
  }
97
128
  //#endregion
98
129
  //#region src/transitions/broadcast.ts
@@ -119,4 +150,4 @@ function tag(...stateNames) {
119
150
  return { has: (snapshot) => names.some((name) => isIn(snapshot, name)) };
120
151
  }
121
152
  //#endregion
122
- export { ActorMap, activeLeaves, broadcast, events, isIn, matches, states, tag, withPromise, withTimeout };
153
+ export { ActorMap, activeLeaves, broadcast, events, isIn, matches, onOutput, states, tag, withPromise, withTimeout };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mantaq/sugar",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Userland sugar for mantaq actors — matches, effect helpers, transition utilities.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -20,8 +20,8 @@
20
20
  "access": "public"
21
21
  },
22
22
  "dependencies": {
23
- "@mantaq/core": "0.1.0",
24
- "@mantaq/utils": "0.0.2"
23
+ "@mantaq/utils": "0.0.2",
24
+ "@mantaq/core": "0.2.0"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@stryker-mutator/core": "^9",