effect-machine 0.1.0 → 0.2.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 +17 -0
- package/package.json +1 -1
- package/src/actor.ts +544 -455
- package/src/cluster/entity-machine.ts +81 -82
- package/src/index.ts +1 -0
- package/src/inspection.ts +17 -0
- package/src/internal/inspection.ts +18 -0
- package/src/internal/transition.ts +150 -94
- package/src/machine.ts +139 -71
- package/src/persistence/adapter.ts +4 -3
- package/src/persistence/adapters/in-memory.ts +201 -182
- package/src/persistence/persistent-actor.ts +582 -386
- package/src/testing.ts +92 -98
package/src/actor.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - Actor creation and event loop
|
|
8
8
|
*/
|
|
9
9
|
import {
|
|
10
|
-
|
|
10
|
+
Cause,
|
|
11
11
|
Context,
|
|
12
12
|
Effect,
|
|
13
13
|
Exit,
|
|
@@ -16,22 +16,28 @@ import {
|
|
|
16
16
|
MutableHashMap,
|
|
17
17
|
Option,
|
|
18
18
|
Queue,
|
|
19
|
+
Ref,
|
|
19
20
|
Scope,
|
|
21
|
+
Stream,
|
|
20
22
|
SubscriptionRef,
|
|
21
23
|
} from "effect";
|
|
22
|
-
import type { Stream } from "effect";
|
|
23
24
|
|
|
24
25
|
import type { Machine, MachineRef } from "./machine.js";
|
|
25
|
-
import type {
|
|
26
|
+
import type { Inspector } from "./inspection.js";
|
|
26
27
|
import { Inspector as InspectorTag } from "./inspection.js";
|
|
27
28
|
import { processEventCore, runSpawnEffects, resolveTransition } from "./internal/transition.js";
|
|
28
|
-
import type { ProcessEventHooks } from "./internal/transition.js";
|
|
29
|
+
import type { ProcessEventError, ProcessEventHooks } from "./internal/transition.js";
|
|
30
|
+
import { emitWithTimestamp } from "./internal/inspection.js";
|
|
29
31
|
|
|
30
32
|
// Re-export for external use (cluster, persistence)
|
|
31
33
|
export { resolveTransition, runSpawnEffects, processEventCore } from "./internal/transition.js";
|
|
32
|
-
export type {
|
|
34
|
+
export type {
|
|
35
|
+
ProcessEventError,
|
|
36
|
+
ProcessEventHooks,
|
|
37
|
+
ProcessEventResult,
|
|
38
|
+
} from "./internal/transition.js";
|
|
33
39
|
import type { GuardsDef, EffectsDef } from "./slot.js";
|
|
34
|
-
import { DuplicateActorError } from "./errors.js";
|
|
40
|
+
import { DuplicateActorError, UnprovidedSlotsError } from "./errors.js";
|
|
35
41
|
import { INTERNAL_INIT_EVENT } from "./internal/utils.js";
|
|
36
42
|
import type {
|
|
37
43
|
ActorMetadata,
|
|
@@ -111,6 +117,24 @@ export interface ActorRef<State extends { readonly _tag: string }, Event> {
|
|
|
111
117
|
*/
|
|
112
118
|
readonly changes: Stream.Stream<State>;
|
|
113
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Wait for a state that matches predicate (includes current snapshot)
|
|
122
|
+
*/
|
|
123
|
+
readonly waitFor: (predicate: (state: State) => boolean) => Effect.Effect<State>;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Wait for a final state (includes current snapshot)
|
|
127
|
+
*/
|
|
128
|
+
readonly awaitFinal: Effect.Effect<State>;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Send event and wait for predicate or final state
|
|
132
|
+
*/
|
|
133
|
+
readonly sendAndWait: (
|
|
134
|
+
event: Event,
|
|
135
|
+
predicate?: (state: State) => boolean,
|
|
136
|
+
) => Effect.Effect<State>;
|
|
137
|
+
|
|
114
138
|
/**
|
|
115
139
|
* Subscribe to state changes (sync callback)
|
|
116
140
|
* Returns unsubscribe function
|
|
@@ -162,15 +186,15 @@ export interface ActorSystem {
|
|
|
162
186
|
id: string,
|
|
163
187
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Schema fields need wide acceptance
|
|
164
188
|
machine: Machine<S, E, R, any, any, GD, EFD>,
|
|
165
|
-
): Effect.Effect<ActorRef<S, E>, DuplicateActorError, R | Scope.Scope>;
|
|
189
|
+
): Effect.Effect<ActorRef<S, E>, DuplicateActorError | UnprovidedSlotsError, R | Scope.Scope>;
|
|
166
190
|
|
|
167
191
|
// Persistent machine overload
|
|
168
192
|
<S extends { readonly _tag: string }, E extends { readonly _tag: string }, R>(
|
|
169
193
|
id: string,
|
|
170
194
|
machine: PersistentMachine<S, E, R>,
|
|
171
195
|
): Effect.Effect<
|
|
172
|
-
PersistentActorRef<S, E>,
|
|
173
|
-
PersistenceError | VersionConflictError | DuplicateActorError,
|
|
196
|
+
PersistentActorRef<S, E, R>,
|
|
197
|
+
PersistenceError | VersionConflictError | DuplicateActorError | UnprovidedSlotsError,
|
|
174
198
|
R | Scope.Scope | PersistenceAdapterTag
|
|
175
199
|
>;
|
|
176
200
|
};
|
|
@@ -193,8 +217,8 @@ export interface ActorSystem {
|
|
|
193
217
|
id: string,
|
|
194
218
|
machine: PersistentMachine<S, E, R>,
|
|
195
219
|
) => Effect.Effect<
|
|
196
|
-
Option.Option<PersistentActorRef<S, E>>,
|
|
197
|
-
PersistenceError | DuplicateActorError,
|
|
220
|
+
Option.Option<PersistentActorRef<S, E, R>>,
|
|
221
|
+
PersistenceError | DuplicateActorError | UnprovidedSlotsError,
|
|
198
222
|
R | Scope.Scope | PersistenceAdapterTag
|
|
199
223
|
>;
|
|
200
224
|
|
|
@@ -243,7 +267,7 @@ export interface ActorSystem {
|
|
|
243
267
|
>(
|
|
244
268
|
ids: ReadonlyArray<string>,
|
|
245
269
|
machine: PersistentMachine<S, E, R>,
|
|
246
|
-
) => Effect.Effect<RestoreResult<S, E>, never, R | Scope.Scope | PersistenceAdapterTag>;
|
|
270
|
+
) => Effect.Effect<RestoreResult<S, E, R>, never, R | Scope.Scope | PersistenceAdapterTag>;
|
|
247
271
|
|
|
248
272
|
/**
|
|
249
273
|
* Restore all persisted actors for a machine type.
|
|
@@ -265,7 +289,7 @@ export interface ActorSystem {
|
|
|
265
289
|
machine: PersistentMachine<S, E, R>,
|
|
266
290
|
options?: { filter?: (meta: ActorMetadata) => boolean },
|
|
267
291
|
) => Effect.Effect<
|
|
268
|
-
RestoreResult<S, E>,
|
|
292
|
+
RestoreResult<S, E, R>,
|
|
269
293
|
PersistenceError,
|
|
270
294
|
R | Scope.Scope | PersistenceAdapterTag
|
|
271
295
|
>;
|
|
@@ -288,7 +312,11 @@ export type Listeners<S> = Set<(state: S) => void>;
|
|
|
288
312
|
*/
|
|
289
313
|
export const notifyListeners = <S>(listeners: Listeners<S>, state: S): void => {
|
|
290
314
|
for (const listener of listeners) {
|
|
291
|
-
|
|
315
|
+
try {
|
|
316
|
+
listener(state);
|
|
317
|
+
} catch {
|
|
318
|
+
// Ignore listener failures to avoid crashing the actor loop
|
|
319
|
+
}
|
|
292
320
|
}
|
|
293
321
|
};
|
|
294
322
|
|
|
@@ -307,50 +335,84 @@ export const buildActorRefCore = <
|
|
|
307
335
|
machine: Machine<S, E, R, any, any, GD, EFD>,
|
|
308
336
|
stateRef: SubscriptionRef.SubscriptionRef<S>,
|
|
309
337
|
eventQueue: Queue.Queue<E>,
|
|
338
|
+
stoppedRef: Ref.Ref<boolean>,
|
|
310
339
|
listeners: Listeners<S>,
|
|
311
340
|
stop: Effect.Effect<void>,
|
|
312
|
-
): ActorRef<S, E> =>
|
|
313
|
-
({
|
|
341
|
+
): ActorRef<S, E> => {
|
|
342
|
+
const send = Effect.fn("effect-machine.actor.send")(function* (event: E) {
|
|
343
|
+
const stopped = yield* Ref.get(stoppedRef);
|
|
344
|
+
if (stopped) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
yield* Queue.offer(eventQueue, event);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const snapshot = SubscriptionRef.get(stateRef).pipe(
|
|
351
|
+
Effect.withSpan("effect-machine.actor.snapshot"),
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
const matches = Effect.fn("effect-machine.actor.matches")(function* (tag: S["_tag"]) {
|
|
355
|
+
const state = yield* SubscriptionRef.get(stateRef);
|
|
356
|
+
return state._tag === tag;
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const can = Effect.fn("effect-machine.actor.can")(function* (event: E) {
|
|
360
|
+
const state = yield* SubscriptionRef.get(stateRef);
|
|
361
|
+
return resolveTransition(machine, state, event) !== undefined;
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const waitFor = Effect.fn("effect-machine.actor.waitFor")(function* (
|
|
365
|
+
predicate: (state: S) => boolean,
|
|
366
|
+
) {
|
|
367
|
+
const current = yield* SubscriptionRef.get(stateRef);
|
|
368
|
+
if (predicate(current)) {
|
|
369
|
+
return current;
|
|
370
|
+
}
|
|
371
|
+
const next = yield* stateRef.changes.pipe(Stream.filter(predicate), Stream.runHead);
|
|
372
|
+
return Option.getOrElse(next, () => current);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
const awaitFinal = waitFor((state) => machine.finalStates.has(state._tag)).pipe(
|
|
376
|
+
Effect.withSpan("effect-machine.actor.awaitFinal"),
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
const sendAndWait = Effect.fn("effect-machine.actor.sendAndWait")(function* (
|
|
380
|
+
event: E,
|
|
381
|
+
predicate?: (state: S) => boolean,
|
|
382
|
+
) {
|
|
383
|
+
yield* send(event);
|
|
384
|
+
if (predicate !== undefined) {
|
|
385
|
+
return yield* waitFor(predicate);
|
|
386
|
+
}
|
|
387
|
+
return yield* awaitFinal;
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
return {
|
|
314
391
|
id,
|
|
315
|
-
send
|
|
392
|
+
send,
|
|
316
393
|
state: stateRef,
|
|
317
394
|
stop,
|
|
318
|
-
snapshot
|
|
395
|
+
snapshot,
|
|
319
396
|
snapshotSync: () => Effect.runSync(SubscriptionRef.get(stateRef)),
|
|
320
|
-
matches
|
|
397
|
+
matches,
|
|
321
398
|
matchesSync: (tag) => Effect.runSync(SubscriptionRef.get(stateRef))._tag === tag,
|
|
322
|
-
can
|
|
323
|
-
Effect.map(
|
|
324
|
-
SubscriptionRef.get(stateRef),
|
|
325
|
-
(s) => resolveTransition(machine, s, event) !== undefined,
|
|
326
|
-
),
|
|
399
|
+
can,
|
|
327
400
|
canSync: (event) => {
|
|
328
401
|
const state = Effect.runSync(SubscriptionRef.get(stateRef));
|
|
329
402
|
return resolveTransition(machine, state, event) !== undefined;
|
|
330
403
|
},
|
|
331
404
|
changes: stateRef.changes,
|
|
405
|
+
waitFor,
|
|
406
|
+
awaitFinal,
|
|
407
|
+
sendAndWait,
|
|
332
408
|
subscribe: (fn) => {
|
|
333
409
|
listeners.add(fn);
|
|
334
410
|
return () => {
|
|
335
411
|
listeners.delete(fn);
|
|
336
412
|
};
|
|
337
413
|
},
|
|
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
|
-
);
|
|
414
|
+
};
|
|
415
|
+
};
|
|
354
416
|
|
|
355
417
|
// ============================================================================
|
|
356
418
|
// Actor Creation and Event Loop
|
|
@@ -359,143 +421,142 @@ const emitWithTimestamp = <S, E>(
|
|
|
359
421
|
/**
|
|
360
422
|
* Create and start an actor for a machine
|
|
361
423
|
*/
|
|
362
|
-
export const createActor = <
|
|
424
|
+
export const createActor = Effect.fn("effect-machine.actor.spawn")(function* <
|
|
363
425
|
S extends { readonly _tag: string },
|
|
364
426
|
E extends { readonly _tag: string },
|
|
365
427
|
R,
|
|
366
428
|
GD extends GuardsDef,
|
|
367
429
|
EFD extends EffectsDef,
|
|
368
|
-
>(
|
|
369
|
-
id
|
|
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);
|
|
430
|
+
>(id: string, machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>) {
|
|
431
|
+
yield* Effect.annotateCurrentSpan("effect_machine.actor.id", id);
|
|
389
432
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
});
|
|
433
|
+
const missing = machine._missingSlots();
|
|
434
|
+
if (missing.length > 0) {
|
|
435
|
+
return yield* new UnprovidedSlotsError({ slots: missing });
|
|
436
|
+
}
|
|
410
437
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
438
|
+
// Get optional inspector from context
|
|
439
|
+
const inspectorValue = Option.getOrUndefined(yield* Effect.serviceOption(InspectorTag)) as
|
|
440
|
+
| Inspector<S, E>
|
|
441
|
+
| undefined;
|
|
442
|
+
|
|
443
|
+
// Create self reference for sending events
|
|
444
|
+
const eventQueue = yield* Queue.unbounded<E>();
|
|
445
|
+
const stoppedRef = yield* Ref.make(false);
|
|
446
|
+
const self: MachineRef<E> = {
|
|
447
|
+
send: Effect.fn("effect-machine.actor.self.send")(function* (event: E) {
|
|
448
|
+
const stopped = yield* Ref.get(stoppedRef);
|
|
449
|
+
if (stopped) {
|
|
450
|
+
return;
|
|
416
451
|
}
|
|
452
|
+
yield* Queue.offer(eventQueue, event);
|
|
453
|
+
}),
|
|
454
|
+
};
|
|
417
455
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
456
|
+
// Annotate span with initial state
|
|
457
|
+
yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", machine.initial._tag);
|
|
458
|
+
|
|
459
|
+
// Emit spawn event
|
|
460
|
+
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
461
|
+
type: "@machine.spawn",
|
|
462
|
+
actorId: id,
|
|
463
|
+
initialState: machine.initial,
|
|
464
|
+
timestamp,
|
|
465
|
+
}));
|
|
466
|
+
|
|
467
|
+
// Initialize state
|
|
468
|
+
const stateRef = yield* SubscriptionRef.make(machine.initial);
|
|
469
|
+
const listeners: Listeners<S> = new Set();
|
|
470
|
+
|
|
471
|
+
// Fork background effects (run for entire machine lifetime)
|
|
472
|
+
const backgroundFibers: Fiber.Fiber<void, never>[] = [];
|
|
473
|
+
const initEvent = { _tag: INTERNAL_INIT_EVENT } as E;
|
|
474
|
+
const ctx = { state: machine.initial, event: initEvent, self };
|
|
475
|
+
const { effects: effectSlots } = machine._slots;
|
|
476
|
+
|
|
477
|
+
for (const bg of machine.backgroundEffects) {
|
|
478
|
+
const fiber = yield* Effect.fork(
|
|
479
|
+
bg
|
|
480
|
+
.handler({ state: machine.initial, event: initEvent, self, effects: effectSlots })
|
|
481
|
+
.pipe(Effect.provideService(machine.Context, ctx)),
|
|
482
|
+
);
|
|
483
|
+
backgroundFibers.push(fiber);
|
|
484
|
+
}
|
|
422
485
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
-
}
|
|
486
|
+
// Create state scope for initial state's spawn effects
|
|
487
|
+
const stateScopeRef: { current: Scope.CloseableScope } = {
|
|
488
|
+
current: yield* Scope.make(),
|
|
489
|
+
};
|
|
454
490
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
id,
|
|
466
|
-
inspectorValue,
|
|
467
|
-
),
|
|
468
|
-
);
|
|
491
|
+
// Run initial spawn effects
|
|
492
|
+
yield* runSpawnEffectsWithInspection(
|
|
493
|
+
machine,
|
|
494
|
+
machine.initial,
|
|
495
|
+
initEvent,
|
|
496
|
+
self,
|
|
497
|
+
stateScopeRef.current,
|
|
498
|
+
id,
|
|
499
|
+
inspectorValue,
|
|
500
|
+
);
|
|
469
501
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
502
|
+
// Check if initial state (after always) is final
|
|
503
|
+
if (machine.finalStates.has(machine.initial._tag)) {
|
|
504
|
+
// Close state scope and interrupt background effects
|
|
505
|
+
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
506
|
+
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
507
|
+
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
508
|
+
type: "@machine.stop",
|
|
509
|
+
actorId: id,
|
|
510
|
+
finalState: machine.initial,
|
|
511
|
+
timestamp,
|
|
512
|
+
}));
|
|
513
|
+
yield* Ref.set(stoppedRef, true);
|
|
514
|
+
const stop = Ref.set(stoppedRef, true).pipe(
|
|
515
|
+
Effect.withSpan("effect-machine.actor.stop"),
|
|
516
|
+
Effect.asVoid,
|
|
517
|
+
);
|
|
518
|
+
return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, stop);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Start the event loop
|
|
522
|
+
const loopFiber = yield* Effect.fork(
|
|
523
|
+
eventLoop(
|
|
524
|
+
machine,
|
|
525
|
+
stateRef,
|
|
526
|
+
eventQueue,
|
|
527
|
+
stoppedRef,
|
|
528
|
+
self,
|
|
529
|
+
listeners,
|
|
530
|
+
backgroundFibers,
|
|
531
|
+
stateScopeRef,
|
|
532
|
+
id,
|
|
533
|
+
inspectorValue,
|
|
534
|
+
),
|
|
493
535
|
);
|
|
494
536
|
|
|
537
|
+
const stop = Effect.gen(function* () {
|
|
538
|
+
const finalState = yield* SubscriptionRef.get(stateRef);
|
|
539
|
+
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
540
|
+
type: "@machine.stop",
|
|
541
|
+
actorId: id,
|
|
542
|
+
finalState,
|
|
543
|
+
timestamp,
|
|
544
|
+
}));
|
|
545
|
+
yield* Ref.set(stoppedRef, true);
|
|
546
|
+
yield* Fiber.interrupt(loopFiber);
|
|
547
|
+
// Close state scope (interrupts spawn fibers)
|
|
548
|
+
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
549
|
+
// Interrupt background effects (in parallel)
|
|
550
|
+
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
551
|
+
}).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid);
|
|
552
|
+
|
|
553
|
+
return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, stop);
|
|
554
|
+
});
|
|
555
|
+
|
|
495
556
|
/**
|
|
496
557
|
* Main event loop for the actor
|
|
497
558
|
*/
|
|
498
|
-
const eventLoop = <
|
|
559
|
+
const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* <
|
|
499
560
|
S extends { readonly _tag: string },
|
|
500
561
|
E extends { readonly _tag: string },
|
|
501
562
|
R,
|
|
@@ -505,55 +566,56 @@ const eventLoop = <
|
|
|
505
566
|
machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>,
|
|
506
567
|
stateRef: SubscriptionRef.SubscriptionRef<S>,
|
|
507
568
|
eventQueue: Queue.Queue<E>,
|
|
569
|
+
stoppedRef: Ref.Ref<boolean>,
|
|
508
570
|
self: MachineRef<E>,
|
|
509
571
|
listeners: Listeners<S>,
|
|
510
572
|
backgroundFibers: Fiber.Fiber<void, never>[],
|
|
511
573
|
stateScopeRef: { current: Scope.CloseableScope },
|
|
512
574
|
actorId: string,
|
|
513
575
|
inspector?: Inspector<S, E>,
|
|
514
|
-
)
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
}
|
|
576
|
+
) {
|
|
577
|
+
while (true) {
|
|
578
|
+
// Block waiting for next event - will fail with QueueShutdown when queue is shut down
|
|
579
|
+
const event = yield* Queue.take(eventQueue);
|
|
580
|
+
|
|
581
|
+
const currentState = yield* SubscriptionRef.get(stateRef);
|
|
582
|
+
|
|
583
|
+
// Process event in a span
|
|
584
|
+
const shouldStop = yield* Effect.withSpan("effect-machine.event.process", {
|
|
585
|
+
attributes: {
|
|
586
|
+
"effect_machine.actor.id": actorId,
|
|
587
|
+
"effect_machine.state.current": currentState._tag,
|
|
588
|
+
"effect_machine.event.type": event._tag,
|
|
589
|
+
},
|
|
590
|
+
})(
|
|
591
|
+
processEvent(
|
|
592
|
+
machine,
|
|
593
|
+
currentState,
|
|
594
|
+
event,
|
|
595
|
+
stateRef,
|
|
596
|
+
self,
|
|
597
|
+
listeners,
|
|
598
|
+
stateScopeRef,
|
|
599
|
+
actorId,
|
|
600
|
+
inspector,
|
|
601
|
+
),
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
if (shouldStop) {
|
|
605
|
+
// Close state scope and interrupt background effects when reaching final state
|
|
606
|
+
yield* Ref.set(stoppedRef, true);
|
|
607
|
+
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
608
|
+
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
609
|
+
return;
|
|
549
610
|
}
|
|
550
|
-
}
|
|
611
|
+
}
|
|
612
|
+
});
|
|
551
613
|
|
|
552
614
|
/**
|
|
553
615
|
* Process a single event, returning true if the actor should stop.
|
|
554
616
|
* Wraps processEventCore with actor-specific concerns (inspection, listeners, state ref).
|
|
555
617
|
*/
|
|
556
|
-
const processEvent = <
|
|
618
|
+
const processEvent = Effect.fn("effect-machine.actor.processEvent")(function* <
|
|
557
619
|
S extends { readonly _tag: string },
|
|
558
620
|
E extends { readonly _tag: string },
|
|
559
621
|
R,
|
|
@@ -569,89 +631,91 @@ const processEvent = <
|
|
|
569
631
|
stateScopeRef: { current: Scope.CloseableScope },
|
|
570
632
|
actorId: string,
|
|
571
633
|
inspector?: Inspector<S, E>,
|
|
572
|
-
)
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
634
|
+
) {
|
|
635
|
+
// Emit event received
|
|
636
|
+
yield* emitWithTimestamp(inspector, (timestamp) => ({
|
|
637
|
+
type: "@machine.event",
|
|
638
|
+
actorId,
|
|
639
|
+
state: currentState,
|
|
640
|
+
event,
|
|
641
|
+
timestamp,
|
|
642
|
+
}));
|
|
643
|
+
|
|
644
|
+
// Build inspection hooks for processEventCore
|
|
645
|
+
const hooks: ProcessEventHooks<S, E> | undefined =
|
|
646
|
+
inspector === undefined
|
|
647
|
+
? undefined
|
|
648
|
+
: {
|
|
649
|
+
onSpawnEffect: (state) =>
|
|
650
|
+
emitWithTimestamp(inspector, (timestamp) => ({
|
|
651
|
+
type: "@machine.effect",
|
|
652
|
+
actorId,
|
|
653
|
+
effectType: "spawn",
|
|
654
|
+
state,
|
|
655
|
+
timestamp,
|
|
656
|
+
})),
|
|
657
|
+
onTransition: (from, to, ev) =>
|
|
658
|
+
emitWithTimestamp(inspector, (timestamp) => ({
|
|
659
|
+
type: "@machine.transition",
|
|
660
|
+
actorId,
|
|
661
|
+
fromState: from,
|
|
662
|
+
toState: to,
|
|
663
|
+
event: ev,
|
|
664
|
+
timestamp,
|
|
665
|
+
})),
|
|
666
|
+
onError: (info) =>
|
|
667
|
+
emitWithTimestamp(inspector, (timestamp) => ({
|
|
668
|
+
type: "@machine.error",
|
|
669
|
+
actorId,
|
|
670
|
+
phase: info.phase,
|
|
671
|
+
state: info.state,
|
|
672
|
+
event: info.event,
|
|
673
|
+
error: Cause.pretty(info.cause),
|
|
674
|
+
timestamp,
|
|
675
|
+
})),
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
// Process event using shared core
|
|
679
|
+
const result = yield* processEventCore(machine, currentState, event, self, stateScopeRef, hooks);
|
|
680
|
+
|
|
681
|
+
if (!result.transitioned) {
|
|
682
|
+
yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", false);
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
621
685
|
|
|
622
|
-
|
|
686
|
+
yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", true);
|
|
623
687
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
688
|
+
// Update state ref and notify listeners
|
|
689
|
+
yield* SubscriptionRef.set(stateRef, result.newState);
|
|
690
|
+
notifyListeners(listeners, result.newState);
|
|
627
691
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
692
|
+
if (result.lifecycleRan) {
|
|
693
|
+
yield* Effect.annotateCurrentSpan("effect_machine.state.from", result.previousState._tag);
|
|
694
|
+
yield* Effect.annotateCurrentSpan("effect_machine.state.to", result.newState._tag);
|
|
631
695
|
|
|
632
|
-
|
|
696
|
+
// Transition inspection event emitted via hooks in processEventCore
|
|
633
697
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
}
|
|
698
|
+
// Check if new state is final
|
|
699
|
+
if (result.isFinal) {
|
|
700
|
+
yield* emitWithTimestamp(inspector, (timestamp) => ({
|
|
701
|
+
type: "@machine.stop",
|
|
702
|
+
actorId,
|
|
703
|
+
finalState: result.newState,
|
|
704
|
+
timestamp,
|
|
705
|
+
}));
|
|
706
|
+
return true;
|
|
644
707
|
}
|
|
708
|
+
}
|
|
645
709
|
|
|
646
|
-
|
|
647
|
-
|
|
710
|
+
return false;
|
|
711
|
+
});
|
|
648
712
|
|
|
649
713
|
/**
|
|
650
714
|
* Run spawn effects with actor-specific inspection and tracing.
|
|
651
715
|
* Wraps the core runSpawnEffects with inspection events and spans.
|
|
652
716
|
* @internal
|
|
653
717
|
*/
|
|
654
|
-
const runSpawnEffectsWithInspection = <
|
|
718
|
+
const runSpawnEffectsWithInspection = Effect.fn("effect-machine.actor.spawnEffects")(function* <
|
|
655
719
|
S extends { readonly _tag: string },
|
|
656
720
|
E extends { readonly _tag: string },
|
|
657
721
|
R,
|
|
@@ -665,20 +729,33 @@ const runSpawnEffectsWithInspection = <
|
|
|
665
729
|
stateScope: Scope.CloseableScope,
|
|
666
730
|
actorId: string,
|
|
667
731
|
inspector?: Inspector<S, E>,
|
|
668
|
-
)
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
732
|
+
) {
|
|
733
|
+
// Emit inspection event before running effects
|
|
734
|
+
yield* emitWithTimestamp(inspector, (timestamp) => ({
|
|
735
|
+
type: "@machine.effect",
|
|
736
|
+
actorId,
|
|
737
|
+
effectType: "spawn",
|
|
738
|
+
state,
|
|
739
|
+
timestamp,
|
|
740
|
+
}));
|
|
741
|
+
|
|
742
|
+
// Use shared core
|
|
743
|
+
const onError =
|
|
744
|
+
inspector === undefined
|
|
745
|
+
? undefined
|
|
746
|
+
: (info: ProcessEventError<S, E>) =>
|
|
747
|
+
emitWithTimestamp(inspector, (timestamp) => ({
|
|
748
|
+
type: "@machine.error",
|
|
749
|
+
actorId,
|
|
750
|
+
phase: info.phase,
|
|
751
|
+
state: info.state,
|
|
752
|
+
event: info.event,
|
|
753
|
+
error: Cause.pretty(info.cause),
|
|
754
|
+
timestamp,
|
|
755
|
+
}));
|
|
678
756
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
});
|
|
757
|
+
yield* runSpawnEffects(machine, state, event, self, stateScope, onError);
|
|
758
|
+
});
|
|
682
759
|
|
|
683
760
|
// ============================================================================
|
|
684
761
|
// ActorSystem Implementation
|
|
@@ -687,85 +764,102 @@ const runSpawnEffectsWithInspection = <
|
|
|
687
764
|
/**
|
|
688
765
|
* Internal implementation
|
|
689
766
|
*/
|
|
690
|
-
const make = Effect.
|
|
767
|
+
const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
691
768
|
// MutableHashMap for O(1) spawn/stop/get operations
|
|
692
769
|
const actors = MutableHashMap.empty<string, ActorRef<AnyState, unknown>>();
|
|
770
|
+
const spawnGate = yield* Effect.makeSemaphore(1);
|
|
771
|
+
const withSpawnGate = spawnGate.withPermits(1);
|
|
693
772
|
|
|
694
773
|
/** Check for duplicate ID, register actor, add cleanup finalizer */
|
|
695
|
-
const registerActor =
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
//
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
774
|
+
const registerActor = Effect.fn("effect-machine.actorSystem.register")(function* <
|
|
775
|
+
T extends { stop: Effect.Effect<void> },
|
|
776
|
+
>(id: string, actor: T) {
|
|
777
|
+
// Check if actor already exists
|
|
778
|
+
if (MutableHashMap.has(actors, id)) {
|
|
779
|
+
// Stop the newly created actor to avoid leaks
|
|
780
|
+
yield* actor.stop;
|
|
781
|
+
return yield* new DuplicateActorError({ actorId: id });
|
|
782
|
+
}
|
|
704
783
|
|
|
705
|
-
|
|
706
|
-
|
|
784
|
+
// Register it - O(1)
|
|
785
|
+
MutableHashMap.set(actors, id, actor as unknown as ActorRef<AnyState, unknown>);
|
|
707
786
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
787
|
+
// Register cleanup on scope finalization
|
|
788
|
+
yield* Effect.addFinalizer(
|
|
789
|
+
Effect.fn("effect-machine.actorSystem.register.finalizer")(function* () {
|
|
790
|
+
yield* actor.stop;
|
|
791
|
+
MutableHashMap.remove(actors, id);
|
|
792
|
+
}),
|
|
793
|
+
);
|
|
715
794
|
|
|
716
|
-
|
|
717
|
-
|
|
795
|
+
return actor;
|
|
796
|
+
});
|
|
718
797
|
|
|
719
|
-
const spawnRegular = <
|
|
798
|
+
const spawnRegular = Effect.fn("effect-machine.actorSystem.spawnRegular")(function* <
|
|
720
799
|
S extends { readonly _tag: string },
|
|
721
800
|
E extends { readonly _tag: string },
|
|
722
801
|
R,
|
|
723
802
|
GD extends GuardsDef = Record<string, never>,
|
|
724
803
|
EFD extends EffectsDef = Record<string, never>,
|
|
725
|
-
>(
|
|
726
|
-
id
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
804
|
+
>(id: string, machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>) {
|
|
805
|
+
if (MutableHashMap.has(actors, id)) {
|
|
806
|
+
return yield* new DuplicateActorError({ actorId: id });
|
|
807
|
+
}
|
|
808
|
+
// Create and register the actor
|
|
809
|
+
const actor = yield* createActor(id, machine);
|
|
810
|
+
return yield* registerActor(id, actor);
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
const spawnPersistent = Effect.fn("effect-machine.actorSystem.spawnPersistent")(function* <
|
|
814
|
+
S extends { readonly _tag: string },
|
|
815
|
+
E extends { readonly _tag: string },
|
|
816
|
+
R,
|
|
817
|
+
>(id: string, persistentMachine: PersistentMachine<S, E, R>) {
|
|
818
|
+
if (MutableHashMap.has(actors, id)) {
|
|
819
|
+
return yield* new DuplicateActorError({ actorId: id });
|
|
820
|
+
}
|
|
821
|
+
const adapter = yield* PersistenceAdapterTag;
|
|
822
|
+
|
|
823
|
+
// Try to load existing snapshot
|
|
824
|
+
const maybeSnapshot = yield* adapter.loadSnapshot(
|
|
825
|
+
id,
|
|
826
|
+
persistentMachine.persistence.stateSchema,
|
|
827
|
+
);
|
|
828
|
+
|
|
829
|
+
// Load events after snapshot (or all events if no snapshot)
|
|
830
|
+
const events = yield* adapter.loadEvents(
|
|
831
|
+
id,
|
|
832
|
+
persistentMachine.persistence.eventSchema,
|
|
833
|
+
Option.isSome(maybeSnapshot) ? maybeSnapshot.value.version : undefined,
|
|
834
|
+
);
|
|
835
|
+
|
|
836
|
+
// Create and register the persistent actor
|
|
837
|
+
const actor = yield* createPersistentActor(id, persistentMachine, maybeSnapshot, events);
|
|
838
|
+
return yield* registerActor(id, actor);
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
const spawnImpl = Effect.fn("effect-machine.actorSystem.spawn")(function* <
|
|
736
842
|
S extends { readonly _tag: string },
|
|
737
843
|
E extends { readonly _tag: string },
|
|
738
844
|
R,
|
|
845
|
+
GD extends GuardsDef = Record<string, never>,
|
|
846
|
+
EFD extends EffectsDef = Record<string, never>,
|
|
739
847
|
>(
|
|
740
848
|
id: string,
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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
|
-
});
|
|
849
|
+
machine:
|
|
850
|
+
| Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>
|
|
851
|
+
| PersistentMachine<S, E, R>,
|
|
852
|
+
) {
|
|
853
|
+
if (isPersistentMachine(machine)) {
|
|
854
|
+
// TypeScript can't narrow union with invariant generic params
|
|
855
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
856
|
+
return yield* spawnPersistent(id, machine as PersistentMachine<S, E, R>);
|
|
857
|
+
}
|
|
858
|
+
return yield* spawnRegular(
|
|
859
|
+
id,
|
|
860
|
+
machine as Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>,
|
|
861
|
+
);
|
|
862
|
+
});
|
|
769
863
|
|
|
770
864
|
// Type-safe overloaded spawn implementation
|
|
771
865
|
function spawn<
|
|
@@ -777,13 +871,13 @@ const make = Effect.sync(() => {
|
|
|
777
871
|
>(
|
|
778
872
|
id: string,
|
|
779
873
|
machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>,
|
|
780
|
-
): Effect.Effect<ActorRef<S, E>, DuplicateActorError, R | Scope.Scope>;
|
|
874
|
+
): Effect.Effect<ActorRef<S, E>, DuplicateActorError | UnprovidedSlotsError, R | Scope.Scope>;
|
|
781
875
|
function spawn<S extends { readonly _tag: string }, E extends { readonly _tag: string }, R>(
|
|
782
876
|
id: string,
|
|
783
877
|
machine: PersistentMachine<S, E, R>,
|
|
784
878
|
): Effect.Effect<
|
|
785
|
-
PersistentActorRef<S, E>,
|
|
786
|
-
PersistenceError | VersionConflictError | DuplicateActorError,
|
|
879
|
+
PersistentActorRef<S, E, R>,
|
|
880
|
+
PersistenceError | VersionConflictError | DuplicateActorError | UnprovidedSlotsError,
|
|
787
881
|
R | Scope.Scope | PersistenceAdapterTag
|
|
788
882
|
>;
|
|
789
883
|
function spawn<
|
|
@@ -798,140 +892,135 @@ const make = Effect.sync(() => {
|
|
|
798
892
|
| Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>
|
|
799
893
|
| PersistentMachine<S, E, R>,
|
|
800
894
|
):
|
|
801
|
-
| Effect.Effect<ActorRef<S, E>, DuplicateActorError, R | Scope.Scope>
|
|
895
|
+
| Effect.Effect<ActorRef<S, E>, DuplicateActorError | UnprovidedSlotsError, R | Scope.Scope>
|
|
802
896
|
| Effect.Effect<
|
|
803
|
-
PersistentActorRef<S, E>,
|
|
804
|
-
PersistenceError | VersionConflictError | DuplicateActorError,
|
|
897
|
+
PersistentActorRef<S, E, R>,
|
|
898
|
+
PersistenceError | VersionConflictError | DuplicateActorError | UnprovidedSlotsError,
|
|
805
899
|
R | Scope.Scope | PersistenceAdapterTag
|
|
806
900
|
> {
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
machine as Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>,
|
|
815
|
-
);
|
|
901
|
+
return withSpawnGate(spawnImpl(id, machine)) as
|
|
902
|
+
| Effect.Effect<ActorRef<S, E>, DuplicateActorError | UnprovidedSlotsError, R | Scope.Scope>
|
|
903
|
+
| Effect.Effect<
|
|
904
|
+
PersistentActorRef<S, E, R>,
|
|
905
|
+
PersistenceError | VersionConflictError | DuplicateActorError | UnprovidedSlotsError,
|
|
906
|
+
R | Scope.Scope | PersistenceAdapterTag
|
|
907
|
+
>;
|
|
816
908
|
}
|
|
817
909
|
|
|
910
|
+
const restoreImpl = Effect.fn("effect-machine.actorSystem.restore")(function* <
|
|
911
|
+
S extends { readonly _tag: string },
|
|
912
|
+
E extends { readonly _tag: string },
|
|
913
|
+
R,
|
|
914
|
+
>(id: string, persistentMachine: PersistentMachine<S, E, R>) {
|
|
915
|
+
// Try to restore from persistence
|
|
916
|
+
const maybeActor = yield* restorePersistentActor(id, persistentMachine);
|
|
917
|
+
|
|
918
|
+
if (Option.isSome(maybeActor)) {
|
|
919
|
+
yield* registerActor(id, maybeActor.value);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
return maybeActor;
|
|
923
|
+
});
|
|
818
924
|
const restore = <S extends { readonly _tag: string }, E extends { readonly _tag: string }, R>(
|
|
819
925
|
id: string,
|
|
820
926
|
persistentMachine: PersistentMachine<S, E, R>,
|
|
821
|
-
)
|
|
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);
|
|
927
|
+
) => withSpawnGate(restoreImpl(id, persistentMachine));
|
|
829
928
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
return maybeActor;
|
|
835
|
-
});
|
|
929
|
+
const get = Effect.fn("effect-machine.actorSystem.get")(function* (id: string) {
|
|
930
|
+
return yield* Effect.sync(() => MutableHashMap.get(actors, id));
|
|
931
|
+
});
|
|
836
932
|
|
|
837
|
-
const
|
|
838
|
-
|
|
933
|
+
const stop = Effect.fn("effect-machine.actorSystem.stop")(function* (id: string) {
|
|
934
|
+
const maybeActor = MutableHashMap.get(actors, id);
|
|
935
|
+
if (Option.isNone(maybeActor)) {
|
|
936
|
+
return false;
|
|
937
|
+
}
|
|
839
938
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
return false;
|
|
845
|
-
}
|
|
939
|
+
yield* maybeActor.value.stop;
|
|
940
|
+
MutableHashMap.remove(actors, id);
|
|
941
|
+
return true;
|
|
942
|
+
});
|
|
846
943
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
944
|
+
const listPersisted = Effect.fn("effect-machine.actorSystem.listPersisted")(function* () {
|
|
945
|
+
const adapter = yield* PersistenceAdapterTag;
|
|
946
|
+
if (adapter.listActors === undefined) {
|
|
947
|
+
return [];
|
|
948
|
+
}
|
|
949
|
+
return yield* adapter.listActors();
|
|
950
|
+
});
|
|
851
951
|
|
|
852
|
-
const
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
>
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
952
|
+
const restoreMany = Effect.fn("effect-machine.actorSystem.restoreMany")(function* <
|
|
953
|
+
S extends { readonly _tag: string },
|
|
954
|
+
E extends { readonly _tag: string },
|
|
955
|
+
R,
|
|
956
|
+
>(ids: ReadonlyArray<string>, persistentMachine: PersistentMachine<S, E, R>) {
|
|
957
|
+
const restored: PersistentActorRef<S, E, R>[] = [];
|
|
958
|
+
const failed: {
|
|
959
|
+
id: string;
|
|
960
|
+
error: PersistenceError | DuplicateActorError | UnprovidedSlotsError;
|
|
961
|
+
}[] = [];
|
|
962
|
+
|
|
963
|
+
for (const id of ids) {
|
|
964
|
+
// Skip if already running
|
|
965
|
+
if (MutableHashMap.has(actors, id)) {
|
|
966
|
+
continue;
|
|
861
967
|
}
|
|
862
|
-
return yield* adapter.listActors();
|
|
863
|
-
});
|
|
864
968
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
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
|
-
}
|
|
969
|
+
const result = yield* Effect.either(restore(id, persistentMachine));
|
|
970
|
+
if (result._tag === "Left") {
|
|
971
|
+
failed.push({ id, error: result.left });
|
|
972
|
+
} else if (Option.isSome(result.right)) {
|
|
973
|
+
restored.push(result.right.value);
|
|
974
|
+
} else {
|
|
975
|
+
// No persisted state for this ID
|
|
976
|
+
failed.push({
|
|
977
|
+
id,
|
|
978
|
+
error: new PersistenceErrorClass({
|
|
979
|
+
operation: "restore",
|
|
980
|
+
actorId: id,
|
|
981
|
+
message: "No persisted state found",
|
|
982
|
+
}),
|
|
983
|
+
});
|
|
895
984
|
}
|
|
985
|
+
}
|
|
896
986
|
|
|
897
|
-
|
|
898
|
-
|
|
987
|
+
return { restored, failed };
|
|
988
|
+
});
|
|
899
989
|
|
|
900
|
-
const restoreAll = <
|
|
990
|
+
const restoreAll = Effect.fn("effect-machine.actorSystem.restoreAll")(function* <
|
|
991
|
+
S extends { readonly _tag: string },
|
|
992
|
+
E extends { readonly _tag: string },
|
|
993
|
+
R,
|
|
994
|
+
>(
|
|
901
995
|
persistentMachine: PersistentMachine<S, E, R>,
|
|
902
996
|
options?: { filter?: (meta: ActorMetadata) => boolean },
|
|
903
|
-
)
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
Effect.gen(function* () {
|
|
909
|
-
const adapter = yield* PersistenceAdapterTag;
|
|
910
|
-
if (adapter.listActors === undefined) {
|
|
911
|
-
return { restored: [], failed: [] };
|
|
912
|
-
}
|
|
997
|
+
) {
|
|
998
|
+
const adapter = yield* PersistenceAdapterTag;
|
|
999
|
+
if (adapter.listActors === undefined) {
|
|
1000
|
+
return { restored: [], failed: [] };
|
|
1001
|
+
}
|
|
913
1002
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1003
|
+
// Require explicit machineType to prevent cross-machine restores
|
|
1004
|
+
const machineType = persistentMachine.persistence.machineType;
|
|
1005
|
+
if (machineType === undefined) {
|
|
1006
|
+
return yield* new PersistenceErrorClass({
|
|
1007
|
+
operation: "restoreAll",
|
|
1008
|
+
actorId: "*",
|
|
1009
|
+
message: "restoreAll requires explicit machineType in persistence config",
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
923
1012
|
|
|
924
|
-
|
|
1013
|
+
const allMetadata = yield* adapter.listActors();
|
|
925
1014
|
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1015
|
+
// Filter by machineType and optional user filter
|
|
1016
|
+
let filtered = allMetadata.filter((meta) => meta.machineType === machineType);
|
|
1017
|
+
if (options?.filter !== undefined) {
|
|
1018
|
+
filtered = filtered.filter(options.filter);
|
|
1019
|
+
}
|
|
931
1020
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
1021
|
+
const ids = filtered.map((meta) => meta.id);
|
|
1022
|
+
return yield* restoreMany(ids, persistentMachine);
|
|
1023
|
+
});
|
|
935
1024
|
|
|
936
1025
|
return ActorSystem.of({ spawn, restore, get, stop, listPersisted, restoreMany, restoreAll });
|
|
937
1026
|
});
|
|
@@ -939,4 +1028,4 @@ const make = Effect.sync(() => {
|
|
|
939
1028
|
/**
|
|
940
1029
|
* Default ActorSystem layer
|
|
941
1030
|
*/
|
|
942
|
-
export const Default = Layer.effect(ActorSystem, make);
|
|
1031
|
+
export const Default = Layer.effect(ActorSystem, make());
|