effect-machine 0.8.0 → 0.10.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 +76 -16
- package/dist/_virtual/_rolldown/runtime.js +6 -11
- package/dist/actor.d.ts +58 -72
- package/dist/actor.js +166 -32
- package/dist/cluster/entity-machine.d.ts +0 -1
- package/dist/cluster/entity-machine.js +6 -6
- package/dist/cluster/index.js +1 -2
- package/dist/cluster/to-entity.js +1 -3
- package/dist/errors.d.ts +12 -1
- package/dist/errors.js +8 -3
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -3
- package/dist/inspection.js +1 -3
- package/dist/internal/inspection.js +1 -3
- package/dist/internal/transition.d.ts +26 -2
- package/dist/internal/transition.js +37 -10
- package/dist/internal/utils.d.ts +7 -2
- package/dist/internal/utils.js +1 -3
- package/dist/machine.d.ts +66 -4
- package/dist/machine.js +67 -31
- package/dist/persistence/adapter.js +1 -3
- package/dist/persistence/adapters/in-memory.js +1 -3
- package/dist/persistence/index.js +1 -2
- package/dist/persistence/persistent-actor.js +54 -19
- package/dist/persistence/persistent-machine.js +1 -3
- package/dist/schema.js +1 -3
- package/dist/slot.js +1 -3
- package/dist/testing.js +58 -6
- package/package.json +19 -18
- package/v3/dist/_virtual/_rolldown/runtime.js +13 -0
- package/{dist-v3 → v3/dist}/actor.d.ts +65 -78
- package/{dist-v3 → v3/dist}/actor.js +173 -37
- package/{dist-v3 → v3/dist}/cluster/entity-machine.d.ts +1 -2
- package/{dist-v3 → v3/dist}/cluster/entity-machine.js +9 -9
- package/{dist-v3 → v3/dist}/cluster/index.js +1 -2
- package/{dist-v3 → v3/dist}/cluster/to-entity.d.ts +1 -1
- package/{dist-v3 → v3/dist}/cluster/to-entity.js +2 -4
- package/v3/dist/errors.d.ts +76 -0
- package/{dist-v3 → v3/dist}/errors.js +9 -4
- package/v3/dist/index.d.ts +13 -0
- package/v3/dist/index.js +13 -0
- package/{dist-v3 → v3/dist}/inspection.d.ts +53 -8
- package/v3/dist/inspection.js +156 -0
- package/{dist-v3 → v3/dist}/internal/brands.d.ts +1 -1
- package/{dist-v3 → v3/dist}/internal/inspection.d.ts +1 -1
- package/v3/dist/internal/inspection.js +20 -0
- package/{dist-v3 → v3/dist}/internal/transition.d.ts +35 -11
- package/{dist-v3 → v3/dist}/internal/transition.js +47 -17
- package/{dist-v3 → v3/dist}/internal/utils.d.ts +9 -4
- package/{dist-v3 → v3/dist}/internal/utils.js +2 -4
- package/{dist-v3 → v3/dist}/machine.d.ts +86 -10
- package/{dist-v3 → v3/dist}/machine.js +130 -33
- package/{dist-v3 → v3/dist}/persistence/adapter.d.ts +18 -5
- package/{dist-v3 → v3/dist}/persistence/adapter.js +2 -4
- package/{dist-v3 → v3/dist}/persistence/adapters/in-memory.d.ts +1 -1
- package/{dist-v3 → v3/dist}/persistence/adapters/in-memory.js +2 -4
- package/{dist-v3 → v3/dist}/persistence/index.js +1 -2
- package/{dist-v3 → v3/dist}/persistence/persistent-actor.d.ts +7 -6
- package/{dist-v3 → v3/dist}/persistence/persistent-actor.js +59 -22
- package/{dist-v3 → v3/dist}/persistence/persistent-machine.d.ts +1 -1
- package/{dist-v3 → v3/dist}/persistence/persistent-machine.js +2 -4
- package/{dist-v3 → v3/dist}/schema.d.ts +1 -1
- package/{dist-v3 → v3/dist}/schema.js +6 -5
- package/{dist-v3 → v3/dist}/slot.d.ts +4 -3
- package/{dist-v3 → v3/dist}/slot.js +2 -4
- package/{dist-v3 → v3/dist}/testing.d.ts +14 -8
- package/{dist-v3 → v3/dist}/testing.js +61 -9
- package/dist-v3/_virtual/_rolldown/runtime.js +0 -18
- package/dist-v3/errors.d.ts +0 -27
- package/dist-v3/index.d.ts +0 -13
- package/dist-v3/index.js +0 -14
- package/dist-v3/inspection.js +0 -50
- package/dist-v3/internal/inspection.js +0 -15
- /package/{dist-v3 → v3/dist}/cluster/index.d.ts +0 -0
- /package/{dist-v3 → v3/dist}/internal/brands.js +0 -0
- /package/{dist-v3 → v3/dist}/persistence/index.d.ts +0 -0
package/README.md
CHANGED
|
@@ -74,8 +74,12 @@ const orderMachine = Machine.make({
|
|
|
74
74
|
const program = Effect.gen(function* () {
|
|
75
75
|
const actor = yield* Machine.spawn(orderMachine);
|
|
76
76
|
|
|
77
|
+
// fire-and-forget
|
|
77
78
|
yield* actor.send(OrderEvent.Process);
|
|
78
|
-
|
|
79
|
+
|
|
80
|
+
// request-reply — get ProcessEventResult back
|
|
81
|
+
const result = yield* actor.call(OrderEvent.Ship({ trackingId: "TRACK-123" }));
|
|
82
|
+
console.log(result.transitioned); // true
|
|
79
83
|
|
|
80
84
|
const state = yield* actor.waitFor(OrderState.Shipped);
|
|
81
85
|
console.log(state); // Shipped { orderId: "order-1", trackingId: "TRACK-123" }
|
|
@@ -183,6 +187,53 @@ machine.task(State.Loading, ({ effects, state }) => effects.fetchData({ url: sta
|
|
|
183
187
|
});
|
|
184
188
|
```
|
|
185
189
|
|
|
190
|
+
### State Timeouts
|
|
191
|
+
|
|
192
|
+
`.timeout()` — gen_statem-style state timeouts. Timer starts on state entry, cancels on exit:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
machine
|
|
196
|
+
.timeout(State.Loading, {
|
|
197
|
+
duration: Duration.seconds(30),
|
|
198
|
+
event: Event.Timeout,
|
|
199
|
+
})
|
|
200
|
+
// Dynamic duration from state
|
|
201
|
+
.timeout(State.Retrying, {
|
|
202
|
+
duration: (state) => Duration.seconds(state.backoff),
|
|
203
|
+
event: Event.GiveUp,
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`.reenter()` restarts the timer with fresh state values.
|
|
208
|
+
|
|
209
|
+
### Event Postpone
|
|
210
|
+
|
|
211
|
+
`.postpone()` — gen_statem-style event postpone. When a matching event arrives in the given state, it is buffered. After the next state transition (tag change), buffered events drain in FIFO order:
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
machine
|
|
215
|
+
.postpone(State.Connecting, Event.Data) // single event
|
|
216
|
+
.postpone(State.Connecting, [Event.Data, Event.Cmd]); // multiple events
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Reply-bearing events (`call`/`ask`) in the postpone buffer are settled with `ActorStoppedError` on stop/interrupt/final-state.
|
|
220
|
+
|
|
221
|
+
### ask / reply
|
|
222
|
+
|
|
223
|
+
Handlers can return a domain reply via `{ state, reply }`:
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
.on(State.Active, Event.GetCount, ({ state }) => ({
|
|
227
|
+
state, // stay in same state
|
|
228
|
+
reply: state.count, // domain value returned to caller
|
|
229
|
+
}))
|
|
230
|
+
|
|
231
|
+
// Caller side:
|
|
232
|
+
const count = yield* actor.ask<number>(Event.GetCount);
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
`ask` fails with `NoReplyError` if the handler doesn't provide a reply, and `ActorStoppedError` if the actor stops while the request is pending.
|
|
236
|
+
|
|
186
237
|
### Child Actors
|
|
187
238
|
|
|
188
239
|
Spawn children from `.spawn()` handlers with `self.spawn`. Children are state-scoped — auto-stopped on state exit:
|
|
@@ -253,7 +304,7 @@ See the [primer](./primer/) for comprehensive documentation:
|
|
|
253
304
|
| ----------- | ----------------------------------------- | ------------------------------ |
|
|
254
305
|
| Overview | [index.md](./primer/index.md) | Navigation and quick reference |
|
|
255
306
|
| Basics | [basics.md](./primer/basics.md) | Core concepts |
|
|
256
|
-
| Handlers | [handlers.md](./primer/handlers.md) | Transitions
|
|
307
|
+
| Handlers | [handlers.md](./primer/handlers.md) | Transitions, guards, reply |
|
|
257
308
|
| Effects | [effects.md](./primer/effects.md) | spawn, background, timeouts |
|
|
258
309
|
| Testing | [testing.md](./primer/testing.md) | simulate, harness, assertions |
|
|
259
310
|
| Actors | [actors.md](./primer/actors.md) | ActorSystem, ActorRef |
|
|
@@ -273,6 +324,8 @@ See the [primer](./primer/) for comprehensive documentation:
|
|
|
273
324
|
| `.reenter(State.X, Event.Y, handler)` | Force re-entry on same state |
|
|
274
325
|
| `.spawn(State.X, handler)` | State-scoped effect |
|
|
275
326
|
| `.task(State.X, run, { onSuccess })` | State-scoped task |
|
|
327
|
+
| `.timeout(State.X, { duration, event })` | State timeout (gen_statem) |
|
|
328
|
+
| `.postpone(State.X, Event.Y)` | Postpone event in state (gen_statem) |
|
|
276
329
|
| `.background(handler)` | Machine-lifetime effect |
|
|
277
330
|
| `.final(State.X)` | Mark final state |
|
|
278
331
|
| `.build({ slot: impl })` | Provide implementations, returns `BuiltMachine` (terminal) |
|
|
@@ -308,20 +361,27 @@ See the [primer](./primer/) for comprehensive documentation:
|
|
|
308
361
|
|
|
309
362
|
### Actor
|
|
310
363
|
|
|
311
|
-
| Method | Description
|
|
312
|
-
| -------------------------------- |
|
|
313
|
-
| `actor.send(event)` |
|
|
314
|
-
| `actor.
|
|
315
|
-
| `actor.
|
|
316
|
-
| `actor.
|
|
317
|
-
| `actor.
|
|
318
|
-
| `actor.
|
|
319
|
-
| `actor.
|
|
320
|
-
| `actor.
|
|
321
|
-
| `actor.
|
|
322
|
-
| `actor.
|
|
323
|
-
| `actor.
|
|
324
|
-
| `actor.
|
|
364
|
+
| Method | Description |
|
|
365
|
+
| -------------------------------- | ------------------------------------------- |
|
|
366
|
+
| `actor.send(event)` | Fire-and-forget (queue event) |
|
|
367
|
+
| `actor.cast(event)` | Alias for send (OTP gen_server:cast) |
|
|
368
|
+
| `actor.call(event)` | Request-reply, returns `ProcessEventResult` |
|
|
369
|
+
| `actor.ask<R>(event)` | Typed domain reply from handler |
|
|
370
|
+
| `actor.snapshot` | Get current state |
|
|
371
|
+
| `actor.matches(tag)` | Check state tag |
|
|
372
|
+
| `actor.can(event)` | Can handle event? |
|
|
373
|
+
| `actor.changes` | Stream of changes |
|
|
374
|
+
| `actor.waitFor(State.X)` | Wait for state (constructor or fn) |
|
|
375
|
+
| `actor.awaitFinal` | Wait final state |
|
|
376
|
+
| `actor.sendAndWait(ev, State.X)` | Send + wait for state |
|
|
377
|
+
| `actor.subscribe(fn)` | Sync callback |
|
|
378
|
+
| `actor.sync.send(event)` | Sync fire-and-forget (for UI) |
|
|
379
|
+
| `actor.sync.stop()` | Sync stop |
|
|
380
|
+
| `actor.sync.snapshot()` | Sync get state |
|
|
381
|
+
| `actor.sync.matches(tag)` | Sync check state tag |
|
|
382
|
+
| `actor.sync.can(event)` | Sync can handle event? |
|
|
383
|
+
| `actor.system` | Access the actor's `ActorSystem` |
|
|
384
|
+
| `actor.children` | Child actors (`ReadonlyMap`) |
|
|
325
385
|
|
|
326
386
|
### ActorSystem
|
|
327
387
|
|
|
@@ -2,17 +2,12 @@
|
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
3
|
var __exportAll = (all, no_symbols) => {
|
|
4
4
|
let target = {};
|
|
5
|
-
for (var name in all) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
if (!no_symbols) {
|
|
12
|
-
__defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
13
|
-
}
|
|
5
|
+
for (var name in all) __defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true
|
|
8
|
+
});
|
|
9
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
14
10
|
return target;
|
|
15
11
|
};
|
|
16
|
-
|
|
17
12
|
//#endregion
|
|
18
|
-
export { __exportAll };
|
|
13
|
+
export { __exportAll };
|
package/dist/actor.d.ts
CHANGED
|
@@ -1,88 +1,85 @@
|
|
|
1
1
|
import { EffectsDef, GuardsDef, MachineContext } from "./slot.js";
|
|
2
2
|
import { PersistentMachine } from "./persistence/persistent-machine.js";
|
|
3
|
-
import { DuplicateActorError } from "./errors.js";
|
|
3
|
+
import { ActorStoppedError, DuplicateActorError, NoReplyError } from "./errors.js";
|
|
4
4
|
import { ProcessEventError, ProcessEventHooks, ProcessEventResult, processEventCore, resolveTransition, runSpawnEffects } from "./internal/transition.js";
|
|
5
5
|
import { PersistentActorRef } from "./persistence/persistent-actor.js";
|
|
6
6
|
import { ActorMetadata, PersistenceAdapterTag, PersistenceError, RestoreResult, VersionConflictError } from "./persistence/adapter.js";
|
|
7
7
|
import { BuiltMachine, Machine, MachineRef } from "./machine.js";
|
|
8
|
-
import { Effect, Layer, Option, Queue, Ref, Scope, ServiceMap, Stream, SubscriptionRef } from "effect";
|
|
8
|
+
import { Deferred, Effect, Layer, Option, Queue, Ref, Scope, ServiceMap, Stream, SubscriptionRef } from "effect";
|
|
9
9
|
import * as effect_Tracer0 from "effect/Tracer";
|
|
10
10
|
|
|
11
11
|
//#region src/actor.d.ts
|
|
12
|
+
/** Discriminated mailbox request */
|
|
13
|
+
type QueuedEvent<E> = {
|
|
14
|
+
readonly _tag: "send";
|
|
15
|
+
readonly event: E;
|
|
16
|
+
} | {
|
|
17
|
+
readonly _tag: "call";
|
|
18
|
+
readonly event: E;
|
|
19
|
+
readonly reply: Deferred.Deferred<ProcessEventResult<{
|
|
20
|
+
readonly _tag: string;
|
|
21
|
+
}>, ActorStoppedError>;
|
|
22
|
+
} | {
|
|
23
|
+
readonly _tag: "ask";
|
|
24
|
+
readonly event: E;
|
|
25
|
+
readonly reply: Deferred.Deferred<unknown, NoReplyError | ActorStoppedError>;
|
|
26
|
+
};
|
|
12
27
|
/**
|
|
13
28
|
* Reference to a running actor.
|
|
14
29
|
*/
|
|
30
|
+
/**
|
|
31
|
+
* Sync projection of ActorRef for non-Effect boundaries (React hooks, framework callbacks).
|
|
32
|
+
*/
|
|
33
|
+
interface ActorRefSync<State extends {
|
|
34
|
+
readonly _tag: string;
|
|
35
|
+
}, Event> {
|
|
36
|
+
readonly send: (event: Event) => void;
|
|
37
|
+
readonly stop: () => void;
|
|
38
|
+
readonly snapshot: () => State;
|
|
39
|
+
readonly matches: (tag: State["_tag"]) => boolean;
|
|
40
|
+
readonly can: (event: Event) => boolean;
|
|
41
|
+
}
|
|
15
42
|
interface ActorRef<State extends {
|
|
16
43
|
readonly _tag: string;
|
|
17
44
|
}, Event> {
|
|
18
|
-
/**
|
|
19
|
-
* Unique identifier for this actor
|
|
20
|
-
*/
|
|
21
45
|
readonly id: string;
|
|
22
|
-
/**
|
|
23
|
-
* Send an event to the actor
|
|
24
|
-
*/
|
|
46
|
+
/** Send an event (fire-and-forget). */
|
|
25
47
|
readonly send: (event: Event) => Effect.Effect<void>;
|
|
48
|
+
/** Fire-and-forget alias for send (OTP gen_server:cast). */
|
|
49
|
+
readonly cast: (event: Event) => Effect.Effect<void>;
|
|
26
50
|
/**
|
|
27
|
-
*
|
|
51
|
+
* Serialized request-reply (OTP gen_server:call).
|
|
52
|
+
* Event is processed through the queue; caller gets ProcessEventResult back.
|
|
28
53
|
*/
|
|
29
|
-
readonly
|
|
54
|
+
readonly call: (event: Event) => Effect.Effect<ProcessEventResult<State>>;
|
|
30
55
|
/**
|
|
31
|
-
*
|
|
56
|
+
* Typed request-reply. Event is processed through the queue; caller gets
|
|
57
|
+
* the domain value returned by the handler's `reply` field.
|
|
58
|
+
* Fails with NoReplyError if the handler doesn't provide a reply.
|
|
32
59
|
*/
|
|
60
|
+
readonly ask: <R>(event: Event) => Effect.Effect<R, NoReplyError | ActorStoppedError>;
|
|
61
|
+
/** Observable state. */
|
|
62
|
+
readonly state: SubscriptionRef.SubscriptionRef<State>;
|
|
63
|
+
/** Stop the actor gracefully. */
|
|
33
64
|
readonly stop: Effect.Effect<void>;
|
|
34
|
-
/**
|
|
35
|
-
* Stop the actor (fire-and-forget).
|
|
36
|
-
* Signals graceful shutdown without waiting for completion.
|
|
37
|
-
* Use when stopping from sync contexts (e.g. framework cleanup hooks).
|
|
38
|
-
*/
|
|
39
|
-
readonly stopSync: () => void;
|
|
40
|
-
/**
|
|
41
|
-
* Get current state snapshot (Effect)
|
|
42
|
-
*/
|
|
65
|
+
/** Get current state snapshot. */
|
|
43
66
|
readonly snapshot: Effect.Effect<State>;
|
|
44
|
-
/**
|
|
45
|
-
* Get current state snapshot (sync)
|
|
46
|
-
*/
|
|
47
|
-
readonly snapshotSync: () => State;
|
|
48
|
-
/**
|
|
49
|
-
* Check if current state matches tag (Effect)
|
|
50
|
-
*/
|
|
67
|
+
/** Check if current state matches tag. */
|
|
51
68
|
readonly matches: (tag: State["_tag"]) => Effect.Effect<boolean>;
|
|
52
|
-
/**
|
|
53
|
-
* Check if current state matches tag (sync)
|
|
54
|
-
*/
|
|
55
|
-
readonly matchesSync: (tag: State["_tag"]) => boolean;
|
|
56
|
-
/**
|
|
57
|
-
* Check if event can be handled in current state (Effect)
|
|
58
|
-
*/
|
|
69
|
+
/** Check if event can be handled in current state. */
|
|
59
70
|
readonly can: (event: Event) => Effect.Effect<boolean>;
|
|
60
|
-
/**
|
|
61
|
-
* Check if event can be handled in current state (sync)
|
|
62
|
-
*/
|
|
63
|
-
readonly canSync: (event: Event) => boolean;
|
|
64
|
-
/**
|
|
65
|
-
* Stream of state changes
|
|
66
|
-
*/
|
|
71
|
+
/** Stream of state changes. */
|
|
67
72
|
readonly changes: Stream.Stream<State>;
|
|
68
|
-
/**
|
|
69
|
-
* Wait for a state that matches predicate or state variant (includes current snapshot).
|
|
70
|
-
* Accepts a predicate function or a state constructor/value (e.g. `State.Active`).
|
|
71
|
-
*/
|
|
73
|
+
/** Wait for a state matching predicate or variant (includes current snapshot). */
|
|
72
74
|
readonly waitFor: {
|
|
73
75
|
(predicate: (state: State) => boolean): Effect.Effect<State>;
|
|
74
76
|
(state: {
|
|
75
77
|
readonly _tag: State["_tag"];
|
|
76
78
|
}): Effect.Effect<State>;
|
|
77
79
|
};
|
|
78
|
-
/**
|
|
79
|
-
* Wait for a final state (includes current snapshot)
|
|
80
|
-
*/
|
|
80
|
+
/** Wait for a final state (includes current snapshot). */
|
|
81
81
|
readonly awaitFinal: Effect.Effect<State>;
|
|
82
|
-
/**
|
|
83
|
-
* Send event and wait for predicate, state variant, or final state.
|
|
84
|
-
* Accepts a predicate function or a state constructor/value (e.g. `State.Active`).
|
|
85
|
-
*/
|
|
82
|
+
/** Send event and wait for predicate, state variant, or final state. */
|
|
86
83
|
readonly sendAndWait: {
|
|
87
84
|
(event: Event, predicate: (state: State) => boolean): Effect.Effect<State>;
|
|
88
85
|
(event: Event, state: {
|
|
@@ -90,26 +87,13 @@ interface ActorRef<State extends {
|
|
|
90
87
|
}): Effect.Effect<State>;
|
|
91
88
|
(event: Event): Effect.Effect<State>;
|
|
92
89
|
};
|
|
93
|
-
/**
|
|
94
|
-
* Send event synchronously (fire-and-forget).
|
|
95
|
-
* No-op on stopped actors. Use when you need to send from sync contexts
|
|
96
|
-
* (e.g. framework hooks, event handlers).
|
|
97
|
-
*/
|
|
98
|
-
readonly sendSync: (event: Event) => void;
|
|
99
|
-
/**
|
|
100
|
-
* Subscribe to state changes (sync callback)
|
|
101
|
-
* Returns unsubscribe function
|
|
102
|
-
*/
|
|
90
|
+
/** Subscribe to state changes (sync callback). Returns unsubscribe function. */
|
|
103
91
|
readonly subscribe: (fn: (state: State) => void) => () => void;
|
|
104
|
-
/**
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
*/
|
|
92
|
+
/** Sync helpers for non-Effect boundaries. */
|
|
93
|
+
readonly sync: ActorRefSync<State, Event>;
|
|
94
|
+
/** The actor system this actor belongs to. */
|
|
108
95
|
readonly system: ActorSystem;
|
|
109
|
-
/**
|
|
110
|
-
* Child actors spawned via `self.spawn` in this actor's handlers.
|
|
111
|
-
* State-scoped children are auto-removed on state exit.
|
|
112
|
-
*/
|
|
96
|
+
/** Child actors spawned via `self.spawn` in this actor's handlers. */
|
|
113
97
|
readonly children: ReadonlyMap<string, ActorRef<AnyState, unknown>>;
|
|
114
98
|
}
|
|
115
99
|
/** Base type for stored actors (internal) */
|
|
@@ -275,7 +259,7 @@ declare const buildActorRefCore: <S extends {
|
|
|
275
259
|
readonly _tag: string;
|
|
276
260
|
}, E extends {
|
|
277
261
|
readonly _tag: string;
|
|
278
|
-
}, R, GD extends GuardsDef, EFD extends EffectsDef>(id: string, machine: Machine<S, E, R, any, any, GD, EFD>, stateRef: SubscriptionRef.SubscriptionRef<S>, eventQueue: Queue.Queue<E
|
|
262
|
+
}, R, GD extends GuardsDef, EFD extends EffectsDef>(id: string, machine: Machine<S, E, R, any, any, GD, EFD>, stateRef: SubscriptionRef.SubscriptionRef<S>, eventQueue: Queue.Queue<QueuedEvent<E>>, stoppedRef: Ref.Ref<boolean>, listeners: Listeners<S>, stop: Effect.Effect<void>, system: ActorSystem, childrenMap: ReadonlyMap<string, ActorRef<AnyState, unknown>>, pendingReplies: Set<Deferred.Deferred<unknown, unknown>>) => ActorRef<S, E>;
|
|
279
263
|
/**
|
|
280
264
|
* Create and start an actor for a machine
|
|
281
265
|
*/
|
|
@@ -284,9 +268,11 @@ declare const createActor: <S extends {
|
|
|
284
268
|
}, E extends {
|
|
285
269
|
readonly _tag: string;
|
|
286
270
|
}, R, GD extends GuardsDef, EFD extends EffectsDef>(id: string, machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>) => Effect.Effect<ActorRef<S, E>, never, Exclude<R, MachineContext<S, E, MachineRef<E>>> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, effect_Tracer0.ParentSpan> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope> | Exclude<Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>, effect_Tracer0.ParentSpan>>;
|
|
271
|
+
/** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
|
|
272
|
+
declare const settlePendingReplies: (pendingReplies: Set<Deferred.Deferred<unknown, unknown>>, actorId: string) => Effect.Effect<void, never, never>;
|
|
287
273
|
/**
|
|
288
274
|
* Default ActorSystem layer
|
|
289
275
|
*/
|
|
290
276
|
declare const Default: Layer.Layer<ActorSystem, never, never>;
|
|
291
277
|
//#endregion
|
|
292
|
-
export { ActorRef, ActorSystem, Default, Listeners, type ProcessEventError, type ProcessEventHooks, type ProcessEventResult, SystemEvent, SystemEventListener, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects };
|
|
278
|
+
export { ActorRef, ActorRefSync, ActorSystem, Default, Listeners, type ProcessEventError, type ProcessEventHooks, type ProcessEventResult, QueuedEvent, SystemEvent, SystemEventListener, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };
|
package/dist/actor.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { Inspector } from "./inspection.js";
|
|
2
2
|
import { INTERNAL_INIT_EVENT } from "./internal/utils.js";
|
|
3
|
-
import { DuplicateActorError } from "./errors.js";
|
|
3
|
+
import { ActorStoppedError, DuplicateActorError, NoReplyError } from "./errors.js";
|
|
4
4
|
import { isPersistentMachine } from "./persistence/persistent-machine.js";
|
|
5
5
|
import { emitWithTimestamp } from "./internal/inspection.js";
|
|
6
|
-
import { processEventCore, resolveTransition, runSpawnEffects } from "./internal/transition.js";
|
|
6
|
+
import { processEventCore, resolveTransition, runSpawnEffects, shouldPostpone } from "./internal/transition.js";
|
|
7
7
|
import { PersistenceAdapterTag, PersistenceError } from "./persistence/adapter.js";
|
|
8
8
|
import { createPersistentActor, restorePersistentActor } from "./persistence/persistent-actor.js";
|
|
9
9
|
import { Cause, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, PubSub, Queue, Ref, Scope, Semaphore, ServiceMap, Stream, SubscriptionRef } from "effect";
|
|
10
|
-
|
|
11
10
|
//#region src/actor.ts
|
|
12
11
|
/**
|
|
13
12
|
* Actor system: spawning, lifecycle, and event processing.
|
|
@@ -32,10 +31,50 @@ const notifyListeners = (listeners, state) => {
|
|
|
32
31
|
/**
|
|
33
32
|
* Build core ActorRef methods shared between regular and persistent actors.
|
|
34
33
|
*/
|
|
35
|
-
const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listeners, stop, system, childrenMap) => {
|
|
34
|
+
const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listeners, stop, system, childrenMap, pendingReplies) => {
|
|
36
35
|
const send = Effect.fn("effect-machine.actor.send")(function* (event) {
|
|
37
36
|
if (yield* Ref.get(stoppedRef)) return;
|
|
38
|
-
yield* Queue.offer(eventQueue,
|
|
37
|
+
yield* Queue.offer(eventQueue, {
|
|
38
|
+
_tag: "send",
|
|
39
|
+
event
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
const call = Effect.fn("effect-machine.actor.call")(function* (event) {
|
|
43
|
+
if (yield* Ref.get(stoppedRef)) {
|
|
44
|
+
const currentState = yield* SubscriptionRef.get(stateRef);
|
|
45
|
+
return {
|
|
46
|
+
newState: currentState,
|
|
47
|
+
previousState: currentState,
|
|
48
|
+
transitioned: false,
|
|
49
|
+
lifecycleRan: false,
|
|
50
|
+
isFinal: machine.finalStates.has(currentState._tag)
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const reply = yield* Deferred.make();
|
|
54
|
+
pendingReplies.add(reply);
|
|
55
|
+
yield* Queue.offer(eventQueue, {
|
|
56
|
+
_tag: "call",
|
|
57
|
+
event,
|
|
58
|
+
reply
|
|
59
|
+
});
|
|
60
|
+
return yield* Deferred.await(reply).pipe(Effect.ensuring(Effect.sync(() => pendingReplies.delete(reply))), Effect.catchTag("ActorStoppedError", () => SubscriptionRef.get(stateRef).pipe(Effect.map((currentState) => ({
|
|
61
|
+
newState: currentState,
|
|
62
|
+
previousState: currentState,
|
|
63
|
+
transitioned: false,
|
|
64
|
+
lifecycleRan: false,
|
|
65
|
+
isFinal: machine.finalStates.has(currentState._tag)
|
|
66
|
+
})))));
|
|
67
|
+
});
|
|
68
|
+
const ask = Effect.fn("effect-machine.actor.ask")(function* (event) {
|
|
69
|
+
if (yield* Ref.get(stoppedRef)) return yield* new ActorStoppedError({ actorId: id });
|
|
70
|
+
const reply = yield* Deferred.make();
|
|
71
|
+
pendingReplies.add(reply);
|
|
72
|
+
yield* Queue.offer(eventQueue, {
|
|
73
|
+
_tag: "ask",
|
|
74
|
+
event,
|
|
75
|
+
reply
|
|
76
|
+
});
|
|
77
|
+
return yield* Deferred.await(reply).pipe(Effect.ensuring(Effect.sync(() => pendingReplies.delete(reply))));
|
|
39
78
|
});
|
|
40
79
|
const snapshot = SubscriptionRef.get(stateRef).pipe(Effect.withSpan("effect-machine.actor.snapshot"));
|
|
41
80
|
const matches = Effect.fn("effect-machine.actor.matches")(function* (tag) {
|
|
@@ -71,30 +110,38 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
|
|
|
71
110
|
return {
|
|
72
111
|
id,
|
|
73
112
|
send,
|
|
113
|
+
cast: send,
|
|
114
|
+
call,
|
|
115
|
+
ask,
|
|
74
116
|
state: stateRef,
|
|
75
117
|
stop,
|
|
76
|
-
stopSync: () => Effect.runFork(stop),
|
|
77
118
|
snapshot,
|
|
78
|
-
snapshotSync: () => Effect.runSync(SubscriptionRef.get(stateRef)),
|
|
79
119
|
matches,
|
|
80
|
-
matchesSync: (tag) => Effect.runSync(SubscriptionRef.get(stateRef))._tag === tag,
|
|
81
120
|
can,
|
|
82
|
-
canSync: (event) => {
|
|
83
|
-
return resolveTransition(machine, Effect.runSync(SubscriptionRef.get(stateRef)), event) !== void 0;
|
|
84
|
-
},
|
|
85
121
|
changes: SubscriptionRef.changes(stateRef),
|
|
86
122
|
waitFor,
|
|
87
123
|
awaitFinal,
|
|
88
124
|
sendAndWait,
|
|
89
|
-
sendSync: (event) => {
|
|
90
|
-
if (!Effect.runSync(Ref.get(stoppedRef))) Effect.runSync(Queue.offer(eventQueue, event));
|
|
91
|
-
},
|
|
92
125
|
subscribe: (fn) => {
|
|
93
126
|
listeners.add(fn);
|
|
94
127
|
return () => {
|
|
95
128
|
listeners.delete(fn);
|
|
96
129
|
};
|
|
97
130
|
},
|
|
131
|
+
sync: {
|
|
132
|
+
send: (event) => {
|
|
133
|
+
if (!Effect.runSync(Ref.get(stoppedRef))) Effect.runSync(Queue.offer(eventQueue, {
|
|
134
|
+
_tag: "send",
|
|
135
|
+
event
|
|
136
|
+
}));
|
|
137
|
+
},
|
|
138
|
+
stop: () => Effect.runFork(stop),
|
|
139
|
+
snapshot: () => Effect.runSync(SubscriptionRef.get(stateRef)),
|
|
140
|
+
matches: (tag) => Effect.runSync(SubscriptionRef.get(stateRef))._tag === tag,
|
|
141
|
+
can: (event) => {
|
|
142
|
+
return resolveTransition(machine, Effect.runSync(SubscriptionRef.get(stateRef)), event) !== void 0;
|
|
143
|
+
}
|
|
144
|
+
},
|
|
98
145
|
system,
|
|
99
146
|
children: childrenMap
|
|
100
147
|
};
|
|
@@ -117,11 +164,16 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
117
164
|
const eventQueue = yield* Queue.unbounded();
|
|
118
165
|
const stoppedRef = yield* Ref.make(false);
|
|
119
166
|
const childrenMap = /* @__PURE__ */ new Map();
|
|
167
|
+
const selfSend = Effect.fn("effect-machine.actor.self.send")(function* (event) {
|
|
168
|
+
if (yield* Ref.get(stoppedRef)) return;
|
|
169
|
+
yield* Queue.offer(eventQueue, {
|
|
170
|
+
_tag: "send",
|
|
171
|
+
event
|
|
172
|
+
});
|
|
173
|
+
});
|
|
120
174
|
const self = {
|
|
121
|
-
send:
|
|
122
|
-
|
|
123
|
-
yield* Queue.offer(eventQueue, event);
|
|
124
|
-
}),
|
|
175
|
+
send: selfSend,
|
|
176
|
+
cast: selfSend,
|
|
125
177
|
spawn: (childId, childMachine) => Effect.gen(function* () {
|
|
126
178
|
const child = yield* system.spawn(childId, childMachine).pipe(Effect.provideService(ActorSystem, system));
|
|
127
179
|
childrenMap.set(childId, child);
|
|
@@ -175,9 +227,10 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
175
227
|
}));
|
|
176
228
|
yield* Ref.set(stoppedRef, true);
|
|
177
229
|
if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
|
|
178
|
-
return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, Ref.set(stoppedRef, true).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap);
|
|
230
|
+
return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, Ref.set(stoppedRef, true).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap, /* @__PURE__ */ new Set());
|
|
179
231
|
}
|
|
180
|
-
const
|
|
232
|
+
const pendingReplies = /* @__PURE__ */ new Set();
|
|
233
|
+
const loopFiber = yield* Effect.forkDetach(eventLoop(machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, id, inspectorValue, system, pendingReplies));
|
|
181
234
|
return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, Effect.gen(function* () {
|
|
182
235
|
const finalState = yield* SubscriptionRef.get(stateRef);
|
|
183
236
|
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
@@ -188,31 +241,104 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
188
241
|
}));
|
|
189
242
|
yield* Ref.set(stoppedRef, true);
|
|
190
243
|
yield* Fiber.interrupt(loopFiber);
|
|
244
|
+
yield* settlePendingReplies(pendingReplies, id);
|
|
191
245
|
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
192
246
|
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
193
247
|
if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
|
|
194
|
-
}).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap);
|
|
248
|
+
}).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap, pendingReplies);
|
|
249
|
+
});
|
|
250
|
+
/** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
|
|
251
|
+
const settlePendingReplies = (pendingReplies, actorId) => Effect.sync(() => {
|
|
252
|
+
const error = new ActorStoppedError({ actorId });
|
|
253
|
+
for (const deferred of pendingReplies) Effect.runFork(Deferred.fail(deferred, error));
|
|
254
|
+
pendingReplies.clear();
|
|
195
255
|
});
|
|
196
256
|
/**
|
|
197
|
-
* Main event loop for the actor
|
|
257
|
+
* Main event loop for the actor.
|
|
258
|
+
* Includes postpone buffer — events matching postpone rules are buffered
|
|
259
|
+
* and drained after state tag changes (gen_statem semantics).
|
|
198
260
|
*/
|
|
199
|
-
const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, actorId, inspector, system) {
|
|
200
|
-
|
|
201
|
-
|
|
261
|
+
const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, actorId, inspector, system, pendingReplies) {
|
|
262
|
+
const postponed = [];
|
|
263
|
+
const hasPostponeRules = machine.postponeRules.length > 0;
|
|
264
|
+
const processQueued = Effect.fn("effect-machine.actor.processQueued")(function* (queued) {
|
|
265
|
+
const event = queued.event;
|
|
202
266
|
const currentState = yield* SubscriptionRef.get(stateRef);
|
|
203
|
-
if (
|
|
267
|
+
if (hasPostponeRules && shouldPostpone(machine, currentState._tag, event._tag)) {
|
|
268
|
+
postponed.push(queued);
|
|
269
|
+
if (queued._tag === "call") {
|
|
270
|
+
const postponedResult = {
|
|
271
|
+
newState: currentState,
|
|
272
|
+
previousState: currentState,
|
|
273
|
+
transitioned: false,
|
|
274
|
+
lifecycleRan: false,
|
|
275
|
+
isFinal: false,
|
|
276
|
+
hasReply: false,
|
|
277
|
+
reply: void 0,
|
|
278
|
+
postponed: true
|
|
279
|
+
};
|
|
280
|
+
yield* Deferred.succeed(queued.reply, postponedResult);
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
shouldStop: false,
|
|
284
|
+
stateChanged: false
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const { shouldStop, result } = yield* Effect.withSpan("effect-machine.event.process", { attributes: {
|
|
204
288
|
"effect_machine.actor.id": actorId,
|
|
205
289
|
"effect_machine.state.current": currentState._tag,
|
|
206
290
|
"effect_machine.event.type": event._tag
|
|
207
|
-
} })(processEvent(machine, currentState, event, stateRef, self, listeners, stateScopeRef, actorId, inspector, system))
|
|
291
|
+
} })(processEvent(machine, currentState, event, stateRef, self, listeners, stateScopeRef, actorId, inspector, system));
|
|
292
|
+
switch (queued._tag) {
|
|
293
|
+
case "call":
|
|
294
|
+
yield* Deferred.succeed(queued.reply, result);
|
|
295
|
+
break;
|
|
296
|
+
case "ask":
|
|
297
|
+
if (result.hasReply) yield* Deferred.succeed(queued.reply, result.reply);
|
|
298
|
+
else yield* Deferred.fail(queued.reply, new NoReplyError({
|
|
299
|
+
actorId,
|
|
300
|
+
eventTag: event._tag
|
|
301
|
+
}));
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
shouldStop,
|
|
306
|
+
stateChanged: result.lifecycleRan
|
|
307
|
+
};
|
|
308
|
+
});
|
|
309
|
+
while (true) {
|
|
310
|
+
const { shouldStop, stateChanged } = yield* processQueued(yield* Queue.take(eventQueue));
|
|
311
|
+
if (shouldStop) {
|
|
208
312
|
yield* Ref.set(stoppedRef, true);
|
|
313
|
+
settlePostponedBuffer(postponed, pendingReplies, actorId);
|
|
314
|
+
yield* settlePendingReplies(pendingReplies, actorId);
|
|
209
315
|
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
210
316
|
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
211
317
|
return;
|
|
212
318
|
}
|
|
319
|
+
if (stateChanged && postponed.length > 0) {
|
|
320
|
+
const drained = postponed.splice(0);
|
|
321
|
+
for (const entry of drained) if ((yield* processQueued(entry)).shouldStop) {
|
|
322
|
+
yield* Ref.set(stoppedRef, true);
|
|
323
|
+
settlePostponedBuffer(postponed, pendingReplies, actorId);
|
|
324
|
+
yield* settlePendingReplies(pendingReplies, actorId);
|
|
325
|
+
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
326
|
+
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
213
330
|
}
|
|
214
331
|
});
|
|
215
332
|
/**
|
|
333
|
+
* Settle all reply-bearing entries in the postpone buffer on shutdown.
|
|
334
|
+
* Call entries already had their Deferred settled with the postponed result
|
|
335
|
+
* (so their pendingReplies entry is already removed). Ask/send entries
|
|
336
|
+
* with Deferreds are settled via the pendingReplies registry.
|
|
337
|
+
*/
|
|
338
|
+
const settlePostponedBuffer = (postponed, _pendingReplies, _actorId) => {
|
|
339
|
+
postponed.length = 0;
|
|
340
|
+
};
|
|
341
|
+
/**
|
|
216
342
|
* Process a single event, returning true if the actor should stop.
|
|
217
343
|
* Wraps processEventCore with actor-specific concerns (inspection, listeners, state ref).
|
|
218
344
|
*/
|
|
@@ -252,7 +378,10 @@ const processEvent = Effect.fn("effect-machine.actor.processEvent")(function* (m
|
|
|
252
378
|
});
|
|
253
379
|
if (!result.transitioned) {
|
|
254
380
|
yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", false);
|
|
255
|
-
return
|
|
381
|
+
return {
|
|
382
|
+
shouldStop: false,
|
|
383
|
+
result
|
|
384
|
+
};
|
|
256
385
|
}
|
|
257
386
|
yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", true);
|
|
258
387
|
yield* SubscriptionRef.set(stateRef, result.newState);
|
|
@@ -267,10 +396,16 @@ const processEvent = Effect.fn("effect-machine.actor.processEvent")(function* (m
|
|
|
267
396
|
finalState: result.newState,
|
|
268
397
|
timestamp
|
|
269
398
|
}));
|
|
270
|
-
return
|
|
399
|
+
return {
|
|
400
|
+
shouldStop: true,
|
|
401
|
+
result
|
|
402
|
+
};
|
|
271
403
|
}
|
|
272
404
|
}
|
|
273
|
-
return
|
|
405
|
+
return {
|
|
406
|
+
shouldStop: false,
|
|
407
|
+
result
|
|
408
|
+
};
|
|
274
409
|
});
|
|
275
410
|
/**
|
|
276
411
|
* Run spawn effects with actor-specific inspection and tracing.
|
|
@@ -454,6 +589,5 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
454
589
|
* Default ActorSystem layer
|
|
455
590
|
*/
|
|
456
591
|
const Default = Layer.effect(ActorSystem, make());
|
|
457
|
-
|
|
458
592
|
//#endregion
|
|
459
|
-
export { ActorSystem, Default, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects };
|
|
593
|
+
export { ActorSystem, Default, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { EffectsDef, GuardsDef } from "../slot.js";
|
|
2
2
|
import { ProcessEventHooks } from "../internal/transition.js";
|
|
3
3
|
import { Machine } from "../machine.js";
|
|
4
|
-
import "../actor.js";
|
|
5
4
|
import { Layer } from "effect";
|
|
6
5
|
import { Entity } from "effect/unstable/cluster";
|
|
7
6
|
import { Rpc } from "effect/unstable/rpc";
|