effect-machine 0.3.0 → 0.3.2
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/dist/_virtual/_rolldown/runtime.js +18 -0
- package/dist/actor.d.ts +251 -0
- package/dist/actor.js +385 -0
- package/dist/cluster/entity-machine.d.ts +90 -0
- package/dist/cluster/entity-machine.js +74 -0
- package/dist/cluster/index.d.ts +3 -0
- package/dist/cluster/index.js +4 -0
- package/dist/cluster/to-entity.d.ts +64 -0
- package/dist/cluster/to-entity.js +53 -0
- package/dist/errors.d.ts +61 -0
- package/dist/errors.js +38 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +14 -0
- package/dist/inspection.d.ts +125 -0
- package/dist/inspection.js +50 -0
- package/dist/internal/brands.d.ts +40 -0
- package/dist/internal/brands.js +0 -0
- package/dist/internal/inspection.d.ts +11 -0
- package/dist/internal/inspection.js +15 -0
- package/dist/internal/transition.d.ts +159 -0
- package/dist/internal/transition.js +235 -0
- package/dist/internal/utils.d.ts +52 -0
- package/dist/internal/utils.js +31 -0
- package/dist/machine.d.ts +271 -0
- package/dist/machine.js +317 -0
- package/{src/persistence/adapter.ts → dist/persistence/adapter.d.ts} +40 -72
- package/dist/persistence/adapter.js +27 -0
- package/dist/persistence/adapters/in-memory.d.ts +32 -0
- package/dist/persistence/adapters/in-memory.js +176 -0
- package/dist/persistence/index.d.ts +5 -0
- package/dist/persistence/index.js +6 -0
- package/dist/persistence/persistent-actor.d.ts +50 -0
- package/dist/persistence/persistent-actor.js +348 -0
- package/{src/persistence/persistent-machine.ts → dist/persistence/persistent-machine.d.ts} +28 -54
- package/dist/persistence/persistent-machine.js +24 -0
- package/dist/schema.d.ts +141 -0
- package/dist/schema.js +165 -0
- package/dist/slot.d.ts +128 -0
- package/dist/slot.js +99 -0
- package/dist/testing.d.ts +142 -0
- package/dist/testing.js +131 -0
- package/package.json +18 -7
- package/src/actor.ts +0 -1050
- package/src/cluster/entity-machine.ts +0 -201
- package/src/cluster/index.ts +0 -43
- package/src/cluster/to-entity.ts +0 -99
- package/src/errors.ts +0 -64
- package/src/index.ts +0 -105
- package/src/inspection.ts +0 -178
- package/src/internal/brands.ts +0 -51
- package/src/internal/inspection.ts +0 -18
- package/src/internal/transition.ts +0 -489
- package/src/internal/utils.ts +0 -80
- package/src/machine.ts +0 -836
- package/src/persistence/adapters/in-memory.ts +0 -294
- package/src/persistence/index.ts +0 -24
- package/src/persistence/persistent-actor.ts +0 -791
- package/src/schema.ts +0 -362
- package/src/slot.ts +0 -281
- package/src/testing.ts +0 -284
- package/tsconfig.json +0 -65
package/src/actor.ts
DELETED
|
@@ -1,1050 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Actor system: spawning, lifecycle, and event processing.
|
|
3
|
-
*
|
|
4
|
-
* Combines:
|
|
5
|
-
* - ActorRef interface (running actor handle)
|
|
6
|
-
* - ActorSystem service (spawn/stop/get actors)
|
|
7
|
-
* - Actor creation and event loop
|
|
8
|
-
*/
|
|
9
|
-
import type { Stream } from "effect";
|
|
10
|
-
import {
|
|
11
|
-
Cause,
|
|
12
|
-
Context,
|
|
13
|
-
Deferred,
|
|
14
|
-
Effect,
|
|
15
|
-
Exit,
|
|
16
|
-
Fiber,
|
|
17
|
-
Layer,
|
|
18
|
-
MutableHashMap,
|
|
19
|
-
Option,
|
|
20
|
-
Queue,
|
|
21
|
-
Ref,
|
|
22
|
-
Runtime,
|
|
23
|
-
Scope,
|
|
24
|
-
SubscriptionRef,
|
|
25
|
-
} from "effect";
|
|
26
|
-
|
|
27
|
-
import type { Machine, MachineRef, BuiltMachine } from "./machine.js";
|
|
28
|
-
import type { Inspector } from "./inspection.js";
|
|
29
|
-
import { Inspector as InspectorTag } from "./inspection.js";
|
|
30
|
-
import { processEventCore, runSpawnEffects, resolveTransition } from "./internal/transition.js";
|
|
31
|
-
import type { ProcessEventError, ProcessEventHooks } from "./internal/transition.js";
|
|
32
|
-
import { emitWithTimestamp } from "./internal/inspection.js";
|
|
33
|
-
|
|
34
|
-
// Re-export for external use (cluster, persistence)
|
|
35
|
-
export { resolveTransition, runSpawnEffects, processEventCore } from "./internal/transition.js";
|
|
36
|
-
export type {
|
|
37
|
-
ProcessEventError,
|
|
38
|
-
ProcessEventHooks,
|
|
39
|
-
ProcessEventResult,
|
|
40
|
-
} from "./internal/transition.js";
|
|
41
|
-
import type { GuardsDef, EffectsDef } from "./slot.js";
|
|
42
|
-
import { DuplicateActorError } from "./errors.js";
|
|
43
|
-
import { INTERNAL_INIT_EVENT } from "./internal/utils.js";
|
|
44
|
-
import type {
|
|
45
|
-
ActorMetadata,
|
|
46
|
-
PersistenceError,
|
|
47
|
-
RestoreResult,
|
|
48
|
-
VersionConflictError,
|
|
49
|
-
} from "./persistence/adapter.js";
|
|
50
|
-
import {
|
|
51
|
-
PersistenceAdapterTag,
|
|
52
|
-
PersistenceError as PersistenceErrorClass,
|
|
53
|
-
} from "./persistence/adapter.js";
|
|
54
|
-
import type { PersistentMachine } from "./persistence/persistent-machine.js";
|
|
55
|
-
import { isPersistentMachine } from "./persistence/persistent-machine.js";
|
|
56
|
-
import type { PersistentActorRef } from "./persistence/persistent-actor.js";
|
|
57
|
-
import { createPersistentActor, restorePersistentActor } from "./persistence/persistent-actor.js";
|
|
58
|
-
|
|
59
|
-
// ============================================================================
|
|
60
|
-
// ActorRef Interface
|
|
61
|
-
// ============================================================================
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Reference to a running actor.
|
|
65
|
-
*/
|
|
66
|
-
export interface ActorRef<State extends { readonly _tag: string }, Event> {
|
|
67
|
-
/**
|
|
68
|
-
* Unique identifier for this actor
|
|
69
|
-
*/
|
|
70
|
-
readonly id: string;
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Send an event to the actor
|
|
74
|
-
*/
|
|
75
|
-
readonly send: (event: Event) => Effect.Effect<void>;
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Observable state of the actor
|
|
79
|
-
*/
|
|
80
|
-
readonly state: SubscriptionRef.SubscriptionRef<State>;
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Stop the actor gracefully
|
|
84
|
-
*/
|
|
85
|
-
readonly stop: Effect.Effect<void>;
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Get current state snapshot (Effect)
|
|
89
|
-
*/
|
|
90
|
-
readonly snapshot: Effect.Effect<State>;
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Get current state snapshot (sync)
|
|
94
|
-
*/
|
|
95
|
-
readonly snapshotSync: () => State;
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Check if current state matches tag (Effect)
|
|
99
|
-
*/
|
|
100
|
-
readonly matches: (tag: State["_tag"]) => Effect.Effect<boolean>;
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Check if current state matches tag (sync)
|
|
104
|
-
*/
|
|
105
|
-
readonly matchesSync: (tag: State["_tag"]) => boolean;
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Check if event can be handled in current state (Effect)
|
|
109
|
-
*/
|
|
110
|
-
readonly can: (event: Event) => Effect.Effect<boolean>;
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Check if event can be handled in current state (sync)
|
|
114
|
-
*/
|
|
115
|
-
readonly canSync: (event: Event) => boolean;
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Stream of state changes
|
|
119
|
-
*/
|
|
120
|
-
readonly changes: Stream.Stream<State>;
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Wait for a state that matches predicate or state variant (includes current snapshot).
|
|
124
|
-
* Accepts a predicate function or a state constructor/value (e.g. `State.Active`).
|
|
125
|
-
*/
|
|
126
|
-
readonly waitFor: {
|
|
127
|
-
(predicate: (state: State) => boolean): Effect.Effect<State>;
|
|
128
|
-
(state: { readonly _tag: State["_tag"] }): Effect.Effect<State>;
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Wait for a final state (includes current snapshot)
|
|
133
|
-
*/
|
|
134
|
-
readonly awaitFinal: Effect.Effect<State>;
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Send event and wait for predicate, state variant, or final state.
|
|
138
|
-
* Accepts a predicate function or a state constructor/value (e.g. `State.Active`).
|
|
139
|
-
*/
|
|
140
|
-
readonly sendAndWait: {
|
|
141
|
-
(event: Event, predicate: (state: State) => boolean): Effect.Effect<State>;
|
|
142
|
-
(event: Event, state: { readonly _tag: State["_tag"] }): Effect.Effect<State>;
|
|
143
|
-
(event: Event): Effect.Effect<State>;
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Send event synchronously (fire-and-forget).
|
|
148
|
-
* No-op on stopped actors. Use when you need to send from sync contexts
|
|
149
|
-
* (e.g. framework hooks, event handlers).
|
|
150
|
-
*/
|
|
151
|
-
readonly sendSync: (event: Event) => void;
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Subscribe to state changes (sync callback)
|
|
155
|
-
* Returns unsubscribe function
|
|
156
|
-
*/
|
|
157
|
-
readonly subscribe: (fn: (state: State) => void) => () => void;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// ============================================================================
|
|
161
|
-
// ActorSystem Interface
|
|
162
|
-
// ============================================================================
|
|
163
|
-
|
|
164
|
-
/** Base type for stored actors (internal) */
|
|
165
|
-
type AnyState = { readonly _tag: string };
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Actor system for managing actor lifecycles
|
|
169
|
-
*/
|
|
170
|
-
export interface ActorSystem {
|
|
171
|
-
/**
|
|
172
|
-
* Spawn a new actor with the given machine.
|
|
173
|
-
*
|
|
174
|
-
* For regular machines, returns ActorRef.
|
|
175
|
-
* For persistent machines (created with Machine.persist), returns PersistentActorRef.
|
|
176
|
-
*
|
|
177
|
-
* All effect slots must be provided via `.build()` before spawning.
|
|
178
|
-
*
|
|
179
|
-
* @example
|
|
180
|
-
* ```ts
|
|
181
|
-
* // Regular machine (built)
|
|
182
|
-
* const built = machine.build({ fetchData: ... })
|
|
183
|
-
* const actor = yield* system.spawn("my-actor", built);
|
|
184
|
-
*
|
|
185
|
-
* // Persistent machine (auto-detected)
|
|
186
|
-
* const persistentActor = yield* system.spawn("my-actor", persistentMachine);
|
|
187
|
-
* persistentActor.persist; // available
|
|
188
|
-
* persistentActor.version; // available
|
|
189
|
-
* ```
|
|
190
|
-
*/
|
|
191
|
-
readonly spawn: {
|
|
192
|
-
// Regular machine overload (BuiltMachine)
|
|
193
|
-
<S extends { readonly _tag: string }, E extends { readonly _tag: string }, R>(
|
|
194
|
-
id: string,
|
|
195
|
-
machine: BuiltMachine<S, E, R>,
|
|
196
|
-
): Effect.Effect<ActorRef<S, E>, DuplicateActorError, R>;
|
|
197
|
-
|
|
198
|
-
// Persistent machine overload
|
|
199
|
-
<S extends { readonly _tag: string }, E extends { readonly _tag: string }, R>(
|
|
200
|
-
id: string,
|
|
201
|
-
machine: PersistentMachine<S, E, R>,
|
|
202
|
-
): Effect.Effect<
|
|
203
|
-
PersistentActorRef<S, E, R>,
|
|
204
|
-
PersistenceError | VersionConflictError | DuplicateActorError,
|
|
205
|
-
R | PersistenceAdapterTag
|
|
206
|
-
>;
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Restore an actor from persistence.
|
|
211
|
-
* Returns None if no persisted state exists for the given ID.
|
|
212
|
-
*
|
|
213
|
-
* @example
|
|
214
|
-
* ```ts
|
|
215
|
-
* const maybeActor = yield* system.restore("order-1", persistentMachine);
|
|
216
|
-
* if (Option.isSome(maybeActor)) {
|
|
217
|
-
* const actor = maybeActor.value;
|
|
218
|
-
* const state = yield* actor.snapshot;
|
|
219
|
-
* console.log(`Restored to state: ${state._tag}`);
|
|
220
|
-
* }
|
|
221
|
-
* ```
|
|
222
|
-
*/
|
|
223
|
-
readonly restore: <S extends { readonly _tag: string }, E extends { readonly _tag: string }, R>(
|
|
224
|
-
id: string,
|
|
225
|
-
machine: PersistentMachine<S, E, R>,
|
|
226
|
-
) => Effect.Effect<
|
|
227
|
-
Option.Option<PersistentActorRef<S, E, R>>,
|
|
228
|
-
PersistenceError | DuplicateActorError,
|
|
229
|
-
R | PersistenceAdapterTag
|
|
230
|
-
>;
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Get an existing actor by ID
|
|
234
|
-
*/
|
|
235
|
-
readonly get: (id: string) => Effect.Effect<Option.Option<ActorRef<AnyState, unknown>>>;
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Stop an actor by ID
|
|
239
|
-
*/
|
|
240
|
-
readonly stop: (id: string) => Effect.Effect<boolean>;
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* List all persisted actor metadata.
|
|
244
|
-
* Returns empty array if adapter doesn't support registry.
|
|
245
|
-
*
|
|
246
|
-
* @example
|
|
247
|
-
* ```ts
|
|
248
|
-
* const actors = yield* system.listPersisted();
|
|
249
|
-
* for (const meta of actors) {
|
|
250
|
-
* console.log(`${meta.id}: ${meta.stateTag} (v${meta.version})`);
|
|
251
|
-
* }
|
|
252
|
-
* ```
|
|
253
|
-
*/
|
|
254
|
-
readonly listPersisted: () => Effect.Effect<
|
|
255
|
-
ReadonlyArray<ActorMetadata>,
|
|
256
|
-
PersistenceError,
|
|
257
|
-
PersistenceAdapterTag
|
|
258
|
-
>;
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Restore multiple actors by ID.
|
|
262
|
-
* Returns both successfully restored actors and failures.
|
|
263
|
-
*
|
|
264
|
-
* @example
|
|
265
|
-
* ```ts
|
|
266
|
-
* const result = yield* system.restoreMany(["order-1", "order-2"], orderMachine);
|
|
267
|
-
* console.log(`Restored: ${result.restored.length}, Failed: ${result.failed.length}`);
|
|
268
|
-
* ```
|
|
269
|
-
*/
|
|
270
|
-
readonly restoreMany: <
|
|
271
|
-
S extends { readonly _tag: string },
|
|
272
|
-
E extends { readonly _tag: string },
|
|
273
|
-
R,
|
|
274
|
-
>(
|
|
275
|
-
ids: ReadonlyArray<string>,
|
|
276
|
-
machine: PersistentMachine<S, E, R>,
|
|
277
|
-
) => Effect.Effect<RestoreResult<S, E, R>, never, R | PersistenceAdapterTag>;
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Restore all persisted actors for a machine type.
|
|
281
|
-
* Uses adapter registry if available, otherwise returns empty result.
|
|
282
|
-
*
|
|
283
|
-
* @example
|
|
284
|
-
* ```ts
|
|
285
|
-
* const result = yield* system.restoreAll(orderMachine, {
|
|
286
|
-
* filter: (meta) => meta.stateTag !== "Done"
|
|
287
|
-
* });
|
|
288
|
-
* console.log(`Restored ${result.restored.length} active orders`);
|
|
289
|
-
* ```
|
|
290
|
-
*/
|
|
291
|
-
readonly restoreAll: <
|
|
292
|
-
S extends { readonly _tag: string },
|
|
293
|
-
E extends { readonly _tag: string },
|
|
294
|
-
R,
|
|
295
|
-
>(
|
|
296
|
-
machine: PersistentMachine<S, E, R>,
|
|
297
|
-
options?: { filter?: (meta: ActorMetadata) => boolean },
|
|
298
|
-
) => Effect.Effect<RestoreResult<S, E, R>, PersistenceError, R | PersistenceAdapterTag>;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* ActorSystem service tag
|
|
303
|
-
*/
|
|
304
|
-
export const ActorSystem = Context.GenericTag<ActorSystem>("@effect/machine/ActorSystem");
|
|
305
|
-
|
|
306
|
-
// ============================================================================
|
|
307
|
-
// Actor Core Helpers
|
|
308
|
-
// ============================================================================
|
|
309
|
-
|
|
310
|
-
/** Listener set for sync subscriptions */
|
|
311
|
-
export type Listeners<S> = Set<(state: S) => void>;
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Notify all listeners of state change.
|
|
315
|
-
*/
|
|
316
|
-
export const notifyListeners = <S>(listeners: Listeners<S>, state: S): void => {
|
|
317
|
-
for (const listener of listeners) {
|
|
318
|
-
try {
|
|
319
|
-
listener(state);
|
|
320
|
-
} catch {
|
|
321
|
-
// Ignore listener failures to avoid crashing the actor loop
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* Build core ActorRef methods shared between regular and persistent actors.
|
|
328
|
-
*/
|
|
329
|
-
export const buildActorRefCore = <
|
|
330
|
-
S extends { readonly _tag: string },
|
|
331
|
-
E extends { readonly _tag: string },
|
|
332
|
-
R,
|
|
333
|
-
GD extends GuardsDef,
|
|
334
|
-
EFD extends EffectsDef,
|
|
335
|
-
>(
|
|
336
|
-
id: string,
|
|
337
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Schema fields need wide acceptance
|
|
338
|
-
machine: Machine<S, E, R, any, any, GD, EFD>,
|
|
339
|
-
stateRef: SubscriptionRef.SubscriptionRef<S>,
|
|
340
|
-
eventQueue: Queue.Queue<E>,
|
|
341
|
-
stoppedRef: Ref.Ref<boolean>,
|
|
342
|
-
listeners: Listeners<S>,
|
|
343
|
-
stop: Effect.Effect<void>,
|
|
344
|
-
): ActorRef<S, E> => {
|
|
345
|
-
const send = Effect.fn("effect-machine.actor.send")(function* (event: E) {
|
|
346
|
-
const stopped = yield* Ref.get(stoppedRef);
|
|
347
|
-
if (stopped) {
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
yield* Queue.offer(eventQueue, event);
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
const snapshot = SubscriptionRef.get(stateRef).pipe(
|
|
354
|
-
Effect.withSpan("effect-machine.actor.snapshot"),
|
|
355
|
-
);
|
|
356
|
-
|
|
357
|
-
const matches = Effect.fn("effect-machine.actor.matches")(function* (tag: S["_tag"]) {
|
|
358
|
-
const state = yield* SubscriptionRef.get(stateRef);
|
|
359
|
-
return state._tag === tag;
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
const can = Effect.fn("effect-machine.actor.can")(function* (event: E) {
|
|
363
|
-
const state = yield* SubscriptionRef.get(stateRef);
|
|
364
|
-
return resolveTransition(machine, state, event) !== undefined;
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
const waitFor = Effect.fn("effect-machine.actor.waitFor")(function* (
|
|
368
|
-
predicateOrState: ((state: S) => boolean) | { readonly _tag: S["_tag"] },
|
|
369
|
-
) {
|
|
370
|
-
const predicate =
|
|
371
|
-
typeof predicateOrState === "function" && !("_tag" in predicateOrState)
|
|
372
|
-
? predicateOrState
|
|
373
|
-
: (s: S) => s._tag === (predicateOrState as { readonly _tag: string })._tag;
|
|
374
|
-
|
|
375
|
-
// Check current state first — SubscriptionRef.get acquires/releases
|
|
376
|
-
// the semaphore quickly (read-only), no deadlock risk.
|
|
377
|
-
const current = yield* SubscriptionRef.get(stateRef);
|
|
378
|
-
if (predicate(current)) return current;
|
|
379
|
-
|
|
380
|
-
// Use sync listener + Deferred to avoid holding the SubscriptionRef
|
|
381
|
-
// semaphore for the duration of a stream (which causes deadlock when
|
|
382
|
-
// send triggers SubscriptionRef.set concurrently).
|
|
383
|
-
const done = yield* Deferred.make<S>();
|
|
384
|
-
const rt = yield* Effect.runtime<never>();
|
|
385
|
-
const runFork = Runtime.runFork(rt);
|
|
386
|
-
const listener = (state: S) => {
|
|
387
|
-
if (predicate(state)) {
|
|
388
|
-
runFork(Deferred.succeed(done, state));
|
|
389
|
-
}
|
|
390
|
-
};
|
|
391
|
-
listeners.add(listener);
|
|
392
|
-
|
|
393
|
-
// Re-check after subscribing to close the race window
|
|
394
|
-
const afterSubscribe = yield* SubscriptionRef.get(stateRef);
|
|
395
|
-
if (predicate(afterSubscribe)) {
|
|
396
|
-
listeners.delete(listener);
|
|
397
|
-
return afterSubscribe;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
const result = yield* Deferred.await(done);
|
|
401
|
-
listeners.delete(listener);
|
|
402
|
-
return result;
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
const awaitFinal = waitFor((state) => machine.finalStates.has(state._tag)).pipe(
|
|
406
|
-
Effect.withSpan("effect-machine.actor.awaitFinal"),
|
|
407
|
-
);
|
|
408
|
-
|
|
409
|
-
const sendAndWait = Effect.fn("effect-machine.actor.sendAndWait")(function* (
|
|
410
|
-
event: E,
|
|
411
|
-
predicateOrState?: ((state: S) => boolean) | { readonly _tag: S["_tag"] },
|
|
412
|
-
) {
|
|
413
|
-
yield* send(event);
|
|
414
|
-
if (predicateOrState !== undefined) {
|
|
415
|
-
return yield* waitFor(predicateOrState);
|
|
416
|
-
}
|
|
417
|
-
return yield* awaitFinal;
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
return {
|
|
421
|
-
id,
|
|
422
|
-
send,
|
|
423
|
-
state: stateRef,
|
|
424
|
-
stop,
|
|
425
|
-
snapshot,
|
|
426
|
-
snapshotSync: () => Effect.runSync(SubscriptionRef.get(stateRef)),
|
|
427
|
-
matches,
|
|
428
|
-
matchesSync: (tag) => Effect.runSync(SubscriptionRef.get(stateRef))._tag === tag,
|
|
429
|
-
can,
|
|
430
|
-
canSync: (event) => {
|
|
431
|
-
const state = Effect.runSync(SubscriptionRef.get(stateRef));
|
|
432
|
-
return resolveTransition(machine, state, event) !== undefined;
|
|
433
|
-
},
|
|
434
|
-
changes: stateRef.changes,
|
|
435
|
-
waitFor,
|
|
436
|
-
awaitFinal,
|
|
437
|
-
sendAndWait,
|
|
438
|
-
sendSync: (event) => {
|
|
439
|
-
const stopped = Effect.runSync(Ref.get(stoppedRef));
|
|
440
|
-
if (!stopped) {
|
|
441
|
-
Effect.runSync(Queue.offer(eventQueue, event));
|
|
442
|
-
}
|
|
443
|
-
},
|
|
444
|
-
subscribe: (fn) => {
|
|
445
|
-
listeners.add(fn);
|
|
446
|
-
return () => {
|
|
447
|
-
listeners.delete(fn);
|
|
448
|
-
};
|
|
449
|
-
},
|
|
450
|
-
};
|
|
451
|
-
};
|
|
452
|
-
|
|
453
|
-
// ============================================================================
|
|
454
|
-
// Actor Creation and Event Loop
|
|
455
|
-
// ============================================================================
|
|
456
|
-
|
|
457
|
-
/**
|
|
458
|
-
* Create and start an actor for a machine
|
|
459
|
-
*/
|
|
460
|
-
export const createActor = Effect.fn("effect-machine.actor.spawn")(function* <
|
|
461
|
-
S extends { readonly _tag: string },
|
|
462
|
-
E extends { readonly _tag: string },
|
|
463
|
-
R,
|
|
464
|
-
GD extends GuardsDef,
|
|
465
|
-
EFD extends EffectsDef,
|
|
466
|
-
>(id: string, machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>) {
|
|
467
|
-
yield* Effect.annotateCurrentSpan("effect_machine.actor.id", id);
|
|
468
|
-
|
|
469
|
-
// Get optional inspector from context
|
|
470
|
-
const inspectorValue = Option.getOrUndefined(yield* Effect.serviceOption(InspectorTag)) as
|
|
471
|
-
| Inspector<S, E>
|
|
472
|
-
| undefined;
|
|
473
|
-
|
|
474
|
-
// Create self reference for sending events
|
|
475
|
-
const eventQueue = yield* Queue.unbounded<E>();
|
|
476
|
-
const stoppedRef = yield* Ref.make(false);
|
|
477
|
-
const self: MachineRef<E> = {
|
|
478
|
-
send: Effect.fn("effect-machine.actor.self.send")(function* (event: E) {
|
|
479
|
-
const stopped = yield* Ref.get(stoppedRef);
|
|
480
|
-
if (stopped) {
|
|
481
|
-
return;
|
|
482
|
-
}
|
|
483
|
-
yield* Queue.offer(eventQueue, event);
|
|
484
|
-
}),
|
|
485
|
-
};
|
|
486
|
-
|
|
487
|
-
// Annotate span with initial state
|
|
488
|
-
yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", machine.initial._tag);
|
|
489
|
-
|
|
490
|
-
// Emit spawn event
|
|
491
|
-
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
492
|
-
type: "@machine.spawn",
|
|
493
|
-
actorId: id,
|
|
494
|
-
initialState: machine.initial,
|
|
495
|
-
timestamp,
|
|
496
|
-
}));
|
|
497
|
-
|
|
498
|
-
// Initialize state
|
|
499
|
-
const stateRef = yield* SubscriptionRef.make(machine.initial);
|
|
500
|
-
const listeners: Listeners<S> = new Set();
|
|
501
|
-
|
|
502
|
-
// Fork background effects (run for entire machine lifetime)
|
|
503
|
-
const backgroundFibers: Fiber.Fiber<void, never>[] = [];
|
|
504
|
-
const initEvent = { _tag: INTERNAL_INIT_EVENT } as E;
|
|
505
|
-
const ctx = { state: machine.initial, event: initEvent, self };
|
|
506
|
-
const { effects: effectSlots } = machine._slots;
|
|
507
|
-
|
|
508
|
-
for (const bg of machine.backgroundEffects) {
|
|
509
|
-
const fiber = yield* Effect.forkDaemon(
|
|
510
|
-
bg
|
|
511
|
-
.handler({ state: machine.initial, event: initEvent, self, effects: effectSlots })
|
|
512
|
-
.pipe(Effect.provideService(machine.Context, ctx)),
|
|
513
|
-
);
|
|
514
|
-
backgroundFibers.push(fiber);
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// Create state scope for initial state's spawn effects
|
|
518
|
-
const stateScopeRef: { current: Scope.CloseableScope } = {
|
|
519
|
-
current: yield* Scope.make(),
|
|
520
|
-
};
|
|
521
|
-
|
|
522
|
-
// Run initial spawn effects
|
|
523
|
-
yield* runSpawnEffectsWithInspection(
|
|
524
|
-
machine,
|
|
525
|
-
machine.initial,
|
|
526
|
-
initEvent,
|
|
527
|
-
self,
|
|
528
|
-
stateScopeRef.current,
|
|
529
|
-
id,
|
|
530
|
-
inspectorValue,
|
|
531
|
-
);
|
|
532
|
-
|
|
533
|
-
// Check if initial state (after always) is final
|
|
534
|
-
if (machine.finalStates.has(machine.initial._tag)) {
|
|
535
|
-
// Close state scope and interrupt background effects
|
|
536
|
-
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
537
|
-
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
538
|
-
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
539
|
-
type: "@machine.stop",
|
|
540
|
-
actorId: id,
|
|
541
|
-
finalState: machine.initial,
|
|
542
|
-
timestamp,
|
|
543
|
-
}));
|
|
544
|
-
yield* Ref.set(stoppedRef, true);
|
|
545
|
-
const stop = Ref.set(stoppedRef, true).pipe(
|
|
546
|
-
Effect.withSpan("effect-machine.actor.stop"),
|
|
547
|
-
Effect.asVoid,
|
|
548
|
-
);
|
|
549
|
-
return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, stop);
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// Start the event loop — use forkDaemon so the event loop fiber's lifetime
|
|
553
|
-
// is detached from any parent scope/fiber. actor.stop handles cleanup.
|
|
554
|
-
const loopFiber = yield* Effect.forkDaemon(
|
|
555
|
-
eventLoop(
|
|
556
|
-
machine,
|
|
557
|
-
stateRef,
|
|
558
|
-
eventQueue,
|
|
559
|
-
stoppedRef,
|
|
560
|
-
self,
|
|
561
|
-
listeners,
|
|
562
|
-
backgroundFibers,
|
|
563
|
-
stateScopeRef,
|
|
564
|
-
id,
|
|
565
|
-
inspectorValue,
|
|
566
|
-
),
|
|
567
|
-
);
|
|
568
|
-
|
|
569
|
-
const stop = Effect.gen(function* () {
|
|
570
|
-
const finalState = yield* SubscriptionRef.get(stateRef);
|
|
571
|
-
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
572
|
-
type: "@machine.stop",
|
|
573
|
-
actorId: id,
|
|
574
|
-
finalState,
|
|
575
|
-
timestamp,
|
|
576
|
-
}));
|
|
577
|
-
yield* Ref.set(stoppedRef, true);
|
|
578
|
-
yield* Fiber.interrupt(loopFiber);
|
|
579
|
-
// Close state scope (interrupts spawn fibers)
|
|
580
|
-
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
581
|
-
// Interrupt background effects (in parallel)
|
|
582
|
-
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
583
|
-
}).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid);
|
|
584
|
-
|
|
585
|
-
return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, stop);
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
/**
|
|
589
|
-
* Main event loop for the actor
|
|
590
|
-
*/
|
|
591
|
-
const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* <
|
|
592
|
-
S extends { readonly _tag: string },
|
|
593
|
-
E extends { readonly _tag: string },
|
|
594
|
-
R,
|
|
595
|
-
GD extends GuardsDef,
|
|
596
|
-
EFD extends EffectsDef,
|
|
597
|
-
>(
|
|
598
|
-
machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>,
|
|
599
|
-
stateRef: SubscriptionRef.SubscriptionRef<S>,
|
|
600
|
-
eventQueue: Queue.Queue<E>,
|
|
601
|
-
stoppedRef: Ref.Ref<boolean>,
|
|
602
|
-
self: MachineRef<E>,
|
|
603
|
-
listeners: Listeners<S>,
|
|
604
|
-
backgroundFibers: Fiber.Fiber<void, never>[],
|
|
605
|
-
stateScopeRef: { current: Scope.CloseableScope },
|
|
606
|
-
actorId: string,
|
|
607
|
-
inspector?: Inspector<S, E>,
|
|
608
|
-
) {
|
|
609
|
-
while (true) {
|
|
610
|
-
// Block waiting for next event - will fail with QueueShutdown when queue is shut down
|
|
611
|
-
const event = yield* Queue.take(eventQueue);
|
|
612
|
-
|
|
613
|
-
const currentState = yield* SubscriptionRef.get(stateRef);
|
|
614
|
-
|
|
615
|
-
// Process event in a span
|
|
616
|
-
const shouldStop = yield* Effect.withSpan("effect-machine.event.process", {
|
|
617
|
-
attributes: {
|
|
618
|
-
"effect_machine.actor.id": actorId,
|
|
619
|
-
"effect_machine.state.current": currentState._tag,
|
|
620
|
-
"effect_machine.event.type": event._tag,
|
|
621
|
-
},
|
|
622
|
-
})(
|
|
623
|
-
processEvent(
|
|
624
|
-
machine,
|
|
625
|
-
currentState,
|
|
626
|
-
event,
|
|
627
|
-
stateRef,
|
|
628
|
-
self,
|
|
629
|
-
listeners,
|
|
630
|
-
stateScopeRef,
|
|
631
|
-
actorId,
|
|
632
|
-
inspector,
|
|
633
|
-
),
|
|
634
|
-
);
|
|
635
|
-
|
|
636
|
-
if (shouldStop) {
|
|
637
|
-
// Close state scope and interrupt background effects when reaching final state
|
|
638
|
-
yield* Ref.set(stoppedRef, true);
|
|
639
|
-
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
640
|
-
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
641
|
-
return;
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
});
|
|
645
|
-
|
|
646
|
-
/**
|
|
647
|
-
* Process a single event, returning true if the actor should stop.
|
|
648
|
-
* Wraps processEventCore with actor-specific concerns (inspection, listeners, state ref).
|
|
649
|
-
*/
|
|
650
|
-
const processEvent = Effect.fn("effect-machine.actor.processEvent")(function* <
|
|
651
|
-
S extends { readonly _tag: string },
|
|
652
|
-
E extends { readonly _tag: string },
|
|
653
|
-
R,
|
|
654
|
-
GD extends GuardsDef,
|
|
655
|
-
EFD extends EffectsDef,
|
|
656
|
-
>(
|
|
657
|
-
machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>,
|
|
658
|
-
currentState: S,
|
|
659
|
-
event: E,
|
|
660
|
-
stateRef: SubscriptionRef.SubscriptionRef<S>,
|
|
661
|
-
self: MachineRef<E>,
|
|
662
|
-
listeners: Listeners<S>,
|
|
663
|
-
stateScopeRef: { current: Scope.CloseableScope },
|
|
664
|
-
actorId: string,
|
|
665
|
-
inspector?: Inspector<S, E>,
|
|
666
|
-
) {
|
|
667
|
-
// Emit event received
|
|
668
|
-
yield* emitWithTimestamp(inspector, (timestamp) => ({
|
|
669
|
-
type: "@machine.event",
|
|
670
|
-
actorId,
|
|
671
|
-
state: currentState,
|
|
672
|
-
event,
|
|
673
|
-
timestamp,
|
|
674
|
-
}));
|
|
675
|
-
|
|
676
|
-
// Build inspection hooks for processEventCore
|
|
677
|
-
const hooks: ProcessEventHooks<S, E> | undefined =
|
|
678
|
-
inspector === undefined
|
|
679
|
-
? undefined
|
|
680
|
-
: {
|
|
681
|
-
onSpawnEffect: (state) =>
|
|
682
|
-
emitWithTimestamp(inspector, (timestamp) => ({
|
|
683
|
-
type: "@machine.effect",
|
|
684
|
-
actorId,
|
|
685
|
-
effectType: "spawn",
|
|
686
|
-
state,
|
|
687
|
-
timestamp,
|
|
688
|
-
})),
|
|
689
|
-
onTransition: (from, to, ev) =>
|
|
690
|
-
emitWithTimestamp(inspector, (timestamp) => ({
|
|
691
|
-
type: "@machine.transition",
|
|
692
|
-
actorId,
|
|
693
|
-
fromState: from,
|
|
694
|
-
toState: to,
|
|
695
|
-
event: ev,
|
|
696
|
-
timestamp,
|
|
697
|
-
})),
|
|
698
|
-
onError: (info) =>
|
|
699
|
-
emitWithTimestamp(inspector, (timestamp) => ({
|
|
700
|
-
type: "@machine.error",
|
|
701
|
-
actorId,
|
|
702
|
-
phase: info.phase,
|
|
703
|
-
state: info.state,
|
|
704
|
-
event: info.event,
|
|
705
|
-
error: Cause.pretty(info.cause),
|
|
706
|
-
timestamp,
|
|
707
|
-
})),
|
|
708
|
-
};
|
|
709
|
-
|
|
710
|
-
// Process event using shared core
|
|
711
|
-
const result = yield* processEventCore(machine, currentState, event, self, stateScopeRef, hooks);
|
|
712
|
-
|
|
713
|
-
if (!result.transitioned) {
|
|
714
|
-
yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", false);
|
|
715
|
-
return false;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", true);
|
|
719
|
-
|
|
720
|
-
// Update state ref and notify listeners
|
|
721
|
-
yield* SubscriptionRef.set(stateRef, result.newState);
|
|
722
|
-
notifyListeners(listeners, result.newState);
|
|
723
|
-
|
|
724
|
-
if (result.lifecycleRan) {
|
|
725
|
-
yield* Effect.annotateCurrentSpan("effect_machine.state.from", result.previousState._tag);
|
|
726
|
-
yield* Effect.annotateCurrentSpan("effect_machine.state.to", result.newState._tag);
|
|
727
|
-
|
|
728
|
-
// Transition inspection event emitted via hooks in processEventCore
|
|
729
|
-
|
|
730
|
-
// Check if new state is final
|
|
731
|
-
if (result.isFinal) {
|
|
732
|
-
yield* emitWithTimestamp(inspector, (timestamp) => ({
|
|
733
|
-
type: "@machine.stop",
|
|
734
|
-
actorId,
|
|
735
|
-
finalState: result.newState,
|
|
736
|
-
timestamp,
|
|
737
|
-
}));
|
|
738
|
-
return true;
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
return false;
|
|
743
|
-
});
|
|
744
|
-
|
|
745
|
-
/**
|
|
746
|
-
* Run spawn effects with actor-specific inspection and tracing.
|
|
747
|
-
* Wraps the core runSpawnEffects with inspection events and spans.
|
|
748
|
-
* @internal
|
|
749
|
-
*/
|
|
750
|
-
const runSpawnEffectsWithInspection = Effect.fn("effect-machine.actor.spawnEffects")(function* <
|
|
751
|
-
S extends { readonly _tag: string },
|
|
752
|
-
E extends { readonly _tag: string },
|
|
753
|
-
R,
|
|
754
|
-
GD extends GuardsDef,
|
|
755
|
-
EFD extends EffectsDef,
|
|
756
|
-
>(
|
|
757
|
-
machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>,
|
|
758
|
-
state: S,
|
|
759
|
-
event: E,
|
|
760
|
-
self: MachineRef<E>,
|
|
761
|
-
stateScope: Scope.CloseableScope,
|
|
762
|
-
actorId: string,
|
|
763
|
-
inspector?: Inspector<S, E>,
|
|
764
|
-
) {
|
|
765
|
-
// Emit inspection event before running effects
|
|
766
|
-
yield* emitWithTimestamp(inspector, (timestamp) => ({
|
|
767
|
-
type: "@machine.effect",
|
|
768
|
-
actorId,
|
|
769
|
-
effectType: "spawn",
|
|
770
|
-
state,
|
|
771
|
-
timestamp,
|
|
772
|
-
}));
|
|
773
|
-
|
|
774
|
-
// Use shared core
|
|
775
|
-
const onError =
|
|
776
|
-
inspector === undefined
|
|
777
|
-
? undefined
|
|
778
|
-
: (info: ProcessEventError<S, E>) =>
|
|
779
|
-
emitWithTimestamp(inspector, (timestamp) => ({
|
|
780
|
-
type: "@machine.error",
|
|
781
|
-
actorId,
|
|
782
|
-
phase: info.phase,
|
|
783
|
-
state: info.state,
|
|
784
|
-
event: info.event,
|
|
785
|
-
error: Cause.pretty(info.cause),
|
|
786
|
-
timestamp,
|
|
787
|
-
}));
|
|
788
|
-
|
|
789
|
-
yield* runSpawnEffects(machine, state, event, self, stateScope, onError);
|
|
790
|
-
});
|
|
791
|
-
|
|
792
|
-
// ============================================================================
|
|
793
|
-
// ActorSystem Implementation
|
|
794
|
-
// ============================================================================
|
|
795
|
-
|
|
796
|
-
/**
|
|
797
|
-
* Internal implementation
|
|
798
|
-
*/
|
|
799
|
-
const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
800
|
-
// MutableHashMap for O(1) spawn/stop/get operations
|
|
801
|
-
const actors = MutableHashMap.empty<string, ActorRef<AnyState, unknown>>();
|
|
802
|
-
const spawnGate = yield* Effect.makeSemaphore(1);
|
|
803
|
-
const withSpawnGate = spawnGate.withPermits(1);
|
|
804
|
-
|
|
805
|
-
// Stop all actors on system teardown
|
|
806
|
-
yield* Effect.addFinalizer(() => {
|
|
807
|
-
const stops: Effect.Effect<void>[] = [];
|
|
808
|
-
MutableHashMap.forEach(actors, (actor) => {
|
|
809
|
-
stops.push(actor.stop);
|
|
810
|
-
});
|
|
811
|
-
return Effect.all(stops, { concurrency: "unbounded" }).pipe(Effect.asVoid);
|
|
812
|
-
});
|
|
813
|
-
|
|
814
|
-
/** Check for duplicate ID, register actor, attach scope cleanup if available */
|
|
815
|
-
const registerActor = Effect.fn("effect-machine.actorSystem.register")(function* <
|
|
816
|
-
T extends { stop: Effect.Effect<void> },
|
|
817
|
-
>(id: string, actor: T) {
|
|
818
|
-
// Check if actor already exists
|
|
819
|
-
if (MutableHashMap.has(actors, id)) {
|
|
820
|
-
// Stop the newly created actor to avoid leaks
|
|
821
|
-
yield* actor.stop;
|
|
822
|
-
return yield* new DuplicateActorError({ actorId: id });
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
// Register it - O(1)
|
|
826
|
-
MutableHashMap.set(actors, id, actor as unknown as ActorRef<AnyState, unknown>);
|
|
827
|
-
|
|
828
|
-
// If scope available, attach per-actor cleanup
|
|
829
|
-
const maybeScope = yield* Effect.serviceOption(Scope.Scope);
|
|
830
|
-
if (Option.isSome(maybeScope)) {
|
|
831
|
-
yield* Scope.addFinalizer(
|
|
832
|
-
maybeScope.value,
|
|
833
|
-
Effect.gen(function* () {
|
|
834
|
-
yield* actor.stop;
|
|
835
|
-
MutableHashMap.remove(actors, id);
|
|
836
|
-
}),
|
|
837
|
-
);
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
return actor;
|
|
841
|
-
});
|
|
842
|
-
|
|
843
|
-
const spawnRegular = Effect.fn("effect-machine.actorSystem.spawnRegular")(function* <
|
|
844
|
-
S extends { readonly _tag: string },
|
|
845
|
-
E extends { readonly _tag: string },
|
|
846
|
-
R,
|
|
847
|
-
>(id: string, built: BuiltMachine<S, E, R>) {
|
|
848
|
-
if (MutableHashMap.has(actors, id)) {
|
|
849
|
-
return yield* new DuplicateActorError({ actorId: id });
|
|
850
|
-
}
|
|
851
|
-
// Create and register the actor
|
|
852
|
-
const actor = yield* createActor(id, built._inner);
|
|
853
|
-
return yield* registerActor(id, actor);
|
|
854
|
-
});
|
|
855
|
-
|
|
856
|
-
const spawnPersistent = Effect.fn("effect-machine.actorSystem.spawnPersistent")(function* <
|
|
857
|
-
S extends { readonly _tag: string },
|
|
858
|
-
E extends { readonly _tag: string },
|
|
859
|
-
R,
|
|
860
|
-
>(id: string, persistentMachine: PersistentMachine<S, E, R>) {
|
|
861
|
-
if (MutableHashMap.has(actors, id)) {
|
|
862
|
-
return yield* new DuplicateActorError({ actorId: id });
|
|
863
|
-
}
|
|
864
|
-
const adapter = yield* PersistenceAdapterTag;
|
|
865
|
-
|
|
866
|
-
// Try to load existing snapshot
|
|
867
|
-
const maybeSnapshot = yield* adapter.loadSnapshot(
|
|
868
|
-
id,
|
|
869
|
-
persistentMachine.persistence.stateSchema,
|
|
870
|
-
);
|
|
871
|
-
|
|
872
|
-
// Load events after snapshot (or all events if no snapshot)
|
|
873
|
-
const events = yield* adapter.loadEvents(
|
|
874
|
-
id,
|
|
875
|
-
persistentMachine.persistence.eventSchema,
|
|
876
|
-
Option.isSome(maybeSnapshot) ? maybeSnapshot.value.version : undefined,
|
|
877
|
-
);
|
|
878
|
-
|
|
879
|
-
// Create and register the persistent actor
|
|
880
|
-
const actor = yield* createPersistentActor(id, persistentMachine, maybeSnapshot, events);
|
|
881
|
-
return yield* registerActor(id, actor);
|
|
882
|
-
});
|
|
883
|
-
|
|
884
|
-
const spawnImpl = Effect.fn("effect-machine.actorSystem.spawn")(function* <
|
|
885
|
-
S extends { readonly _tag: string },
|
|
886
|
-
E extends { readonly _tag: string },
|
|
887
|
-
R,
|
|
888
|
-
>(id: string, machine: BuiltMachine<S, E, R> | PersistentMachine<S, E, R>) {
|
|
889
|
-
if (isPersistentMachine(machine)) {
|
|
890
|
-
// TypeScript can't narrow union with invariant generic params
|
|
891
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
892
|
-
return yield* spawnPersistent(id, machine as PersistentMachine<S, E, R>);
|
|
893
|
-
}
|
|
894
|
-
return yield* spawnRegular(id, machine as BuiltMachine<S, E, R>);
|
|
895
|
-
});
|
|
896
|
-
|
|
897
|
-
// Type-safe overloaded spawn implementation
|
|
898
|
-
function spawn<S extends { readonly _tag: string }, E extends { readonly _tag: string }, R>(
|
|
899
|
-
id: string,
|
|
900
|
-
machine: BuiltMachine<S, E, R>,
|
|
901
|
-
): Effect.Effect<ActorRef<S, E>, DuplicateActorError, R>;
|
|
902
|
-
function spawn<S extends { readonly _tag: string }, E extends { readonly _tag: string }, R>(
|
|
903
|
-
id: string,
|
|
904
|
-
machine: PersistentMachine<S, E, R>,
|
|
905
|
-
): Effect.Effect<
|
|
906
|
-
PersistentActorRef<S, E, R>,
|
|
907
|
-
PersistenceError | VersionConflictError | DuplicateActorError,
|
|
908
|
-
R | PersistenceAdapterTag
|
|
909
|
-
>;
|
|
910
|
-
function spawn<S extends { readonly _tag: string }, E extends { readonly _tag: string }, R>(
|
|
911
|
-
id: string,
|
|
912
|
-
machine: BuiltMachine<S, E, R> | PersistentMachine<S, E, R>,
|
|
913
|
-
):
|
|
914
|
-
| Effect.Effect<ActorRef<S, E>, DuplicateActorError, R>
|
|
915
|
-
| Effect.Effect<
|
|
916
|
-
PersistentActorRef<S, E, R>,
|
|
917
|
-
PersistenceError | VersionConflictError | DuplicateActorError,
|
|
918
|
-
R | PersistenceAdapterTag
|
|
919
|
-
> {
|
|
920
|
-
return withSpawnGate(spawnImpl(id, machine)) as
|
|
921
|
-
| Effect.Effect<ActorRef<S, E>, DuplicateActorError, R>
|
|
922
|
-
| Effect.Effect<
|
|
923
|
-
PersistentActorRef<S, E, R>,
|
|
924
|
-
PersistenceError | VersionConflictError | DuplicateActorError,
|
|
925
|
-
R | PersistenceAdapterTag
|
|
926
|
-
>;
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
const restoreImpl = Effect.fn("effect-machine.actorSystem.restore")(function* <
|
|
930
|
-
S extends { readonly _tag: string },
|
|
931
|
-
E extends { readonly _tag: string },
|
|
932
|
-
R,
|
|
933
|
-
>(id: string, persistentMachine: PersistentMachine<S, E, R>) {
|
|
934
|
-
// Try to restore from persistence
|
|
935
|
-
const maybeActor = yield* restorePersistentActor(id, persistentMachine);
|
|
936
|
-
|
|
937
|
-
if (Option.isSome(maybeActor)) {
|
|
938
|
-
yield* registerActor(id, maybeActor.value);
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
return maybeActor;
|
|
942
|
-
});
|
|
943
|
-
const restore = <S extends { readonly _tag: string }, E extends { readonly _tag: string }, R>(
|
|
944
|
-
id: string,
|
|
945
|
-
persistentMachine: PersistentMachine<S, E, R>,
|
|
946
|
-
) => withSpawnGate(restoreImpl(id, persistentMachine));
|
|
947
|
-
|
|
948
|
-
const get = Effect.fn("effect-machine.actorSystem.get")(function* (id: string) {
|
|
949
|
-
return yield* Effect.sync(() => MutableHashMap.get(actors, id));
|
|
950
|
-
});
|
|
951
|
-
|
|
952
|
-
const stop = Effect.fn("effect-machine.actorSystem.stop")(function* (id: string) {
|
|
953
|
-
const maybeActor = MutableHashMap.get(actors, id);
|
|
954
|
-
if (Option.isNone(maybeActor)) {
|
|
955
|
-
return false;
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
yield* maybeActor.value.stop;
|
|
959
|
-
MutableHashMap.remove(actors, id);
|
|
960
|
-
return true;
|
|
961
|
-
});
|
|
962
|
-
|
|
963
|
-
const listPersisted = Effect.fn("effect-machine.actorSystem.listPersisted")(function* () {
|
|
964
|
-
const adapter = yield* PersistenceAdapterTag;
|
|
965
|
-
if (adapter.listActors === undefined) {
|
|
966
|
-
return [];
|
|
967
|
-
}
|
|
968
|
-
return yield* adapter.listActors();
|
|
969
|
-
});
|
|
970
|
-
|
|
971
|
-
const restoreMany = Effect.fn("effect-machine.actorSystem.restoreMany")(function* <
|
|
972
|
-
S extends { readonly _tag: string },
|
|
973
|
-
E extends { readonly _tag: string },
|
|
974
|
-
R,
|
|
975
|
-
>(ids: ReadonlyArray<string>, persistentMachine: PersistentMachine<S, E, R>) {
|
|
976
|
-
const restored: PersistentActorRef<S, E, R>[] = [];
|
|
977
|
-
const failed: {
|
|
978
|
-
id: string;
|
|
979
|
-
error: PersistenceError | DuplicateActorError;
|
|
980
|
-
}[] = [];
|
|
981
|
-
|
|
982
|
-
for (const id of ids) {
|
|
983
|
-
// Skip if already running
|
|
984
|
-
if (MutableHashMap.has(actors, id)) {
|
|
985
|
-
continue;
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
const result = yield* Effect.either(restore(id, persistentMachine));
|
|
989
|
-
if (result._tag === "Left") {
|
|
990
|
-
failed.push({ id, error: result.left });
|
|
991
|
-
} else if (Option.isSome(result.right)) {
|
|
992
|
-
restored.push(result.right.value);
|
|
993
|
-
} else {
|
|
994
|
-
// No persisted state for this ID
|
|
995
|
-
failed.push({
|
|
996
|
-
id,
|
|
997
|
-
error: new PersistenceErrorClass({
|
|
998
|
-
operation: "restore",
|
|
999
|
-
actorId: id,
|
|
1000
|
-
message: "No persisted state found",
|
|
1001
|
-
}),
|
|
1002
|
-
});
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
return { restored, failed };
|
|
1007
|
-
});
|
|
1008
|
-
|
|
1009
|
-
const restoreAll = Effect.fn("effect-machine.actorSystem.restoreAll")(function* <
|
|
1010
|
-
S extends { readonly _tag: string },
|
|
1011
|
-
E extends { readonly _tag: string },
|
|
1012
|
-
R,
|
|
1013
|
-
>(
|
|
1014
|
-
persistentMachine: PersistentMachine<S, E, R>,
|
|
1015
|
-
options?: { filter?: (meta: ActorMetadata) => boolean },
|
|
1016
|
-
) {
|
|
1017
|
-
const adapter = yield* PersistenceAdapterTag;
|
|
1018
|
-
if (adapter.listActors === undefined) {
|
|
1019
|
-
return { restored: [], failed: [] };
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
// Require explicit machineType to prevent cross-machine restores
|
|
1023
|
-
const machineType = persistentMachine.persistence.machineType;
|
|
1024
|
-
if (machineType === undefined) {
|
|
1025
|
-
return yield* new PersistenceErrorClass({
|
|
1026
|
-
operation: "restoreAll",
|
|
1027
|
-
actorId: "*",
|
|
1028
|
-
message: "restoreAll requires explicit machineType in persistence config",
|
|
1029
|
-
});
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
const allMetadata = yield* adapter.listActors();
|
|
1033
|
-
|
|
1034
|
-
// Filter by machineType and optional user filter
|
|
1035
|
-
let filtered = allMetadata.filter((meta) => meta.machineType === machineType);
|
|
1036
|
-
if (options?.filter !== undefined) {
|
|
1037
|
-
filtered = filtered.filter(options.filter);
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
const ids = filtered.map((meta) => meta.id);
|
|
1041
|
-
return yield* restoreMany(ids, persistentMachine);
|
|
1042
|
-
});
|
|
1043
|
-
|
|
1044
|
-
return ActorSystem.of({ spawn, restore, get, stop, listPersisted, restoreMany, restoreAll });
|
|
1045
|
-
});
|
|
1046
|
-
|
|
1047
|
-
/**
|
|
1048
|
-
* Default ActorSystem layer
|
|
1049
|
-
*/
|
|
1050
|
-
export const Default = Layer.scoped(ActorSystem, make());
|