@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 +10 -7
- package/dist/index.d.mts +82 -66
- package/dist/index.mjs +170 -115
- package/package.json +10 -10
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,48 +1,66 @@
|
|
|
1
1
|
import { Actor, ActorBuilder, ActorOptions, AnyActor, AnyEventRef, AnyStateRef, Clock, EffectInput, EventRef, InitialState, InternalEvent, Snapshot, StateRef } from "@mantaq/core";
|
|
2
|
-
|
|
3
|
-
declare function matches(actor: {
|
|
2
|
+
interface MatchTarget {
|
|
4
3
|
snapshot(): Snapshot;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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(...
|
|
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> = (
|
|
119
|
-
declare function definePart<S extends ActorSpec = never>(
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
declare function
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
3
|
-
function
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
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
|
-
*
|
|
24
|
-
*
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
74
|
+
function ensure(key) {
|
|
75
|
+
if (!actors.has(key)) spawn(key);
|
|
65
76
|
}
|
|
66
|
-
|
|
67
|
-
return
|
|
77
|
+
function has(key) {
|
|
78
|
+
return actors.has(key);
|
|
68
79
|
}
|
|
69
|
-
|
|
70
|
-
|
|
80
|
+
function keys() {
|
|
81
|
+
return [...actors.keys()];
|
|
71
82
|
}
|
|
72
|
-
|
|
73
|
-
return
|
|
83
|
+
function snapshotOf(key) {
|
|
84
|
+
return actors.get(key)?.snapshot();
|
|
74
85
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
//#region src/output.ts
|
|
131
|
+
// src/output.ts
|
|
96
132
|
/**
|
|
97
|
-
* Route an actor's emitted outputs to
|
|
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,
|
|
104
|
-
actor.on("output", handler);
|
|
140
|
+
function onOutput(actor, ...handlers) {
|
|
141
|
+
for (const handler of handlers) actor.on("output", { fn: handler });
|
|
105
142
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
}
|
|
148
|
+
}, (err) => {
|
|
112
149
|
if (!signal.aborted) emit(events.error(err));
|
|
113
150
|
});
|
|
114
151
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
input.clock.setTimeout(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
for (const
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
return fn;
|
|
200
|
+
// src/parts.ts
|
|
201
|
+
function definePart(part) {
|
|
202
|
+
return part;
|
|
151
203
|
}
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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: (
|
|
169
|
-
for (const part of
|
|
224
|
+
setup: (builder) => {
|
|
225
|
+
for (const part of parts) part(builder);
|
|
170
226
|
}
|
|
171
227
|
});
|
|
172
228
|
}
|
|
173
|
-
|
|
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.
|
|
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.
|
|
23
|
+
"@mantaq/core": "0.5.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@stryker-mutator/core": "^
|
|
27
|
-
"@stryker-mutator/vitest-runner": "^
|
|
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": "
|
|
31
|
-
"typescript": "
|
|
32
|
-
"vite": "npm:@voidzero-dev/vite-plus-core@
|
|
33
|
-
"vite-plus": "
|
|
34
|
-
"vitest": "
|
|
35
|
-
"@mantaq/pbt": "0.0.
|
|
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",
|