effect-machine 0.10.0 → 0.12.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.
Files changed (81) hide show
  1. package/README.md +65 -62
  2. package/dist/actor.d.ts +35 -97
  3. package/dist/actor.js +69 -98
  4. package/dist/cluster/adapters/in-memory.d.ts +28 -0
  5. package/dist/cluster/adapters/in-memory.js +79 -0
  6. package/dist/cluster/entity-actor-ref.d.ts +56 -0
  7. package/dist/cluster/entity-actor-ref.js +33 -0
  8. package/dist/cluster/entity-machine.d.ts +31 -49
  9. package/dist/cluster/entity-machine.js +167 -52
  10. package/dist/cluster/index.d.ts +5 -2
  11. package/dist/cluster/index.js +4 -1
  12. package/dist/cluster/persistence.d.ts +49 -0
  13. package/dist/cluster/persistence.js +18 -0
  14. package/dist/cluster/to-entity.d.ts +9 -3
  15. package/dist/cluster/to-entity.js +16 -4
  16. package/dist/errors.d.ts +12 -1
  17. package/dist/errors.js +8 -1
  18. package/dist/index.d.ts +5 -8
  19. package/dist/index.js +1 -6
  20. package/dist/internal/brands.d.ts +14 -1
  21. package/dist/internal/runtime.d.ts +67 -0
  22. package/dist/internal/runtime.js +248 -0
  23. package/dist/internal/transition.d.ts +6 -1
  24. package/dist/internal/transition.js +16 -4
  25. package/dist/internal/utils.d.ts +42 -6
  26. package/dist/internal/utils.js +28 -6
  27. package/dist/machine.d.ts +55 -46
  28. package/dist/machine.js +74 -13
  29. package/dist/schema.d.ts +35 -34
  30. package/dist/schema.js +32 -3
  31. package/dist/testing.js +4 -2
  32. package/package.json +4 -4
  33. package/v3/dist/actor.d.ts +29 -96
  34. package/v3/dist/actor.js +52 -97
  35. package/v3/dist/cluster/adapters/in-memory.d.ts +15 -0
  36. package/v3/dist/cluster/adapters/in-memory.js +62 -0
  37. package/v3/dist/cluster/entity-actor-ref.d.ts +49 -0
  38. package/v3/dist/cluster/entity-actor-ref.js +19 -0
  39. package/v3/dist/cluster/entity-machine.d.ts +34 -49
  40. package/v3/dist/cluster/entity-machine.js +134 -50
  41. package/v3/dist/cluster/index.d.ts +5 -2
  42. package/v3/dist/cluster/index.js +4 -1
  43. package/v3/dist/cluster/persistence.d.ts +48 -0
  44. package/v3/dist/cluster/persistence.js +14 -0
  45. package/v3/dist/cluster/to-entity.d.ts +5 -2
  46. package/v3/dist/cluster/to-entity.js +12 -4
  47. package/v3/dist/errors.d.ts +16 -1
  48. package/v3/dist/errors.js +8 -1
  49. package/v3/dist/index.d.ts +5 -8
  50. package/v3/dist/index.js +1 -6
  51. package/v3/dist/internal/brands.d.ts +15 -1
  52. package/v3/dist/internal/runtime.d.ts +65 -0
  53. package/v3/dist/internal/runtime.js +236 -0
  54. package/v3/dist/internal/transition.d.ts +5 -0
  55. package/v3/dist/internal/transition.js +15 -3
  56. package/v3/dist/internal/utils.d.ts +42 -6
  57. package/v3/dist/internal/utils.js +28 -6
  58. package/v3/dist/machine.d.ts +48 -46
  59. package/v3/dist/machine.js +71 -13
  60. package/v3/dist/schema.d.ts +35 -34
  61. package/v3/dist/schema.js +29 -3
  62. package/dist/persistence/adapter.d.ts +0 -135
  63. package/dist/persistence/adapter.js +0 -25
  64. package/dist/persistence/adapters/in-memory.d.ts +0 -32
  65. package/dist/persistence/adapters/in-memory.js +0 -174
  66. package/dist/persistence/index.d.ts +0 -5
  67. package/dist/persistence/index.js +0 -5
  68. package/dist/persistence/persistent-actor.d.ts +0 -50
  69. package/dist/persistence/persistent-actor.js +0 -404
  70. package/dist/persistence/persistent-machine.d.ts +0 -105
  71. package/dist/persistence/persistent-machine.js +0 -22
  72. package/v3/dist/persistence/adapter.d.ts +0 -138
  73. package/v3/dist/persistence/adapter.js +0 -25
  74. package/v3/dist/persistence/adapters/in-memory.d.ts +0 -32
  75. package/v3/dist/persistence/adapters/in-memory.js +0 -174
  76. package/v3/dist/persistence/index.d.ts +0 -5
  77. package/v3/dist/persistence/index.js +0 -5
  78. package/v3/dist/persistence/persistent-actor.d.ts +0 -50
  79. package/v3/dist/persistence/persistent-actor.js +0 -404
  80. package/v3/dist/persistence/persistent-machine.d.ts +0 -105
  81. package/v3/dist/persistence/persistent-machine.js +0 -22
@@ -1,404 +0,0 @@
1
- import { Inspector } from "../inspection.js";
2
- import { INTERNAL_INIT_EVENT, stubSystem } from "../internal/utils.js";
3
- import { NoReplyError } from "../errors.js";
4
- import { emitWithTimestamp } from "../internal/inspection.js";
5
- import { processEventCore, resolveTransition, runSpawnEffects, runTransitionHandler, shouldPostpone } from "../internal/transition.js";
6
- import { PersistenceAdapterTag } from "./adapter.js";
7
- import { ActorSystem, buildActorRefCore, notifyListeners, settlePendingReplies } from "../actor.js";
8
- import { Cause, Clock, Deferred, Effect, Exit, Fiber, Option, Queue, Ref, Schedule, Scope, SubscriptionRef } from "effect";
9
- //#region src/persistence/persistent-actor.ts
10
- /** Get current time in milliseconds using Effect Clock */
11
- const now = Clock.currentTimeMillis;
12
- /**
13
- * Replay persisted events to compute state.
14
- * Supports async handlers - used for initial restore.
15
- * @internal
16
- */
17
- const replayEvents = Effect.fn("effect-machine.persistentActor.replayEvents")(function* (machine, startState, events, self, stopVersion) {
18
- let state = startState;
19
- let version = 0;
20
- for (const persistedEvent of events) {
21
- if (stopVersion !== void 0 && persistedEvent.version > stopVersion) break;
22
- const transition = resolveTransition(machine, state, persistedEvent.event);
23
- if (transition !== void 0) state = (yield* runTransitionHandler(machine, transition, state, persistedEvent.event, self, stubSystem, "restore")).newState;
24
- version = persistedEvent.version;
25
- }
26
- return {
27
- state,
28
- version
29
- };
30
- });
31
- /**
32
- * Build PersistentActorRef with all methods
33
- */
34
- const buildPersistentActorRef = (id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, listeners, stop, adapter, system, childrenMap, pendingReplies) => {
35
- const { machine, persistence } = persistentMachine;
36
- const typedMachine = machine;
37
- const persist = Effect.gen(function* () {
38
- const snapshot = {
39
- state: yield* SubscriptionRef.get(stateRef),
40
- version: yield* Ref.get(versionRef),
41
- timestamp: yield* now
42
- };
43
- yield* adapter.saveSnapshot(id, snapshot, persistence.stateSchema);
44
- }).pipe(Effect.withSpan("effect-machine.persistentActor.persist"));
45
- const version = Ref.get(versionRef).pipe(Effect.withSpan("effect-machine.persistentActor.version"));
46
- const replayTo = Effect.fn("effect-machine.persistentActor.replayTo")(function* (targetVersion) {
47
- if (targetVersion <= (yield* Ref.get(versionRef))) {
48
- const dummySend = Effect.fn("effect-machine.persistentActor.replay.send")((_event) => Effect.void);
49
- const dummySelf = {
50
- send: dummySend,
51
- cast: dummySend,
52
- spawn: () => Effect.die("spawn not supported in replay")
53
- };
54
- const maybeSnapshot = yield* adapter.loadSnapshot(id, persistence.stateSchema);
55
- if (Option.isSome(maybeSnapshot)) {
56
- const snapshot = maybeSnapshot.value;
57
- if (snapshot.version <= targetVersion) {
58
- const events = yield* adapter.loadEvents(id, persistence.eventSchema, snapshot.version);
59
- const result = yield* replayEvents(typedMachine, snapshot.state, events, dummySelf, targetVersion);
60
- yield* SubscriptionRef.set(stateRef, result.state);
61
- yield* Ref.set(versionRef, result.version);
62
- notifyListeners(listeners, result.state);
63
- }
64
- } else {
65
- const events = yield* adapter.loadEvents(id, persistence.eventSchema);
66
- if (events.length > 0) {
67
- const result = yield* replayEvents(typedMachine, typedMachine.initial, events, dummySelf, targetVersion);
68
- yield* SubscriptionRef.set(stateRef, result.state);
69
- yield* Ref.set(versionRef, result.version);
70
- notifyListeners(listeners, result.state);
71
- }
72
- }
73
- }
74
- });
75
- return {
76
- ...buildActorRefCore(id, typedMachine, stateRef, eventQueue, stoppedRef, listeners, stop, system, childrenMap, pendingReplies),
77
- persist,
78
- version,
79
- replayTo
80
- };
81
- };
82
- /**
83
- * Create a persistent actor from a PersistentMachine.
84
- * Restores from existing snapshot if available, otherwise starts fresh.
85
- */
86
- const createPersistentActor = Effect.fn("effect-machine.persistentActor.spawn")(function* (id, persistentMachine, initialSnapshot, initialEvents) {
87
- yield* Effect.annotateCurrentSpan("effect_machine.actor.id", id);
88
- const adapter = yield* PersistenceAdapterTag;
89
- const { machine, persistence } = persistentMachine;
90
- const typedMachine = machine;
91
- const existingSystem = yield* Effect.serviceOption(ActorSystem);
92
- if (Option.isNone(existingSystem)) return yield* Effect.die("PersistentActor requires ActorSystem in context");
93
- const system = existingSystem.value;
94
- const inspector = Option.getOrUndefined(yield* Effect.serviceOption(Inspector));
95
- const eventQueue = yield* Queue.unbounded();
96
- const stoppedRef = yield* Ref.make(false);
97
- const childrenMap = /* @__PURE__ */ new Map();
98
- const selfSend = Effect.fn("effect-machine.persistentActor.self.send")(function* (event) {
99
- if (yield* Ref.get(stoppedRef)) return;
100
- yield* Queue.offer(eventQueue, {
101
- _tag: "send",
102
- event
103
- });
104
- });
105
- const self = {
106
- send: selfSend,
107
- cast: selfSend,
108
- spawn: (childId, childMachine) => Effect.gen(function* () {
109
- const child = yield* system.spawn(childId, childMachine).pipe(Effect.provideService(ActorSystem, system));
110
- childrenMap.set(childId, child);
111
- const maybeScope = yield* Effect.serviceOption(Scope.Scope);
112
- if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, Effect.sync(() => {
113
- childrenMap.delete(childId);
114
- }));
115
- return child;
116
- })
117
- };
118
- let resolvedInitial;
119
- let initialVersion;
120
- if (Option.isSome(initialSnapshot)) {
121
- const result = yield* replayEvents(typedMachine, initialSnapshot.value.state, initialEvents, self);
122
- resolvedInitial = result.state;
123
- initialVersion = initialEvents.length > 0 ? result.version : initialSnapshot.value.version;
124
- } else if (initialEvents.length > 0) {
125
- const result = yield* replayEvents(typedMachine, typedMachine.initial, initialEvents, self);
126
- resolvedInitial = result.state;
127
- initialVersion = result.version;
128
- } else {
129
- resolvedInitial = typedMachine.initial;
130
- initialVersion = 0;
131
- }
132
- yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", resolvedInitial._tag);
133
- const stateRef = yield* SubscriptionRef.make(resolvedInitial);
134
- const versionRef = yield* Ref.make(initialVersion);
135
- const listeners = /* @__PURE__ */ new Set();
136
- let createdAt;
137
- if (Option.isSome(initialSnapshot)) {
138
- const existingMeta = adapter.loadMetadata !== void 0 ? yield* adapter.loadMetadata(id) : Option.none();
139
- createdAt = Option.isSome(existingMeta) ? existingMeta.value.createdAt : initialSnapshot.value.timestamp;
140
- } else createdAt = yield* now;
141
- yield* emitWithTimestamp(inspector, (timestamp) => ({
142
- type: "@machine.spawn",
143
- actorId: id,
144
- initialState: resolvedInitial,
145
- timestamp
146
- }));
147
- const snapshotEnabledRef = yield* Ref.make(true);
148
- const persistenceQueue = yield* Queue.unbounded();
149
- const persistenceFiber = yield* Effect.forkDaemon(persistenceWorker(persistenceQueue));
150
- yield* Queue.offer(persistenceQueue, saveMetadata(id, resolvedInitial, initialVersion, createdAt, persistence, adapter));
151
- const snapshotQueue = yield* Queue.unbounded();
152
- const snapshotFiber = yield* Effect.forkDaemon(snapshotWorker(id, persistence, adapter, snapshotQueue, snapshotEnabledRef));
153
- const backgroundFibers = [];
154
- const initEvent = { _tag: INTERNAL_INIT_EVENT };
155
- const initCtx = {
156
- actorId: id,
157
- state: resolvedInitial,
158
- event: initEvent,
159
- self,
160
- system
161
- };
162
- const { effects: effectSlots } = typedMachine._slots;
163
- for (const bg of typedMachine.backgroundEffects) {
164
- const fiber = yield* Effect.forkDaemon(bg.handler({
165
- actorId: id,
166
- state: resolvedInitial,
167
- event: initEvent,
168
- self,
169
- effects: effectSlots,
170
- system
171
- }).pipe(Effect.provideService(typedMachine.Context, initCtx)));
172
- backgroundFibers.push(fiber);
173
- }
174
- const stateScopeRef = { current: yield* Scope.make() };
175
- yield* runSpawnEffectsWithInspection(typedMachine, resolvedInitial, initEvent, self, stateScopeRef.current, id, inspector, system);
176
- if (typedMachine.finalStates.has(resolvedInitial._tag)) {
177
- yield* Scope.close(stateScopeRef.current, Exit.void);
178
- yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
179
- yield* Fiber.interrupt(snapshotFiber);
180
- yield* Fiber.interrupt(persistenceFiber);
181
- yield* Ref.set(stoppedRef, true);
182
- yield* emitWithTimestamp(inspector, (timestamp) => ({
183
- type: "@machine.stop",
184
- actorId: id,
185
- finalState: resolvedInitial,
186
- timestamp
187
- }));
188
- return buildPersistentActorRef(id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, listeners, Ref.set(stoppedRef, true).pipe(Effect.withSpan("effect-machine.persistentActor.stop"), Effect.asVoid), adapter, system, childrenMap, /* @__PURE__ */ new Set());
189
- }
190
- const pendingReplies = /* @__PURE__ */ new Set();
191
- const loopFiber = yield* Effect.forkDaemon(persistentEventLoop(id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, self, listeners, adapter, createdAt, stateScopeRef, backgroundFibers, snapshotQueue, snapshotEnabledRef, persistenceQueue, snapshotFiber, persistenceFiber, inspector, system, pendingReplies));
192
- return buildPersistentActorRef(id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, listeners, Effect.gen(function* () {
193
- const finalState = yield* SubscriptionRef.get(stateRef);
194
- yield* emitWithTimestamp(inspector, (timestamp) => ({
195
- type: "@machine.stop",
196
- actorId: id,
197
- finalState,
198
- timestamp
199
- }));
200
- yield* Ref.set(stoppedRef, true);
201
- yield* Fiber.interrupt(loopFiber);
202
- yield* settlePendingReplies(pendingReplies, id);
203
- yield* Scope.close(stateScopeRef.current, Exit.void);
204
- yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
205
- yield* Fiber.interrupt(snapshotFiber);
206
- yield* Fiber.interrupt(persistenceFiber);
207
- }).pipe(Effect.withSpan("effect-machine.persistentActor.stop"), Effect.asVoid), adapter, system, childrenMap, pendingReplies);
208
- });
209
- /**
210
- * Main event loop for persistent actor
211
- */
212
- const persistentEventLoop = Effect.fn("effect-machine.persistentActor.eventLoop")(function* (id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, self, listeners, adapter, createdAt, stateScopeRef, backgroundFibers, snapshotQueue, snapshotEnabledRef, persistenceQueue, snapshotFiber, persistenceFiber, inspector, system, pendingReplies) {
213
- const { machine, persistence } = persistentMachine;
214
- const typedMachine = machine;
215
- const hooks = inspector === void 0 ? void 0 : {
216
- onSpawnEffect: (state) => emitWithTimestamp(inspector, (timestamp) => ({
217
- type: "@machine.effect",
218
- actorId: id,
219
- effectType: "spawn",
220
- state,
221
- timestamp
222
- })),
223
- onTransition: (from, to, ev) => emitWithTimestamp(inspector, (timestamp) => ({
224
- type: "@machine.transition",
225
- actorId: id,
226
- fromState: from,
227
- toState: to,
228
- event: ev,
229
- timestamp
230
- })),
231
- onError: (info) => emitWithTimestamp(inspector, (timestamp) => ({
232
- type: "@machine.error",
233
- actorId: id,
234
- phase: info.phase,
235
- state: info.state,
236
- event: info.event,
237
- error: Cause.pretty(info.cause),
238
- timestamp
239
- }))
240
- };
241
- const postponed = [];
242
- const pendingDrain = [];
243
- const hasPostponeRules = machine.postponeRules.length > 0;
244
- while (true) {
245
- const queued = pendingDrain.length > 0 ? pendingDrain.shift() : yield* Queue.take(eventQueue);
246
- const event = queued.event;
247
- const currentState = yield* SubscriptionRef.get(stateRef);
248
- const currentVersion = yield* Ref.get(versionRef);
249
- if (hasPostponeRules && shouldPostpone(typedMachine, currentState._tag, event._tag)) {
250
- postponed.push(queued);
251
- if (queued._tag === "call") yield* Deferred.succeed(queued.reply, {
252
- newState: currentState,
253
- previousState: currentState,
254
- transitioned: false,
255
- lifecycleRan: false,
256
- isFinal: false,
257
- hasReply: false,
258
- reply: void 0,
259
- postponed: true
260
- });
261
- continue;
262
- }
263
- yield* emitWithTimestamp(inspector, (timestamp) => ({
264
- type: "@machine.event",
265
- actorId: id,
266
- state: currentState,
267
- event,
268
- timestamp
269
- }));
270
- const result = yield* processEventCore(typedMachine, currentState, event, self, stateScopeRef, system, id, hooks);
271
- if (queued._tag === "call") yield* Deferred.succeed(queued.reply, result);
272
- else if (queued._tag === "ask") if (result.hasReply) yield* Deferred.succeed(queued.reply, result.reply);
273
- else yield* Deferred.fail(queued.reply, new NoReplyError({
274
- actorId: id,
275
- eventTag: event._tag
276
- }));
277
- if (!result.transitioned) continue;
278
- const newVersion = currentVersion + 1;
279
- yield* Ref.set(versionRef, newVersion);
280
- yield* SubscriptionRef.set(stateRef, result.newState);
281
- notifyListeners(listeners, result.newState);
282
- if (persistence.journalEvents) {
283
- const persistedEvent = {
284
- event,
285
- version: newVersion,
286
- timestamp: yield* now
287
- };
288
- const journalTask = adapter.appendEvent(id, persistedEvent, persistence.eventSchema).pipe(Effect.catchAll((e) => Effect.logWarning(`Failed to journal event for actor ${id}`, e)), Effect.asVoid);
289
- yield* Queue.offer(persistenceQueue, journalTask);
290
- }
291
- yield* Queue.offer(persistenceQueue, saveMetadata(id, result.newState, newVersion, createdAt, persistence, adapter));
292
- if (yield* Ref.get(snapshotEnabledRef)) yield* Queue.offer(snapshotQueue, {
293
- state: result.newState,
294
- version: newVersion
295
- });
296
- if (result.lifecycleRan && result.isFinal) {
297
- yield* emitWithTimestamp(inspector, (timestamp) => ({
298
- type: "@machine.stop",
299
- actorId: id,
300
- finalState: result.newState,
301
- timestamp
302
- }));
303
- yield* Ref.set(stoppedRef, true);
304
- postponed.length = 0;
305
- yield* settlePendingReplies(pendingReplies, id);
306
- yield* Scope.close(stateScopeRef.current, Exit.void);
307
- yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
308
- yield* Fiber.interrupt(snapshotFiber);
309
- yield* Fiber.interrupt(persistenceFiber);
310
- return;
311
- }
312
- if (result.lifecycleRan && postponed.length > 0) pendingDrain.push(...postponed.splice(0));
313
- }
314
- });
315
- /**
316
- * Run spawn effects with inspection and tracing.
317
- * @internal
318
- */
319
- const runSpawnEffectsWithInspection = Effect.fn("effect-machine.persistentActor.spawnEffects")(function* (machine, state, event, self, stateScope, actorId, inspector, system) {
320
- yield* emitWithTimestamp(inspector, (timestamp) => ({
321
- type: "@machine.effect",
322
- actorId,
323
- effectType: "spawn",
324
- state,
325
- timestamp
326
- }));
327
- yield* runSpawnEffects(machine, state, event, self, stateScope, system, actorId, inspector === void 0 ? void 0 : (info) => emitWithTimestamp(inspector, (timestamp) => ({
328
- type: "@machine.error",
329
- actorId,
330
- phase: info.phase,
331
- state: info.state,
332
- event: info.event,
333
- error: Cause.pretty(info.cause),
334
- timestamp
335
- })));
336
- });
337
- /**
338
- * Persistence worker (journaling + metadata).
339
- */
340
- const persistenceWorker = Effect.fn("effect-machine.persistentActor.persistenceWorker")(function* (queue) {
341
- while (true) yield* yield* Queue.take(queue);
342
- });
343
- /**
344
- * Snapshot scheduler worker (runs in background).
345
- */
346
- const snapshotWorker = Effect.fn("effect-machine.persistentActor.snapshotWorker")(function* (id, persistence, adapter, queue, enabledRef) {
347
- const driver = yield* Schedule.driver(persistence.snapshotSchedule);
348
- while (true) {
349
- const { state, version } = yield* Queue.take(queue);
350
- if (!(yield* Ref.get(enabledRef))) continue;
351
- if (!(yield* driver.next(state).pipe(Effect.match({
352
- onFailure: () => false,
353
- onSuccess: () => true
354
- })))) {
355
- yield* Ref.set(enabledRef, false);
356
- continue;
357
- }
358
- yield* saveSnapshot(id, state, version, persistence, adapter);
359
- }
360
- });
361
- /**
362
- * Save a snapshot after state transition.
363
- * Called by snapshot scheduler.
364
- */
365
- const saveSnapshot = Effect.fn("effect-machine.persistentActor.saveSnapshot")(function* (id, state, version, persistence, adapter) {
366
- const snapshot = {
367
- state,
368
- version,
369
- timestamp: yield* now
370
- };
371
- yield* adapter.saveSnapshot(id, snapshot, persistence.stateSchema).pipe(Effect.catchAll((e) => Effect.logWarning(`Failed to save snapshot for actor ${id}`, e)));
372
- });
373
- /**
374
- * Save or update actor metadata if adapter supports registry.
375
- * Called on spawn and state transitions.
376
- */
377
- const saveMetadata = Effect.fn("effect-machine.persistentActor.saveMetadata")(function* (id, state, version, createdAt, persistence, adapter) {
378
- const save = adapter.saveMetadata;
379
- if (save === void 0) return;
380
- const lastActivityAt = yield* now;
381
- yield* save({
382
- id,
383
- machineType: persistence.machineType ?? "unknown",
384
- createdAt,
385
- lastActivityAt,
386
- version,
387
- stateTag: state._tag
388
- }).pipe(Effect.catchAll((e) => Effect.logWarning(`Failed to save metadata for actor ${id}`, e)));
389
- });
390
- /**
391
- * Restore an actor from persistence.
392
- * Returns None if no persisted state exists.
393
- */
394
- const restorePersistentActor = Effect.fn("effect-machine.persistentActor.restore")(function* (id, persistentMachine) {
395
- const adapter = yield* PersistenceAdapterTag;
396
- const { persistence } = persistentMachine;
397
- const maybeSnapshot = yield* adapter.loadSnapshot(id, persistence.stateSchema);
398
- const events = yield* adapter.loadEvents(id, persistence.eventSchema, Option.isSome(maybeSnapshot) ? maybeSnapshot.value.version : void 0);
399
- if (Option.isNone(maybeSnapshot) && events.length === 0) return Option.none();
400
- const actor = yield* createPersistentActor(id, persistentMachine, maybeSnapshot, events);
401
- return Option.some(actor);
402
- });
403
- //#endregion
404
- export { createPersistentActor, restorePersistentActor };
@@ -1,105 +0,0 @@
1
- import { EventBrand, StateBrand } from "../internal/brands.js";
2
- import { Machine } from "../machine.js";
3
- import { Schedule, Schema } from "effect";
4
-
5
- //#region src/persistence/persistent-machine.d.ts
6
- type BrandedState = {
7
- readonly _tag: string;
8
- } & StateBrand;
9
- type BrandedEvent = {
10
- readonly _tag: string;
11
- } & EventBrand;
12
- /**
13
- * Configuration for persistence behavior (after resolution).
14
- * Schemas are required at runtime - the persist function ensures this.
15
- *
16
- * Note: Schema types S and E should match the structural shape of the machine's
17
- * state and event types (without brands). The schemas don't know about brands.
18
- */
19
- interface PersistenceConfig<S, E, SSI = unknown, ESI = unknown> {
20
- /**
21
- * Schedule controlling when snapshots are taken.
22
- * Input is the new state after each transition.
23
- *
24
- * Examples:
25
- * - Schedule.forever — snapshot every transition
26
- * - Schedule.spaced("5 seconds") — debounced snapshots
27
- * - Schedule.recurs(100) — every N transitions
28
- */
29
- readonly snapshotSchedule: Schedule.Schedule<unknown, S>;
30
- /**
31
- * Whether to journal events for replay capability.
32
- * When true, all events are appended to the event log.
33
- */
34
- readonly journalEvents: boolean;
35
- /**
36
- * Schema for serializing/deserializing state.
37
- * Always present at runtime (resolved from config or machine).
38
- */
39
- readonly stateSchema: Schema.Schema<S, SSI, never>;
40
- /**
41
- * Schema for serializing/deserializing events.
42
- * Always present at runtime (resolved from config or machine).
43
- */
44
- readonly eventSchema: Schema.Schema<E, ESI, never>;
45
- /**
46
- * User-provided identifier for the machine type.
47
- * Used for filtering actors in restoreAll.
48
- * Optional — defaults to "unknown" if not provided.
49
- */
50
- readonly machineType?: string;
51
- }
52
- /**
53
- * Machine with persistence configuration attached.
54
- * Spawn auto-detects this and returns PersistentActorRef.
55
- */
56
- interface PersistentMachine<S extends {
57
- readonly _tag: string;
58
- }, E extends {
59
- readonly _tag: string;
60
- }, R = never> {
61
- readonly _tag: "PersistentMachine";
62
- readonly machine: Machine<S, E, R>;
63
- readonly persistence: PersistenceConfig<S, E>;
64
- }
65
- /**
66
- * Type guard to check if a value is a PersistentMachine
67
- */
68
- declare const isPersistentMachine: (value: unknown) => value is PersistentMachine<{
69
- readonly _tag: string;
70
- }, {
71
- readonly _tag: string;
72
- }, unknown>;
73
- /**
74
- * Attach persistence configuration to a machine.
75
- *
76
- * Schemas are read from the machine - must use `Machine.make({ state, event, initial })`.
77
- *
78
- * @example
79
- * ```ts
80
- * const orderMachine = Machine.make({
81
- * state: OrderState,
82
- * event: OrderEvent,
83
- * initial: OrderState.Idle(),
84
- * }).pipe(
85
- * Machine.on(OrderState.Idle, OrderEvent.Submit, ({ event }) =>
86
- * OrderState.Pending({ orderId: event.orderId })
87
- * ),
88
- * Machine.final(OrderState.Paid),
89
- * Machine.persist({
90
- * snapshotSchedule: Schedule.forever,
91
- * journalEvents: true,
92
- * }),
93
- * );
94
- * ```
95
- */
96
- interface WithPersistenceConfig {
97
- readonly snapshotSchedule: Schedule.Schedule<unknown, {
98
- readonly _tag: string;
99
- }>;
100
- readonly journalEvents: boolean;
101
- readonly machineType?: string;
102
- }
103
- declare const persist: (config: WithPersistenceConfig) => <S extends BrandedState, E extends BrandedEvent, R>(machine: Machine<S, E, R>) => PersistentMachine<S, E, R>;
104
- //#endregion
105
- export { PersistenceConfig, PersistentMachine, WithPersistenceConfig, isPersistentMachine, persist };
@@ -1,22 +0,0 @@
1
- import { MissingSchemaError } from "../errors.js";
2
- //#region src/persistence/persistent-machine.ts
3
- /**
4
- * Type guard to check if a value is a PersistentMachine
5
- */
6
- const isPersistentMachine = (value) => typeof value === "object" && value !== null && "_tag" in value && value._tag === "PersistentMachine";
7
- const persist = (config) => (machine) => {
8
- const stateSchema = machine.stateSchema;
9
- const eventSchema = machine.eventSchema;
10
- if (stateSchema === void 0 || eventSchema === void 0) throw new MissingSchemaError({ operation: "persist" });
11
- return {
12
- _tag: "PersistentMachine",
13
- machine,
14
- persistence: {
15
- ...config,
16
- stateSchema,
17
- eventSchema
18
- }
19
- };
20
- };
21
- //#endregion
22
- export { isPersistentMachine, persist };