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