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