@mantaq/sugar 0.4.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 +10 -7
- package/dist/index.d.mts +82 -39
- package/dist/index.mjs +159 -92
- package/package.json +4 -4
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,
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
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
|
-
|
|
3
|
+
interface MatchTarget {
|
|
4
4
|
snapshot(): Snapshot;
|
|
5
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,38 +87,48 @@ 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
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
+
};
|
|
82
117
|
};
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
payload?: unknown;
|
|
86
|
-
};
|
|
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>(
|
|
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(...
|
|
131
|
+
declare function tag(...stateRefs: StateRef<string, unknown>[]): Tag;
|
|
97
132
|
//#endregion
|
|
98
133
|
//#region src/parts.d.ts
|
|
99
134
|
interface ActorSpec {
|
|
@@ -115,10 +150,11 @@ type InternalOf<S extends ActorSpec> = "internal" extends keyof S ? [NotUndef<S[
|
|
|
115
150
|
type OutputsOf<S extends ActorSpec> = "outputs" extends keyof S ? [NotUndef<S["outputs"]>] extends [never] ? readonly [] : NotUndef<S["outputs"]> : readonly [];
|
|
116
151
|
type ContextOf<S extends ActorSpec> = "context" extends keyof S ? [NotUndef<S["context"]>] extends [never] ? Record<string, unknown> : NotUndef<S["context"]> : Record<string, unknown>;
|
|
117
152
|
type BuilderOf<S extends ActorSpec> = ActorBuilder<S["states"], S["inputs"], InternalOf<S>, OutputsOf<S>, ContextOf<S>>;
|
|
118
|
-
type Fragment<S extends ActorSpec> = (
|
|
119
|
-
declare function definePart<S extends ActorSpec = never>(
|
|
120
|
-
|
|
121
|
-
|
|
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;
|
|
122
158
|
type SpecValue<States extends readonly AnyStateRef[], Inputs extends readonly AnyEventRef[], Internal extends readonly AnyEventRef[], Outputs extends readonly AnyEventRef[], ActorContext, Initial> = {
|
|
123
159
|
states: States;
|
|
124
160
|
inputs: Inputs;
|
|
@@ -133,14 +169,21 @@ type SpecValue<States extends readonly AnyStateRef[], Inputs extends readonly An
|
|
|
133
169
|
/**
|
|
134
170
|
* Construct the actor's static spec — states, inputs, internal/output events,
|
|
135
171
|
* context, initial state, clock, regions and budget. State/event tuples and
|
|
136
|
-
* the initial ref narrow to literals
|
|
172
|
+
* the initial ref narrow to literals while the context stays mutable, so
|
|
137
173
|
* `definePart` handlers read and write it with no `as` casts.
|
|
138
174
|
*/
|
|
139
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>;
|
|
140
|
-
|
|
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>;
|
|
141
181
|
//#endregion
|
|
142
182
|
//#region src/snapshot.d.ts
|
|
143
|
-
|
|
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;
|
|
144
187
|
declare function activeLeaves(snapshot: Snapshot): string[];
|
|
145
188
|
//#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 };
|
|
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,78 +1,119 @@
|
|
|
1
1
|
import { Actor, event, state } from "@mantaq/core";
|
|
2
2
|
//#region src/actors/matches.ts
|
|
3
|
-
function
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
19
|
+
return matchRegion;
|
|
14
20
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
+
});
|
|
19
33
|
}
|
|
20
34
|
//#endregion
|
|
21
35
|
//#region src/actors/actor-map.ts
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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);
|
|
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;
|
|
49
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);
|
|
50
57
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
this.#reapers.get(key)?.();
|
|
57
|
-
this.#reapers.delete(key);
|
|
58
|
-
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);
|
|
59
63
|
}
|
|
60
|
-
|
|
61
|
-
|
|
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();
|
|
62
74
|
}
|
|
63
|
-
|
|
64
|
-
|
|
75
|
+
function ensure(key) {
|
|
76
|
+
if (!actors.has(key)) spawn(key);
|
|
65
77
|
}
|
|
66
|
-
|
|
67
|
-
return
|
|
78
|
+
function has(key) {
|
|
79
|
+
return actors.has(key);
|
|
68
80
|
}
|
|
69
|
-
|
|
70
|
-
|
|
81
|
+
function keys() {
|
|
82
|
+
return [...actors.keys()];
|
|
71
83
|
}
|
|
72
|
-
|
|
73
|
-
return
|
|
84
|
+
function snapshotOf(key) {
|
|
85
|
+
return actors.get(key)?.snapshot();
|
|
74
86
|
}
|
|
75
|
-
|
|
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
|
+
}
|
|
76
117
|
//#endregion
|
|
77
118
|
//#region src/state.ts
|
|
78
119
|
function states(...entries) {
|
|
@@ -82,56 +123,78 @@ function states(...entries) {
|
|
|
82
123
|
const ref = state(name)();
|
|
83
124
|
result[name] = typeof entry === "object" && entry.final ? ref.final() : ref;
|
|
84
125
|
}
|
|
85
|
-
return result;
|
|
126
|
+
return parseStateRefs(result);
|
|
86
127
|
}
|
|
87
128
|
//#endregion
|
|
88
129
|
//#region src/event.ts
|
|
89
130
|
function events(...names) {
|
|
90
131
|
const result = {};
|
|
91
132
|
for (const name of names) result[name] = event(name)();
|
|
92
|
-
return result;
|
|
133
|
+
return parseEventRefs(result);
|
|
93
134
|
}
|
|
94
135
|
//#endregion
|
|
95
136
|
//#region src/output.ts
|
|
96
137
|
/**
|
|
97
|
-
* Route an actor's emitted outputs to
|
|
138
|
+
* Route an actor's emitted outputs to one or more handlers.
|
|
98
139
|
*
|
|
99
140
|
* `regions` auto-wire child outputs into the parent. ActorMap children do
|
|
100
141
|
* 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
|
|
142
|
+
* factory connects a child's declared outputs to a receiver — pass several
|
|
143
|
+
* handlers to feed multiple receivers.
|
|
102
144
|
*/
|
|
103
|
-
function onOutput(actor,
|
|
104
|
-
actor.on("output", handler);
|
|
145
|
+
function onOutput(actor, ...handlers) {
|
|
146
|
+
for (const handler of handlers) actor.on("output", { fn: handler });
|
|
105
147
|
}
|
|
106
148
|
//#endregion
|
|
107
149
|
//#region src/effects/promise.ts
|
|
108
|
-
function withPromise(
|
|
109
|
-
promise
|
|
150
|
+
function withPromise(options) {
|
|
151
|
+
const { promise, signal, emit, events } = options;
|
|
152
|
+
return promise.then((data) => {
|
|
110
153
|
if (!signal.aborted) emit(events.success(data));
|
|
111
|
-
}
|
|
154
|
+
}, (err) => {
|
|
112
155
|
if (!signal.aborted) emit(events.error(err));
|
|
113
156
|
});
|
|
114
157
|
}
|
|
115
158
|
//#endregion
|
|
116
159
|
//#region src/effects/timeout.ts
|
|
117
|
-
function withTimeout(
|
|
118
|
-
input
|
|
119
|
-
|
|
120
|
-
input.
|
|
121
|
-
|
|
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
|
+
});
|
|
122
169
|
}
|
|
123
170
|
//#endregion
|
|
124
171
|
//#region src/transitions/broadcast.ts
|
|
125
|
-
|
|
126
|
-
|
|
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);
|
|
127
178
|
}
|
|
128
179
|
//#endregion
|
|
129
180
|
//#region src/snapshot.ts
|
|
130
|
-
function
|
|
131
|
-
if (snapshot.path[0] ===
|
|
132
|
-
for (const region of Object.values(snapshot.regions)) if (
|
|
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;
|
|
133
187
|
return false;
|
|
134
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
|
+
}
|
|
135
198
|
function activeLeaves(snapshot) {
|
|
136
199
|
if (Object.keys(snapshot.regions).length === 0) return [snapshot.path.join(".")];
|
|
137
200
|
const leaves = [];
|
|
@@ -140,35 +203,39 @@ function activeLeaves(snapshot) {
|
|
|
140
203
|
}
|
|
141
204
|
//#endregion
|
|
142
205
|
//#region src/tags.ts
|
|
143
|
-
function tag(...
|
|
144
|
-
const names =
|
|
206
|
+
function tag(...stateRefs) {
|
|
207
|
+
const names = stateRefs.map((ref) => ref.name);
|
|
145
208
|
return { has: (snapshot) => names.some((name) => isIn(snapshot, name)) };
|
|
146
209
|
}
|
|
147
210
|
//#endregion
|
|
148
211
|
//#region src/parts.ts
|
|
149
|
-
function definePart(
|
|
150
|
-
return
|
|
212
|
+
function definePart(part) {
|
|
213
|
+
return part;
|
|
151
214
|
}
|
|
152
|
-
|
|
153
|
-
|
|
215
|
+
/** Apply one or more parts to a builder, in order. */
|
|
216
|
+
function use(builder, ...parts) {
|
|
217
|
+
for (const part of parts) part(builder);
|
|
154
218
|
}
|
|
155
219
|
/**
|
|
156
220
|
* Construct the actor's static spec — states, inputs, internal/output events,
|
|
157
221
|
* context, initial state, clock, regions and budget. State/event tuples and
|
|
158
|
-
* the initial ref narrow to literals
|
|
222
|
+
* the initial ref narrow to literals while the context stays mutable, so
|
|
159
223
|
* `definePart` handlers read and write it with no `as` casts.
|
|
160
224
|
*/
|
|
161
225
|
function actorSpec(spec) {
|
|
162
226
|
return spec;
|
|
163
227
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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({
|
|
167
234
|
...base,
|
|
168
|
-
setup: (
|
|
169
|
-
for (const part of
|
|
235
|
+
setup: (builder) => {
|
|
236
|
+
for (const part of parts) part(builder);
|
|
170
237
|
}
|
|
171
238
|
});
|
|
172
239
|
}
|
|
173
240
|
//#endregion
|
|
174
|
-
export {
|
|
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
|
+
"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,7 +20,7 @@
|
|
|
20
20
|
"access": "public"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@mantaq/core": "0.
|
|
23
|
+
"@mantaq/core": "0.4.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@stryker-mutator/core": "^9",
|
|
@@ -28,11 +28,11 @@
|
|
|
28
28
|
"@types/node": "^26.2.0",
|
|
29
29
|
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
|
30
30
|
"@vitest/coverage-v8": "4.1.10",
|
|
31
|
-
"typescript": "
|
|
31
|
+
"typescript": "6.0.3",
|
|
32
32
|
"vite": "npm:@voidzero-dev/vite-plus-core@0.2.8",
|
|
33
33
|
"vite-plus": "0.2.8",
|
|
34
34
|
"vitest": "4.1.10",
|
|
35
|
-
"@mantaq/pbt": "0.0.
|
|
35
|
+
"@mantaq/pbt": "0.0.3"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"build": "vp pack",
|