effect-machine 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/dist/_virtual/_rolldown/runtime.js +18 -0
- package/dist/actor.d.ts +256 -0
- package/dist/actor.js +402 -0
- package/dist/cluster/entity-machine.d.ts +90 -0
- package/dist/cluster/entity-machine.js +80 -0
- package/dist/cluster/index.d.ts +3 -0
- package/dist/cluster/index.js +4 -0
- package/dist/cluster/to-entity.d.ts +64 -0
- package/dist/cluster/to-entity.js +53 -0
- package/dist/errors.d.ts +61 -0
- package/dist/errors.js +38 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +14 -0
- package/dist/inspection.d.ts +125 -0
- package/dist/inspection.js +50 -0
- package/dist/internal/brands.d.ts +40 -0
- package/dist/internal/brands.js +0 -0
- package/dist/internal/inspection.d.ts +11 -0
- package/dist/internal/inspection.js +15 -0
- package/dist/internal/transition.d.ts +160 -0
- package/dist/internal/transition.js +238 -0
- package/dist/internal/utils.d.ts +60 -0
- package/dist/internal/utils.js +46 -0
- package/dist/machine.d.ts +278 -0
- package/dist/machine.js +317 -0
- package/{src/persistence/adapter.ts → dist/persistence/adapter.d.ts} +40 -72
- package/dist/persistence/adapter.js +27 -0
- package/dist/persistence/adapters/in-memory.d.ts +32 -0
- package/dist/persistence/adapters/in-memory.js +176 -0
- package/dist/persistence/index.d.ts +5 -0
- package/dist/persistence/index.js +6 -0
- package/dist/persistence/persistent-actor.d.ts +50 -0
- package/dist/persistence/persistent-actor.js +358 -0
- package/{src/persistence/persistent-machine.ts → dist/persistence/persistent-machine.d.ts} +28 -54
- package/dist/persistence/persistent-machine.js +24 -0
- package/dist/schema.d.ts +141 -0
- package/dist/schema.js +165 -0
- package/dist/slot.d.ts +130 -0
- package/dist/slot.js +99 -0
- package/dist/testing.d.ts +142 -0
- package/dist/testing.js +138 -0
- package/package.json +28 -14
- package/src/actor.ts +0 -1058
- package/src/cluster/entity-machine.ts +0 -201
- package/src/cluster/index.ts +0 -43
- package/src/cluster/to-entity.ts +0 -99
- package/src/errors.ts +0 -64
- package/src/index.ts +0 -105
- package/src/inspection.ts +0 -178
- package/src/internal/brands.ts +0 -51
- package/src/internal/inspection.ts +0 -18
- package/src/internal/transition.ts +0 -489
- package/src/internal/utils.ts +0 -80
- package/src/machine.ts +0 -836
- package/src/persistence/adapters/in-memory.ts +0 -294
- package/src/persistence/index.ts +0 -24
- package/src/persistence/persistent-actor.ts +0 -791
- package/src/schema.ts +0 -362
- package/src/slot.ts +0 -281
- package/src/testing.ts +0 -284
- 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
|
-
);
|