@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 +10 -7
- package/dist/index.d.mts +121 -34
- package/dist/index.mjs +176 -88
- package/package.json +4 -5
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
|
-
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
|
-
|
|
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,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
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
}
|
|
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;
|
|
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
|
-
|
|
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 {
|
|
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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
19
|
+
return matchRegion;
|
|
16
20
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
69
|
-
|
|
75
|
+
function ensure(key) {
|
|
76
|
+
if (!actors.has(key)) spawn(key);
|
|
70
77
|
}
|
|
71
|
-
|
|
72
|
-
return
|
|
78
|
+
function has(key) {
|
|
79
|
+
return actors.has(key);
|
|
73
80
|
}
|
|
74
|
-
|
|
75
|
-
|
|
81
|
+
function keys() {
|
|
82
|
+
return [...actors.keys()];
|
|
76
83
|
}
|
|
77
|
-
|
|
78
|
-
return
|
|
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
|
|
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,
|
|
109
|
-
|
|
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(
|
|
114
|
-
promise
|
|
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
|
-
}
|
|
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(
|
|
123
|
-
input
|
|
124
|
-
|
|
125
|
-
input.
|
|
126
|
-
|
|
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
|
-
|
|
131
|
-
|
|
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
|
|
136
|
-
if (snapshot.path[0] ===
|
|
137
|
-
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;
|
|
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(...
|
|
149
|
-
const names =
|
|
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
|
-
|
|
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
|
+
"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/
|
|
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": "
|
|
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.
|
|
35
|
+
"@mantaq/pbt": "0.0.3"
|
|
37
36
|
},
|
|
38
37
|
"scripts": {
|
|
39
38
|
"build": "vp pack",
|