effect-machine 0.3.1 → 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.
Files changed (61) hide show
  1. package/dist/_virtual/_rolldown/runtime.js +18 -0
  2. package/dist/actor.d.ts +251 -0
  3. package/dist/actor.js +385 -0
  4. package/dist/cluster/entity-machine.d.ts +90 -0
  5. package/dist/cluster/entity-machine.js +74 -0
  6. package/dist/cluster/index.d.ts +3 -0
  7. package/dist/cluster/index.js +4 -0
  8. package/dist/cluster/to-entity.d.ts +64 -0
  9. package/dist/cluster/to-entity.js +53 -0
  10. package/dist/errors.d.ts +61 -0
  11. package/dist/errors.js +38 -0
  12. package/dist/index.d.ts +13 -0
  13. package/dist/index.js +14 -0
  14. package/dist/inspection.d.ts +125 -0
  15. package/dist/inspection.js +50 -0
  16. package/dist/internal/brands.d.ts +40 -0
  17. package/dist/internal/brands.js +0 -0
  18. package/dist/internal/inspection.d.ts +11 -0
  19. package/dist/internal/inspection.js +15 -0
  20. package/dist/internal/transition.d.ts +159 -0
  21. package/dist/internal/transition.js +235 -0
  22. package/dist/internal/utils.d.ts +52 -0
  23. package/dist/internal/utils.js +31 -0
  24. package/dist/machine.d.ts +271 -0
  25. package/dist/machine.js +317 -0
  26. package/{src/persistence/adapter.ts → dist/persistence/adapter.d.ts} +40 -72
  27. package/dist/persistence/adapter.js +27 -0
  28. package/dist/persistence/adapters/in-memory.d.ts +32 -0
  29. package/dist/persistence/adapters/in-memory.js +176 -0
  30. package/dist/persistence/index.d.ts +5 -0
  31. package/dist/persistence/index.js +6 -0
  32. package/dist/persistence/persistent-actor.d.ts +50 -0
  33. package/dist/persistence/persistent-actor.js +348 -0
  34. package/{src/persistence/persistent-machine.ts → dist/persistence/persistent-machine.d.ts} +28 -54
  35. package/dist/persistence/persistent-machine.js +24 -0
  36. package/dist/schema.d.ts +141 -0
  37. package/dist/schema.js +165 -0
  38. package/dist/slot.d.ts +128 -0
  39. package/dist/slot.js +99 -0
  40. package/dist/testing.d.ts +142 -0
  41. package/dist/testing.js +131 -0
  42. package/package.json +18 -7
  43. package/src/actor.ts +0 -1058
  44. package/src/cluster/entity-machine.ts +0 -201
  45. package/src/cluster/index.ts +0 -43
  46. package/src/cluster/to-entity.ts +0 -99
  47. package/src/errors.ts +0 -64
  48. package/src/index.ts +0 -105
  49. package/src/inspection.ts +0 -178
  50. package/src/internal/brands.ts +0 -51
  51. package/src/internal/inspection.ts +0 -18
  52. package/src/internal/transition.ts +0 -489
  53. package/src/internal/utils.ts +0 -80
  54. package/src/machine.ts +0 -836
  55. package/src/persistence/adapters/in-memory.ts +0 -294
  56. package/src/persistence/index.ts +0 -24
  57. package/src/persistence/persistent-actor.ts +0 -791
  58. package/src/schema.ts +0 -362
  59. package/src/slot.ts +0 -281
  60. package/src/testing.ts +0 -284
  61. package/tsconfig.json +0 -65
@@ -1,791 +0,0 @@
1
- // @effect-diagnostics missingEffectContext:off
2
- // @effect-diagnostics anyUnknownInErrorContext:off
3
-
4
- import {
5
- Clock,
6
- Cause,
7
- Effect,
8
- Exit,
9
- Fiber,
10
- Option,
11
- Queue,
12
- Ref,
13
- Schedule,
14
- Scope,
15
- SubscriptionRef,
16
- } from "effect";
17
-
18
- import type { ActorRef, Listeners } from "../actor.js";
19
- import { buildActorRefCore, notifyListeners } from "../actor.js";
20
- import type { MachineRef, Machine } from "../machine.js";
21
- import type { Inspector } from "../inspection.js";
22
- import { Inspector as InspectorTag } from "../inspection.js";
23
- import {
24
- processEventCore,
25
- resolveTransition,
26
- runSpawnEffects,
27
- runTransitionHandler,
28
- } from "../internal/transition.js";
29
- import type { ProcessEventError } from "../internal/transition.js";
30
- import type { GuardsDef, EffectsDef } from "../slot.js";
31
- import { INTERNAL_INIT_EVENT } from "../internal/utils.js";
32
- import { emitWithTimestamp } from "../internal/inspection.js";
33
-
34
- import type {
35
- ActorMetadata,
36
- PersistedEvent,
37
- PersistenceAdapter,
38
- PersistenceError,
39
- Snapshot,
40
- VersionConflictError,
41
- } from "./adapter.js";
42
- import { PersistenceAdapterTag } from "./adapter.js";
43
- import type { PersistentMachine } from "./persistent-machine.js";
44
-
45
- /**
46
- * Extended ActorRef with persistence capabilities
47
- */
48
- export interface PersistentActorRef<
49
- S extends { readonly _tag: string },
50
- E extends { readonly _tag: string },
51
- R = never,
52
- > extends ActorRef<S, E> {
53
- /**
54
- * Force an immediate snapshot save
55
- */
56
- readonly persist: Effect.Effect<void, PersistenceError | VersionConflictError>;
57
-
58
- /**
59
- * Get the current persistence version
60
- */
61
- readonly version: Effect.Effect<number>;
62
-
63
- /**
64
- * Replay events to restore actor to a specific version.
65
- * Note: This only computes state; does not re-run transition effects.
66
- */
67
- readonly replayTo: (version: number) => Effect.Effect<void, PersistenceError, R>;
68
- }
69
-
70
- /** Get current time in milliseconds using Effect Clock */
71
- const now = Clock.currentTimeMillis;
72
-
73
- /**
74
- * Replay persisted events to compute state.
75
- * Supports async handlers - used for initial restore.
76
- * @internal
77
- */
78
- const replayEvents = Effect.fn("effect-machine.persistentActor.replayEvents")(function* <
79
- S extends { readonly _tag: string },
80
- E extends { readonly _tag: string },
81
- R,
82
- GD extends GuardsDef = Record<string, never>,
83
- EFD extends EffectsDef = Record<string, never>,
84
- >(
85
- machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>,
86
- startState: S,
87
- events: ReadonlyArray<PersistedEvent<E>>,
88
- self: MachineRef<E>,
89
- stopVersion?: number,
90
- ) {
91
- let state = startState;
92
- let version = 0;
93
-
94
- for (const persistedEvent of events) {
95
- if (stopVersion !== undefined && persistedEvent.version > stopVersion) break;
96
-
97
- const transition = resolveTransition(machine, state, persistedEvent.event);
98
- if (transition !== undefined) {
99
- state = yield* runTransitionHandler(machine, transition, state, persistedEvent.event, self);
100
- }
101
- version = persistedEvent.version;
102
- }
103
-
104
- return { state, version };
105
- });
106
-
107
- /**
108
- * Build PersistentActorRef with all methods
109
- */
110
- const buildPersistentActorRef = <
111
- S extends { readonly _tag: string },
112
- E extends { readonly _tag: string },
113
- R,
114
- GD extends GuardsDef = Record<string, never>,
115
- EFD extends EffectsDef = Record<string, never>,
116
- >(
117
- id: string,
118
- persistentMachine: PersistentMachine<S, E, R>,
119
- stateRef: SubscriptionRef.SubscriptionRef<S>,
120
- versionRef: Ref.Ref<number>,
121
- eventQueue: Queue.Queue<E>,
122
- stoppedRef: Ref.Ref<boolean>,
123
- listeners: Listeners<S>,
124
- stop: Effect.Effect<void>,
125
- adapter: PersistenceAdapter,
126
- ): PersistentActorRef<S, E, R> => {
127
- const { machine, persistence } = persistentMachine;
128
- const typedMachine = machine as unknown as Machine<
129
- S,
130
- E,
131
- R,
132
- Record<string, never>,
133
- Record<string, never>,
134
- GD,
135
- EFD
136
- >;
137
-
138
- const persist = Effect.gen(function* () {
139
- const state = yield* SubscriptionRef.get(stateRef);
140
- const version = yield* Ref.get(versionRef);
141
- const timestamp = yield* now;
142
- const snapshot: Snapshot<S> = {
143
- state,
144
- version,
145
- timestamp,
146
- };
147
- yield* adapter.saveSnapshot(id, snapshot, persistence.stateSchema);
148
- }).pipe(Effect.withSpan("effect-machine.persistentActor.persist"));
149
-
150
- const version = Ref.get(versionRef).pipe(
151
- Effect.withSpan("effect-machine.persistentActor.version"),
152
- );
153
-
154
- // Replay only computes state - doesn't run spawn effects
155
- const replayTo = Effect.fn("effect-machine.persistentActor.replayTo")(function* (
156
- targetVersion: number,
157
- ) {
158
- const currentVersion = yield* Ref.get(versionRef);
159
- if (targetVersion <= currentVersion) {
160
- const maybeSnapshot = yield* adapter.loadSnapshot(id, persistence.stateSchema);
161
- if (Option.isSome(maybeSnapshot)) {
162
- const snapshot = maybeSnapshot.value;
163
- if (snapshot.version <= targetVersion) {
164
- const events = yield* adapter.loadEvents(id, persistence.eventSchema, snapshot.version);
165
- const dummySelf: MachineRef<E> = {
166
- send: Effect.fn("effect-machine.persistentActor.replay.send")(
167
- (_event: E) => Effect.void,
168
- ),
169
- };
170
-
171
- const result = yield* replayEvents(
172
- typedMachine,
173
- snapshot.state,
174
- events,
175
- dummySelf,
176
- targetVersion,
177
- );
178
-
179
- yield* SubscriptionRef.set(stateRef, result.state);
180
- yield* Ref.set(versionRef, result.version);
181
- notifyListeners(listeners, result.state);
182
- }
183
- } else {
184
- // No snapshot - replay from initial state if events exist
185
- const events = yield* adapter.loadEvents(id, persistence.eventSchema);
186
- if (events.length > 0) {
187
- const dummySelf: MachineRef<E> = {
188
- send: Effect.fn("effect-machine.persistentActor.replay.send")(
189
- (_event: E) => Effect.void,
190
- ),
191
- };
192
- const result = yield* replayEvents(
193
- typedMachine,
194
- typedMachine.initial,
195
- events,
196
- dummySelf,
197
- targetVersion,
198
- );
199
- yield* SubscriptionRef.set(stateRef, result.state);
200
- yield* Ref.set(versionRef, result.version);
201
- notifyListeners(listeners, result.state);
202
- }
203
- }
204
- }
205
- });
206
-
207
- const core = buildActorRefCore(
208
- id,
209
- typedMachine,
210
- stateRef,
211
- eventQueue,
212
- stoppedRef,
213
- listeners,
214
- stop,
215
- );
216
-
217
- return {
218
- ...core,
219
- persist,
220
- version,
221
- replayTo,
222
- };
223
- };
224
-
225
- /**
226
- * Create a persistent actor from a PersistentMachine.
227
- * Restores from existing snapshot if available, otherwise starts fresh.
228
- */
229
- export const createPersistentActor = Effect.fn("effect-machine.persistentActor.spawn")(function* <
230
- S extends { readonly _tag: string },
231
- E extends { readonly _tag: string },
232
- R,
233
- GD extends GuardsDef = Record<string, never>,
234
- EFD extends EffectsDef = Record<string, never>,
235
- >(
236
- id: string,
237
- persistentMachine: PersistentMachine<S, E, R>,
238
- initialSnapshot: Option.Option<Snapshot<S>>,
239
- initialEvents: ReadonlyArray<PersistedEvent<E>>,
240
- ) {
241
- yield* Effect.annotateCurrentSpan("effect_machine.actor.id", id);
242
- const adapter = yield* PersistenceAdapterTag;
243
- const { machine, persistence } = persistentMachine;
244
- const typedMachine = machine as unknown as Machine<
245
- S,
246
- E,
247
- R,
248
- Record<string, never>,
249
- Record<string, never>,
250
- GD,
251
- EFD
252
- >;
253
-
254
- // Get optional inspector from context
255
- const inspector = Option.getOrUndefined(yield* Effect.serviceOption(InspectorTag)) as
256
- | Inspector<S, E>
257
- | undefined;
258
-
259
- // Create self reference for sending events
260
- const eventQueue = yield* Queue.unbounded<E>();
261
- const stoppedRef = yield* Ref.make(false);
262
- const self: MachineRef<E> = {
263
- send: Effect.fn("effect-machine.persistentActor.self.send")(function* (event: E) {
264
- const stopped = yield* Ref.get(stoppedRef);
265
- if (stopped) {
266
- return;
267
- }
268
- yield* Queue.offer(eventQueue, event);
269
- }),
270
- };
271
-
272
- // Determine initial state and version
273
- let resolvedInitial: S;
274
- let initialVersion: number;
275
-
276
- if (Option.isSome(initialSnapshot)) {
277
- // Restore from snapshot + replay events
278
- const result = yield* replayEvents(
279
- typedMachine,
280
- initialSnapshot.value.state,
281
- initialEvents,
282
- self,
283
- );
284
- resolvedInitial = result.state;
285
- initialVersion = initialEvents.length > 0 ? result.version : initialSnapshot.value.version;
286
- } else if (initialEvents.length > 0) {
287
- // Restore from events only
288
- const result = yield* replayEvents(typedMachine, typedMachine.initial, initialEvents, self);
289
- resolvedInitial = result.state;
290
- initialVersion = result.version;
291
- } else {
292
- // Fresh start
293
- resolvedInitial = typedMachine.initial;
294
- initialVersion = 0;
295
- }
296
-
297
- yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", resolvedInitial._tag);
298
-
299
- // Initialize state refs
300
- const stateRef = yield* SubscriptionRef.make(resolvedInitial);
301
- const versionRef = yield* Ref.make(initialVersion);
302
- const listeners: Listeners<S> = new Set();
303
-
304
- // Track creation time for metadata - prefer existing metadata if restoring
305
- let createdAt: number;
306
- if (Option.isSome(initialSnapshot)) {
307
- // Restoring - try to get original createdAt from metadata
308
- const existingMeta =
309
- adapter.loadMetadata !== undefined
310
- ? yield* adapter.loadMetadata(id)
311
- : Option.none<ActorMetadata>();
312
- createdAt = Option.isSome(existingMeta)
313
- ? existingMeta.value.createdAt
314
- : initialSnapshot.value.timestamp; // fallback to snapshot time
315
- } else {
316
- createdAt = yield* now;
317
- }
318
-
319
- // Emit spawn event
320
- yield* emitWithTimestamp(inspector, (timestamp) => ({
321
- type: "@machine.spawn",
322
- actorId: id,
323
- initialState: resolvedInitial,
324
- timestamp,
325
- }));
326
-
327
- const snapshotEnabledRef = yield* Ref.make(true);
328
- const persistenceQueue = yield* Queue.unbounded<Effect.Effect<void, never>>();
329
- const persistenceFiber = yield* Effect.forkDaemon(persistenceWorker(persistenceQueue));
330
-
331
- // Save initial metadata
332
- yield* Queue.offer(
333
- persistenceQueue,
334
- saveMetadata(id, resolvedInitial, initialVersion, createdAt, persistence, adapter),
335
- );
336
-
337
- // Snapshot scheduler
338
- const snapshotQueue = yield* Queue.unbounded<{ state: S; version: number }>();
339
- const snapshotFiber = yield* Effect.forkDaemon(
340
- snapshotWorker(id, persistence, adapter, snapshotQueue, snapshotEnabledRef),
341
- );
342
-
343
- // Fork background effects (run for entire machine lifetime)
344
- const backgroundFibers: Fiber.Fiber<void, never>[] = [];
345
- const initEvent = { _tag: INTERNAL_INIT_EVENT } as E;
346
- const initCtx = { state: resolvedInitial, event: initEvent, self };
347
- const { effects: effectSlots } = typedMachine._slots;
348
-
349
- for (const bg of typedMachine.backgroundEffects) {
350
- const fiber = yield* Effect.forkDaemon(
351
- bg
352
- .handler({ state: resolvedInitial, event: initEvent, self, effects: effectSlots })
353
- .pipe(Effect.provideService(typedMachine.Context, initCtx)),
354
- );
355
- backgroundFibers.push(fiber);
356
- }
357
-
358
- // Create state scope for spawn effects
359
- const stateScopeRef: { current: Scope.CloseableScope } = {
360
- current: yield* Scope.make(),
361
- };
362
-
363
- // Run initial spawn effects
364
- yield* runSpawnEffectsWithInspection(
365
- typedMachine,
366
- resolvedInitial,
367
- initEvent,
368
- self,
369
- stateScopeRef.current,
370
- id,
371
- inspector,
372
- );
373
-
374
- // Check if initial state is final
375
- if (typedMachine.finalStates.has(resolvedInitial._tag)) {
376
- yield* Scope.close(stateScopeRef.current, Exit.void);
377
- yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
378
- yield* Fiber.interrupt(snapshotFiber);
379
- yield* Fiber.interrupt(persistenceFiber);
380
- yield* Ref.set(stoppedRef, true);
381
- yield* emitWithTimestamp(inspector, (timestamp) => ({
382
- type: "@machine.stop",
383
- actorId: id,
384
- finalState: resolvedInitial,
385
- timestamp,
386
- }));
387
- const stop = Ref.set(stoppedRef, true).pipe(
388
- Effect.withSpan("effect-machine.persistentActor.stop"),
389
- Effect.asVoid,
390
- );
391
- return buildPersistentActorRef(
392
- id,
393
- persistentMachine,
394
- stateRef,
395
- versionRef,
396
- eventQueue,
397
- stoppedRef,
398
- listeners,
399
- stop,
400
- adapter,
401
- );
402
- }
403
-
404
- // Start the persistent event loop
405
- const loopFiber = yield* Effect.forkDaemon(
406
- persistentEventLoop(
407
- id,
408
- persistentMachine,
409
- stateRef,
410
- versionRef,
411
- eventQueue,
412
- stoppedRef,
413
- self,
414
- listeners,
415
- adapter,
416
- createdAt,
417
- stateScopeRef,
418
- backgroundFibers,
419
- snapshotQueue,
420
- snapshotEnabledRef,
421
- persistenceQueue,
422
- snapshotFiber,
423
- persistenceFiber,
424
- inspector,
425
- ),
426
- );
427
-
428
- const stop = Effect.gen(function* () {
429
- const finalState = yield* SubscriptionRef.get(stateRef);
430
- yield* emitWithTimestamp(inspector, (timestamp) => ({
431
- type: "@machine.stop",
432
- actorId: id,
433
- finalState,
434
- timestamp,
435
- }));
436
- yield* Ref.set(stoppedRef, true);
437
- yield* Fiber.interrupt(loopFiber);
438
- yield* Scope.close(stateScopeRef.current, Exit.void);
439
- yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
440
- yield* Fiber.interrupt(snapshotFiber);
441
- yield* Fiber.interrupt(persistenceFiber);
442
- }).pipe(Effect.withSpan("effect-machine.persistentActor.stop"), Effect.asVoid);
443
-
444
- return buildPersistentActorRef(
445
- id,
446
- persistentMachine,
447
- stateRef,
448
- versionRef,
449
- eventQueue,
450
- stoppedRef,
451
- listeners,
452
- stop,
453
- adapter,
454
- );
455
- });
456
-
457
- /**
458
- * Main event loop for persistent actor
459
- */
460
- const persistentEventLoop = Effect.fn("effect-machine.persistentActor.eventLoop")(function* <
461
- S extends { readonly _tag: string },
462
- E extends { readonly _tag: string },
463
- R,
464
- GD extends GuardsDef = Record<string, never>,
465
- EFD extends EffectsDef = Record<string, never>,
466
- >(
467
- id: string,
468
- persistentMachine: PersistentMachine<S, E, R>,
469
- stateRef: SubscriptionRef.SubscriptionRef<S>,
470
- versionRef: Ref.Ref<number>,
471
- eventQueue: Queue.Queue<E>,
472
- stoppedRef: Ref.Ref<boolean>,
473
- self: MachineRef<E>,
474
- listeners: Listeners<S>,
475
- adapter: PersistenceAdapter,
476
- createdAt: number,
477
- stateScopeRef: { current: Scope.CloseableScope },
478
- backgroundFibers: ReadonlyArray<Fiber.Fiber<void, never>>,
479
- snapshotQueue: Queue.Queue<{ state: S; version: number }>,
480
- snapshotEnabledRef: Ref.Ref<boolean>,
481
- persistenceQueue: Queue.Queue<Effect.Effect<void, never>>,
482
- snapshotFiber: Fiber.Fiber<void, never>,
483
- persistenceFiber: Fiber.Fiber<void, never>,
484
- inspector?: Inspector<S, E>,
485
- ) {
486
- const { machine, persistence } = persistentMachine;
487
- const typedMachine = machine as unknown as Machine<
488
- S,
489
- E,
490
- R,
491
- Record<string, never>,
492
- Record<string, never>,
493
- GD,
494
- EFD
495
- >;
496
-
497
- const hooks =
498
- inspector === undefined
499
- ? undefined
500
- : {
501
- onSpawnEffect: (state: S) =>
502
- emitWithTimestamp(inspector, (timestamp) => ({
503
- type: "@machine.effect",
504
- actorId: id,
505
- effectType: "spawn",
506
- state,
507
- timestamp,
508
- })),
509
- onTransition: (from: S, to: S, ev: E) =>
510
- emitWithTimestamp(inspector, (timestamp) => ({
511
- type: "@machine.transition",
512
- actorId: id,
513
- fromState: from,
514
- toState: to,
515
- event: ev,
516
- timestamp,
517
- })),
518
- onError: (info: ProcessEventError<S, E>) =>
519
- emitWithTimestamp(inspector, (timestamp) => ({
520
- type: "@machine.error",
521
- actorId: id,
522
- phase: info.phase,
523
- state: info.state,
524
- event: info.event,
525
- error: Cause.pretty(info.cause),
526
- timestamp,
527
- })),
528
- };
529
-
530
- while (true) {
531
- const event = yield* Queue.take(eventQueue);
532
- const currentState = yield* SubscriptionRef.get(stateRef);
533
- const currentVersion = yield* Ref.get(versionRef);
534
-
535
- // Emit event received
536
- yield* emitWithTimestamp(inspector, (timestamp) => ({
537
- type: "@machine.event",
538
- actorId: id,
539
- state: currentState,
540
- event,
541
- timestamp,
542
- }));
543
-
544
- const result = yield* processEventCore(
545
- typedMachine,
546
- currentState,
547
- event,
548
- self,
549
- stateScopeRef,
550
- hooks,
551
- );
552
-
553
- if (!result.transitioned) {
554
- continue;
555
- }
556
-
557
- // Increment version
558
- const newVersion = currentVersion + 1;
559
- yield* Ref.set(versionRef, newVersion);
560
-
561
- // Update state and notify listeners
562
- yield* SubscriptionRef.set(stateRef, result.newState);
563
- notifyListeners(listeners, result.newState);
564
-
565
- // Journal event if enabled (async)
566
- if (persistence.journalEvents) {
567
- const timestamp = yield* now;
568
- const persistedEvent: PersistedEvent<E> = {
569
- event,
570
- version: newVersion,
571
- timestamp,
572
- };
573
- const journalTask = adapter.appendEvent(id, persistedEvent, persistence.eventSchema).pipe(
574
- Effect.catchAll((e) => Effect.logWarning(`Failed to journal event for actor ${id}`, e)),
575
- Effect.asVoid,
576
- );
577
- yield* Queue.offer(persistenceQueue, journalTask);
578
- }
579
-
580
- // Save metadata (async)
581
- yield* Queue.offer(
582
- persistenceQueue,
583
- saveMetadata(id, result.newState, newVersion, createdAt, persistence, adapter),
584
- );
585
-
586
- // Schedule snapshot (non-blocking)
587
- if (yield* Ref.get(snapshotEnabledRef)) {
588
- yield* Queue.offer(snapshotQueue, { state: result.newState, version: newVersion });
589
- }
590
-
591
- // Check if final state reached
592
- if (result.lifecycleRan && result.isFinal) {
593
- yield* emitWithTimestamp(inspector, (timestamp) => ({
594
- type: "@machine.stop",
595
- actorId: id,
596
- finalState: result.newState,
597
- timestamp,
598
- }));
599
- yield* Ref.set(stoppedRef, true);
600
- yield* Scope.close(stateScopeRef.current, Exit.void);
601
- yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
602
- yield* Fiber.interrupt(snapshotFiber);
603
- yield* Fiber.interrupt(persistenceFiber);
604
- return;
605
- }
606
- }
607
- });
608
-
609
- /**
610
- * Run spawn effects with inspection and tracing.
611
- * @internal
612
- */
613
- const runSpawnEffectsWithInspection = Effect.fn("effect-machine.persistentActor.spawnEffects")(
614
- function* <
615
- S extends { readonly _tag: string },
616
- E extends { readonly _tag: string },
617
- R,
618
- GD extends GuardsDef,
619
- EFD extends EffectsDef,
620
- >(
621
- machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>,
622
- state: S,
623
- event: E,
624
- self: MachineRef<E>,
625
- stateScope: Scope.CloseableScope,
626
- actorId: string,
627
- inspector?: Inspector<S, E>,
628
- ) {
629
- yield* emitWithTimestamp(inspector, (timestamp) => ({
630
- type: "@machine.effect",
631
- actorId,
632
- effectType: "spawn",
633
- state,
634
- timestamp,
635
- }));
636
-
637
- const onError =
638
- inspector === undefined
639
- ? undefined
640
- : (info: ProcessEventError<S, E>) =>
641
- emitWithTimestamp(inspector, (timestamp) => ({
642
- type: "@machine.error",
643
- actorId,
644
- phase: info.phase,
645
- state: info.state,
646
- event: info.event,
647
- error: Cause.pretty(info.cause),
648
- timestamp,
649
- }));
650
-
651
- yield* runSpawnEffects(machine, state, event, self, stateScope, onError);
652
- },
653
- );
654
-
655
- /**
656
- * Persistence worker (journaling + metadata).
657
- */
658
- const persistenceWorker = Effect.fn("effect-machine.persistentActor.persistenceWorker")(function* (
659
- queue: Queue.Queue<Effect.Effect<void, never>>,
660
- ) {
661
- while (true) {
662
- const task = yield* Queue.take(queue);
663
- yield* task;
664
- }
665
- });
666
-
667
- /**
668
- * Snapshot scheduler worker (runs in background).
669
- */
670
- const snapshotWorker = Effect.fn("effect-machine.persistentActor.snapshotWorker")(function* <
671
- S extends { readonly _tag: string },
672
- E extends { readonly _tag: string },
673
- >(
674
- id: string,
675
- persistence: PersistentMachine<S, E, never>["persistence"],
676
- adapter: PersistenceAdapter,
677
- queue: Queue.Queue<{ state: S; version: number }>,
678
- enabledRef: Ref.Ref<boolean>,
679
- ) {
680
- const driver = yield* Schedule.driver(persistence.snapshotSchedule);
681
-
682
- while (true) {
683
- const { state, version } = yield* Queue.take(queue);
684
- if (!(yield* Ref.get(enabledRef))) {
685
- continue;
686
- }
687
- const shouldSnapshot = yield* driver.next(state).pipe(
688
- Effect.match({
689
- onFailure: () => false,
690
- onSuccess: () => true,
691
- }),
692
- );
693
- if (!shouldSnapshot) {
694
- yield* Ref.set(enabledRef, false);
695
- continue;
696
- }
697
-
698
- yield* saveSnapshot(id, state, version, persistence, adapter);
699
- }
700
- });
701
-
702
- /**
703
- * Save a snapshot after state transition.
704
- * Called by snapshot scheduler.
705
- */
706
- const saveSnapshot = Effect.fn("effect-machine.persistentActor.saveSnapshot")(function* <
707
- S extends { readonly _tag: string },
708
- E extends { readonly _tag: string },
709
- >(
710
- id: string,
711
- state: S,
712
- version: number,
713
- persistence: PersistentMachine<S, E, never>["persistence"],
714
- adapter: PersistenceAdapter,
715
- ) {
716
- const timestamp = yield* now;
717
- const snapshot: Snapshot<S> = {
718
- state,
719
- version,
720
- timestamp,
721
- };
722
- yield* adapter
723
- .saveSnapshot(id, snapshot, persistence.stateSchema)
724
- .pipe(Effect.catchAll((e) => Effect.logWarning(`Failed to save snapshot for actor ${id}`, e)));
725
- });
726
-
727
- /**
728
- * Save or update actor metadata if adapter supports registry.
729
- * Called on spawn and state transitions.
730
- */
731
- const saveMetadata = Effect.fn("effect-machine.persistentActor.saveMetadata")(function* <
732
- S extends { readonly _tag: string },
733
- E extends { readonly _tag: string },
734
- >(
735
- id: string,
736
- state: S,
737
- version: number,
738
- createdAt: number,
739
- persistence: PersistentMachine<S, E, never>["persistence"],
740
- adapter: PersistenceAdapter,
741
- ) {
742
- const save = adapter.saveMetadata;
743
- if (save === undefined) {
744
- return;
745
- }
746
- const lastActivityAt = yield* now;
747
- const metadata: ActorMetadata = {
748
- id,
749
- machineType: persistence.machineType ?? "unknown",
750
- createdAt,
751
- lastActivityAt,
752
- version,
753
- stateTag: state._tag,
754
- };
755
- yield* save(metadata).pipe(
756
- Effect.catchAll((e) => Effect.logWarning(`Failed to save metadata for actor ${id}`, e)),
757
- );
758
- });
759
-
760
- /**
761
- * Restore an actor from persistence.
762
- * Returns None if no persisted state exists.
763
- */
764
- export const restorePersistentActor = Effect.fn("effect-machine.persistentActor.restore")(
765
- function* <S extends { readonly _tag: string }, E extends { readonly _tag: string }, R>(
766
- id: string,
767
- persistentMachine: PersistentMachine<S, E, R>,
768
- ) {
769
- const adapter = yield* PersistenceAdapterTag;
770
- const { persistence } = persistentMachine;
771
-
772
- // Try to load snapshot
773
- const maybeSnapshot = yield* adapter.loadSnapshot(id, persistence.stateSchema);
774
-
775
- // Load events (after snapshot if present)
776
- const events = yield* adapter.loadEvents(
777
- id,
778
- persistence.eventSchema,
779
- Option.isSome(maybeSnapshot) ? maybeSnapshot.value.version : undefined,
780
- );
781
-
782
- if (Option.isNone(maybeSnapshot) && events.length === 0) {
783
- return Option.none();
784
- }
785
-
786
- // Create actor with restored state
787
- const actor = yield* createPersistentActor(id, persistentMachine, maybeSnapshot, events);
788
-
789
- return Option.some(actor);
790
- },
791
- );