effect-machine 0.14.0 → 0.15.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 +5 -2
- package/dist/actor.d.ts +13 -4
- package/dist/actor.js +91 -55
- package/dist/cluster/entity-machine.js +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/internal/runtime.d.ts +20 -1
- package/dist/internal/runtime.js +60 -47
- package/dist/machine.d.ts +37 -30
- package/dist/machine.js +5 -5
- package/dist/schema.d.ts +3 -1
- package/dist/schema.js +1 -0
- package/package.json +1 -1
- package/v3/dist/actor.d.ts +10 -4
- package/v3/dist/actor.js +80 -50
- package/v3/dist/cluster/entity-machine.js +1 -0
- package/v3/dist/index.d.ts +2 -2
- package/v3/dist/internal/runtime.d.ts +20 -1
- package/v3/dist/internal/runtime.js +60 -47
- package/v3/dist/machine.d.ts +37 -30
- package/v3/dist/machine.js +5 -5
package/dist/machine.js
CHANGED
|
@@ -395,11 +395,11 @@ const make = Machine.make;
|
|
|
395
395
|
* slots: { canRetry: ({ max }) => attempts < max },
|
|
396
396
|
* });
|
|
397
397
|
*
|
|
398
|
-
* // With
|
|
398
|
+
* // With lifecycle (recovery + durability)
|
|
399
399
|
* const actor = yield* Machine.spawn(machine, {
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
* save: (
|
|
400
|
+
* lifecycle: {
|
|
401
|
+
* recovery: { resolve: (ctx) => storage.get("actor-state") },
|
|
402
|
+
* durability: { save: (commit) => storage.set("actor-state", commit.nextState) },
|
|
403
403
|
* },
|
|
404
404
|
* });
|
|
405
405
|
* ```
|
|
@@ -409,7 +409,7 @@ const spawn = Effect.fn("effect-machine.spawn")(function* (machine, idOrOptions)
|
|
|
409
409
|
const actor = yield* createActor(opts?.id ?? `actor-${(yield* Random.next).toString(36).slice(2)}`, materializeMachine(machine, opts?.slots), {
|
|
410
410
|
initialState: opts?.hydrate,
|
|
411
411
|
supervision: opts?.supervision,
|
|
412
|
-
|
|
412
|
+
lifecycle: opts?.lifecycle
|
|
413
413
|
});
|
|
414
414
|
const maybeScope = yield* Effect.serviceOption(Scope.Scope);
|
|
415
415
|
if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, actor.stop);
|
package/dist/schema.d.ts
CHANGED
|
@@ -111,7 +111,9 @@ interface MachineSchemaBase<D extends Record<string, Schema.Struct.Fields>, Bran
|
|
|
111
111
|
* The D type parameter captures the definition, creating a unique brand
|
|
112
112
|
* per distinct schema definition shape.
|
|
113
113
|
*/
|
|
114
|
-
type MachineStateSchema<D extends Record<string, Schema.Struct.Fields>> = Schema.Codec<VariantsUnion<D> & FullStateBrand<D>, unknown, never, never> & MachineSchemaBase<D, FullStateBrand<D>> & VariantConstructors<D, FullStateBrand<D
|
|
114
|
+
type MachineStateSchema<D extends Record<string, Schema.Struct.Fields>> = Schema.Codec<VariantsUnion<D> & FullStateBrand<D>, unknown, never, never> & MachineSchemaBase<D, FullStateBrand<D>> & VariantConstructors<D, FullStateBrand<D>> & {
|
|
115
|
+
/** Unbranded schema for persistence — same structure without FullStateBrand. */readonly plain: Schema.Schema<VariantsUnion<D>>;
|
|
116
|
+
};
|
|
115
117
|
/**
|
|
116
118
|
* Schema-first event definition (same structure as state, different brand)
|
|
117
119
|
*
|
package/dist/schema.js
CHANGED
package/package.json
CHANGED
package/v3/dist/actor.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { ActorStoppedError, DuplicateActorError, NoReplyError } from "./errors.j
|
|
|
3
3
|
import { ProvideSlots, SlotsDef } from "./slot.js";
|
|
4
4
|
import { ActorExit, Supervision } from "./supervision.js";
|
|
5
5
|
import { ProcessEventError, ProcessEventHooks, ProcessEventResult, processEventCore, resolveTransition, runSpawnEffects } from "./internal/transition.js";
|
|
6
|
-
import {
|
|
6
|
+
import { Lifecycle, Machine } from "./machine.js";
|
|
7
7
|
import { RuntimeQueuedEvent } from "./internal/runtime.js";
|
|
8
8
|
import { Context, Deferred, Effect, Layer, Option, PubSub, Queue, Ref, Scope, Stream, SubscriptionRef } from "effect";
|
|
9
9
|
|
|
@@ -54,6 +54,12 @@ interface ActorRef<State extends {
|
|
|
54
54
|
readonly state: SubscriptionRef.SubscriptionRef<State>;
|
|
55
55
|
/** Stop the actor gracefully. */
|
|
56
56
|
readonly stop: Effect.Effect<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Start the actor — fork event loop, background effects, spawn effects.
|
|
59
|
+
* Called automatically by `system.spawn`. For `Machine.spawn`, the caller
|
|
60
|
+
* must call `start` explicitly. Events sent before start() are queued.
|
|
61
|
+
*/
|
|
62
|
+
readonly start: Effect.Effect<void>;
|
|
57
63
|
/** Get current state snapshot. */
|
|
58
64
|
readonly snapshot: Effect.Effect<State>;
|
|
59
65
|
/** Check if current state matches tag. */
|
|
@@ -161,7 +167,7 @@ interface ActorSystem {
|
|
|
161
167
|
}, R, SD extends SlotsDef = Record<string, never>>(id: string, machine: Machine<S, E, R, any, any, SD>, options?: {
|
|
162
168
|
readonly supervision?: Supervision.Policy;
|
|
163
169
|
readonly slots?: ProvideSlots<SD, any>;
|
|
164
|
-
readonly
|
|
170
|
+
readonly lifecycle?: Lifecycle<S, E>;
|
|
165
171
|
}) => Effect.Effect<ActorRef<S, E>, DuplicateActorError, R>;
|
|
166
172
|
/**
|
|
167
173
|
* Get an existing actor by ID
|
|
@@ -204,7 +210,7 @@ declare const buildActorRefCore: <S extends {
|
|
|
204
210
|
readonly _tag: string;
|
|
205
211
|
}, E extends {
|
|
206
212
|
readonly _tag: string;
|
|
207
|
-
}, R, SD extends SlotsDef>(id: string, machine: Machine<S, E, R, any, any, SD>, stateRef: SubscriptionRef.SubscriptionRef<S>, eventQueueRef: Ref.Ref<Queue.Queue<QueuedEvent<E>>>, stoppedRef: Ref.Ref<boolean>, listeners: Listeners<S>, stop: Effect.Effect<void>, system: ActorSystem, childrenMap: ReadonlyMap<string, ActorRef<AnyState, unknown>>, pendingReplies: Set<Deferred.Deferred<unknown, unknown>>, transitionsPubSub: PubSub.PubSub<TransitionInfo<S, E>> | undefined, exitDeferred: Deferred.Deferred<ActorExit<S>, never>) => ActorRef<S, E>;
|
|
213
|
+
}, R, SD extends SlotsDef>(id: string, machine: Machine<S, E, R, any, any, SD>, stateRef: SubscriptionRef.SubscriptionRef<S>, eventQueueRef: Ref.Ref<Queue.Queue<QueuedEvent<E>>>, stoppedRef: Ref.Ref<boolean>, listeners: Listeners<S>, stop: Effect.Effect<void>, start: Effect.Effect<void>, system: ActorSystem, childrenMap: ReadonlyMap<string, ActorRef<AnyState, unknown>>, pendingReplies: Set<Deferred.Deferred<unknown, unknown>>, transitionsPubSub: PubSub.PubSub<TransitionInfo<S, E>> | undefined, exitDeferred: Deferred.Deferred<ActorExit<S>, never>) => ActorRef<S, E>;
|
|
208
214
|
/**
|
|
209
215
|
* Create and start an actor for a machine.
|
|
210
216
|
* Delegates to the shared runtime kernel with actor-specific lifecycle hooks.
|
|
@@ -216,7 +222,7 @@ declare const createActor: <S extends {
|
|
|
216
222
|
}, R, SD extends SlotsDef>(id: string, machine: Machine<S, E, R, any, any, SD>, options?: {
|
|
217
223
|
initialState?: S;
|
|
218
224
|
supervision?: Supervision.Policy;
|
|
219
|
-
|
|
225
|
+
lifecycle?: Lifecycle<S, E>; /** @internal Called by system after each restart — emits ActorRestarted system event */
|
|
220
226
|
onRestart?: (generation: number, exit: ActorExit<unknown>) => Effect.Effect<void>;
|
|
221
227
|
} | undefined) => Effect.Effect<ActorRef<S, E>, never, never>;
|
|
222
228
|
/** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
|
package/v3/dist/actor.js
CHANGED
|
@@ -29,7 +29,7 @@ const notifyListeners = (listeners, state) => {
|
|
|
29
29
|
/**
|
|
30
30
|
* Build core ActorRef methods.
|
|
31
31
|
*/
|
|
32
|
-
const buildActorRefCore = (id, machine, stateRef, eventQueueRef, stoppedRef, listeners, stop, system, childrenMap, pendingReplies, transitionsPubSub, exitDeferred) => {
|
|
32
|
+
const buildActorRefCore = (id, machine, stateRef, eventQueueRef, stoppedRef, listeners, stop, start, system, childrenMap, pendingReplies, transitionsPubSub, exitDeferred) => {
|
|
33
33
|
const send = Effect.fn("effect-machine.actor.send")(function* (event) {
|
|
34
34
|
if (yield* Ref.get(stoppedRef)) return;
|
|
35
35
|
const q = yield* Ref.get(eventQueueRef);
|
|
@@ -116,6 +116,7 @@ const buildActorRefCore = (id, machine, stateRef, eventQueueRef, stoppedRef, lis
|
|
|
116
116
|
ask,
|
|
117
117
|
state: stateRef,
|
|
118
118
|
stop,
|
|
119
|
+
start,
|
|
119
120
|
snapshot,
|
|
120
121
|
matches,
|
|
121
122
|
can,
|
|
@@ -191,18 +192,6 @@ const buildInspectionHooks = (actorId, inspector) => ({
|
|
|
191
192
|
}))
|
|
192
193
|
});
|
|
193
194
|
/**
|
|
194
|
-
* Load persisted state and run onRestore hook if present.
|
|
195
|
-
* Returns the resolved initial state (loaded, restored, or fallback to machineInitial).
|
|
196
|
-
* @internal
|
|
197
|
-
*/
|
|
198
|
-
const loadAndRestore = (persist, machineInitial) => Effect.gen(function* () {
|
|
199
|
-
const loaded = yield* persist.load();
|
|
200
|
-
if (Option.isNone(loaded)) return machineInitial;
|
|
201
|
-
if (persist.onRestore === void 0) return loaded.value;
|
|
202
|
-
const restored = yield* persist.onRestore(loaded.value, { initial: machineInitial });
|
|
203
|
-
return Option.getOrElse(restored, () => machineInitial);
|
|
204
|
-
});
|
|
205
|
-
/**
|
|
206
195
|
* Resolve actor system from context, creating an implicit one if none exists.
|
|
207
196
|
* @internal
|
|
208
197
|
*/
|
|
@@ -242,7 +231,15 @@ const runSupervisionLoop = (params) => Effect.gen(function* () {
|
|
|
242
231
|
yield* Deferred.succeed(params.terminalExitDeferred, generationExit);
|
|
243
232
|
return;
|
|
244
233
|
}
|
|
245
|
-
|
|
234
|
+
let restartState = params.machine.initial;
|
|
235
|
+
if (params.lifecycle?.recovery !== void 0) {
|
|
236
|
+
const resolved = yield* params.lifecycle.recovery.resolve({
|
|
237
|
+
actorId: params.id,
|
|
238
|
+
generation: generation + 1,
|
|
239
|
+
machineInitial: params.machine.initial
|
|
240
|
+
});
|
|
241
|
+
if (Option.isSome(resolved)) restartState = resolved.value;
|
|
242
|
+
}
|
|
246
243
|
yield* settlePendingReplies(params.pendingReplies, params.id);
|
|
247
244
|
const freshQueue = yield* Queue.unbounded();
|
|
248
245
|
yield* Ref.set(params.eventQueueRef, freshQueue);
|
|
@@ -255,6 +252,7 @@ const runSupervisionLoop = (params) => Effect.gen(function* () {
|
|
|
255
252
|
} }) : params.machine;
|
|
256
253
|
const newRuntime = yield* params.spawnGeneration(machineForRestart);
|
|
257
254
|
params.runtimeRef.current = newRuntime;
|
|
255
|
+
yield* newRuntime.start;
|
|
258
256
|
generation++;
|
|
259
257
|
if (params.onRestart !== void 0) yield* params.onRestart(generation, generationExit);
|
|
260
258
|
notifyListeners(params.listeners, restartState);
|
|
@@ -265,8 +263,8 @@ const runSupervisionLoop = (params) => Effect.gen(function* () {
|
|
|
265
263
|
* Delegates to the shared runtime kernel with actor-specific lifecycle hooks.
|
|
266
264
|
*/
|
|
267
265
|
const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machine, options) {
|
|
268
|
-
const
|
|
269
|
-
const initial = options?.initialState ??
|
|
266
|
+
const lifecycle = options?.lifecycle;
|
|
267
|
+
const initial = options?.initialState ?? machine.initial;
|
|
270
268
|
yield* Effect.annotateCurrentSpan("effect_machine.actor.id", id);
|
|
271
269
|
yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", initial._tag);
|
|
272
270
|
const { system, implicitSystemScope } = yield* resolveActorSystem();
|
|
@@ -275,12 +273,6 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
275
273
|
const pendingReplies = /* @__PURE__ */ new Set();
|
|
276
274
|
const listeners = /* @__PURE__ */ new Set();
|
|
277
275
|
const transitionsPubSub = yield* PubSub.unbounded();
|
|
278
|
-
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
279
|
-
type: "@machine.spawn",
|
|
280
|
-
actorId: id,
|
|
281
|
-
initialState: initial,
|
|
282
|
-
timestamp
|
|
283
|
-
}));
|
|
284
276
|
const hooks = inspectorValue !== void 0 ? buildInspectionHooks(id, inspectorValue) : void 0;
|
|
285
277
|
const machineWithState = initial !== machine.initial ? Object.create(machine, { initial: {
|
|
286
278
|
value: initial,
|
|
@@ -292,6 +284,7 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
292
284
|
const eventQueueRef = yield* Ref.make(initialQueue);
|
|
293
285
|
const terminalExitDeferred = yield* Deferred.make();
|
|
294
286
|
let stopEmitted = false;
|
|
287
|
+
let generation = 0;
|
|
295
288
|
const runtimeRef = { current: void 0 };
|
|
296
289
|
/** Build lifecycle hooks for a generation */
|
|
297
290
|
const buildLifecycle = () => {
|
|
@@ -304,10 +297,17 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
304
297
|
event,
|
|
305
298
|
timestamp
|
|
306
299
|
})) : void 0,
|
|
307
|
-
onStateChange: (result,
|
|
300
|
+
onStateChange: (result, event) => Effect.gen(function* () {
|
|
308
301
|
notifyListeners(listeners, result.newState);
|
|
309
|
-
if (
|
|
310
|
-
|
|
302
|
+
if (lifecycle?.durability !== void 0 && result.transitioned) {
|
|
303
|
+
const durability = lifecycle.durability;
|
|
304
|
+
if (durability.shouldSave === void 0 || durability.shouldSave(result.newState, result.previousState)) yield* durability.save({
|
|
305
|
+
actorId: id,
|
|
306
|
+
generation,
|
|
307
|
+
previousState: result.previousState,
|
|
308
|
+
nextState: result.newState,
|
|
309
|
+
event
|
|
310
|
+
});
|
|
311
311
|
}
|
|
312
312
|
yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", true);
|
|
313
313
|
if (result.lifecycleRan) {
|
|
@@ -374,34 +374,62 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
374
374
|
}));
|
|
375
375
|
})
|
|
376
376
|
})));
|
|
377
|
-
|
|
378
|
-
runtimeRef.current = runtime;
|
|
377
|
+
runtimeRef.current = yield* spawnGeneration(machineWithState);
|
|
379
378
|
const supervision = options?.supervision;
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
machine,
|
|
384
|
-
id,
|
|
385
|
-
runtimeRef,
|
|
386
|
-
terminalExitDeferred,
|
|
387
|
-
pendingReplies,
|
|
388
|
-
eventQueueRef,
|
|
389
|
-
stateRef,
|
|
390
|
-
stoppedRef,
|
|
391
|
-
childrenMap,
|
|
392
|
-
listeners,
|
|
393
|
-
spawnGeneration,
|
|
394
|
-
persist,
|
|
395
|
-
onRestart: options?.onRestart
|
|
396
|
-
}));
|
|
397
|
-
else yield* Effect.forkDaemon(Deferred.await(runtime.exitDeferred).pipe(Effect.tap((exit) => Deferred.succeed(terminalExitDeferred, exit))));
|
|
398
|
-
return buildActorRefCore(id, machine, stateRef, eventQueueRef, stoppedRef, listeners, Effect.gen(function* () {
|
|
399
|
-
if (supervisorFiber !== void 0) yield* Fiber.interrupt(supervisorFiber);
|
|
379
|
+
const supervisorFiberRef = { current: void 0 };
|
|
380
|
+
const stop = Effect.gen(function* () {
|
|
381
|
+
if (supervisorFiberRef.current !== void 0) yield* Fiber.interrupt(supervisorFiberRef.current);
|
|
400
382
|
const currentRuntime = runtimeRef.current;
|
|
401
383
|
if (currentRuntime !== void 0) yield* currentRuntime.stop;
|
|
402
384
|
yield* Deferred.succeed(terminalExitDeferred, { _tag: "Stopped" });
|
|
403
385
|
if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
|
|
404
|
-
}).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid)
|
|
386
|
+
}).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid);
|
|
387
|
+
const isHydrated = options?.initialState !== void 0;
|
|
388
|
+
return buildActorRefCore(id, machine, stateRef, eventQueueRef, stoppedRef, listeners, stop, Effect.gen(function* () {
|
|
389
|
+
if (lifecycle?.recovery !== void 0 && !isHydrated) {
|
|
390
|
+
const resolved = yield* lifecycle.recovery.resolve({
|
|
391
|
+
actorId: id,
|
|
392
|
+
generation,
|
|
393
|
+
machineInitial: machine.initial
|
|
394
|
+
});
|
|
395
|
+
if (Option.isSome(resolved)) {
|
|
396
|
+
yield* SubscriptionRef.set(stateRef, resolved.value);
|
|
397
|
+
runtimeRef.current = yield* spawnGeneration(Object.create(machine, { initial: {
|
|
398
|
+
value: resolved.value,
|
|
399
|
+
enumerable: true
|
|
400
|
+
} }));
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const currentState = yield* SubscriptionRef.get(stateRef);
|
|
404
|
+
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
405
|
+
type: "@machine.spawn",
|
|
406
|
+
actorId: id,
|
|
407
|
+
initialState: currentState,
|
|
408
|
+
timestamp
|
|
409
|
+
}));
|
|
410
|
+
if (supervision !== void 0) supervisorFiberRef.current = yield* Effect.forkDaemon(runSupervisionLoop({
|
|
411
|
+
supervision,
|
|
412
|
+
machine,
|
|
413
|
+
id,
|
|
414
|
+
runtimeRef,
|
|
415
|
+
terminalExitDeferred,
|
|
416
|
+
pendingReplies,
|
|
417
|
+
eventQueueRef,
|
|
418
|
+
stateRef,
|
|
419
|
+
stoppedRef,
|
|
420
|
+
childrenMap,
|
|
421
|
+
listeners,
|
|
422
|
+
spawnGeneration,
|
|
423
|
+
lifecycle,
|
|
424
|
+
onRestart: options?.onRestart
|
|
425
|
+
}));
|
|
426
|
+
else {
|
|
427
|
+
const currentRuntime = runtimeRef.current;
|
|
428
|
+
if (currentRuntime !== void 0) yield* Effect.forkDaemon(Deferred.await(currentRuntime.exitDeferred).pipe(Effect.tap((exit) => Deferred.succeed(terminalExitDeferred, exit))));
|
|
429
|
+
}
|
|
430
|
+
const currentRuntime = runtimeRef.current;
|
|
431
|
+
if (currentRuntime !== void 0) yield* currentRuntime.start;
|
|
432
|
+
}).pipe(Effect.withSpan("effect-machine.actor.start"), Effect.asVoid), system, childrenMap, pendingReplies, transitionsPubSub, terminalExitDeferred);
|
|
405
433
|
});
|
|
406
434
|
/** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
|
|
407
435
|
const settlePendingReplies = (pendingReplies, actorId) => Effect.sync(() => {
|
|
@@ -462,7 +490,7 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
462
490
|
let actorRef;
|
|
463
491
|
const actor = yield* createActor(id, materialized, {
|
|
464
492
|
supervision: spawnOptions?.supervision,
|
|
465
|
-
|
|
493
|
+
lifecycle: spawnOptions?.lifecycle,
|
|
466
494
|
onRestart: spawnOptions?.supervision !== void 0 ? (generation, exit) => actorRef !== void 0 ? emitSystemEvent({
|
|
467
495
|
_tag: "ActorRestarted",
|
|
468
496
|
id,
|
|
@@ -472,7 +500,9 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
472
500
|
}) : Effect.void : void 0
|
|
473
501
|
});
|
|
474
502
|
actorRef = actor;
|
|
475
|
-
|
|
503
|
+
yield* registerActor(id, actor);
|
|
504
|
+
yield* actor.start.pipe(Effect.catchAllCause((cause) => actor.stop.pipe(Effect.andThen(Effect.failCause(cause)))));
|
|
505
|
+
return actor;
|
|
476
506
|
});
|
|
477
507
|
const spawn = (id, machine, options) => withSpawnGate(spawnRegular(id, machine, options));
|
|
478
508
|
const get = Effect.fn("effect-machine.actorSystem.get")(function* (id) {
|
|
@@ -50,6 +50,7 @@ const EntityMachine = { layer: (entity, machine, options) => {
|
|
|
50
50
|
hooks: options?.hooks,
|
|
51
51
|
childIdPrefix: `${entityId}/`
|
|
52
52
|
});
|
|
53
|
+
yield* runtime.start;
|
|
53
54
|
if (persistCtx.adapter !== void 0) {
|
|
54
55
|
const { adapter: pAdapter, key } = persistCtx;
|
|
55
56
|
yield* Effect.addFinalizer(() => Effect.gen(function* () {
|
package/v3/dist/index.d.ts
CHANGED
|
@@ -4,8 +4,8 @@ import { ActorStoppedError, AssertionError, DuplicateActorError, InvalidSchemaEr
|
|
|
4
4
|
import { HasSlotKeys, MachineContext, ProvideSlots, Slot, SlotCall, SlotCalls, SlotFnDef, SlotHandler, SlotInvocation, SlotRequest, SlotResult, SlotsDef, SlotsSchema } from "./slot.js";
|
|
5
5
|
import { ActorExit, CellPhase, DefectPhase, Supervision } from "./supervision.js";
|
|
6
6
|
import { ProcessEventResult } from "./internal/transition.js";
|
|
7
|
-
import { BackgroundEffect, HandlerContext, Machine, MachineRef, MakeConfig, SpawnEffect, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, machine_d_exports } from "./machine.js";
|
|
7
|
+
import { BackgroundEffect, Durability, DurabilityCommit, HandlerContext, Lifecycle, Machine, MachineRef, MakeConfig, Recovery, RecoveryContext, SpawnEffect, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, machine_d_exports } from "./machine.js";
|
|
8
8
|
import { ActorRef, ActorRefSync, ActorSystem, Default, SystemEvent, SystemEventListener, TransitionInfo } from "./actor.js";
|
|
9
9
|
import { SimulationResult, TestHarness, TestHarnessOptions, assertNeverReaches, assertPath, assertReaches, createTestHarness, simulate } from "./testing.js";
|
|
10
10
|
import { AnyInspectionEvent, EffectEvent, ErrorEvent, EventReceivedEvent, InspectionEvent, Inspector, InspectorHandler, SpawnEvent, StopEvent, TaskEvent, TracingInspectorOptions, TransitionEvent, collectingInspector, combineInspectors, consoleInspector, makeInspector, makeInspectorEffect, tracingInspector } from "./inspection.js";
|
|
11
|
-
export { ActorExit, type ActorRef, type ActorRefSync, ActorStoppedError, type ActorSystem, Default as ActorSystemDefault, ActorSystem as ActorSystemService, type AnyInspectionEvent, AssertionError, type BackgroundEffect, type CellPhase, type DefectPhase, type DeferReplyResult, DuplicateActorError, type EffectEvent, type ErrorEvent, Event, type EventReceivedEvent, type HandlerContext, type HasSlotKeys, type InspectionEvent, type Inspector, type InspectorHandler, Inspector as InspectorService, InvalidSchemaError, machine_d_exports as Machine, type MachineContext, type MachineEventSchema, type MachineRef, type MachineStateSchema, type Machine as MachineType, type MakeConfig, MissingMatchHandlerError, MissingSchemaError, NoReplyError, PersistenceError, type ProcessEventResult, type ProvideSlots, ProvisionValidationError, type ReplyFields, type ReplyResult, type SimulationResult, Slot, type SlotCall, type SlotCalls, SlotCodecError, type SlotFnDef, type SlotHandler, type SlotInvocation, SlotProvisionError, type SlotRequest, type SlotResult, type SlotsDef, type SlotsSchema, type SpawnEffect, type SpawnEvent, State, type StateHandlerContext, type StopEvent, Supervision, type SystemEvent, type SystemEventListener, type TaskEvent, type TaskOptions, type TestHarness, type TestHarnessOptions, type TimeoutConfig, type TracingInspectorOptions, type Transition, type TransitionEvent, type TransitionInfo, VersionConflictError, assertNeverReaches, assertPath, assertReaches, collectingInspector, combineInspectors, consoleInspector, createTestHarness, makeInspector, makeInspectorEffect, simulate, tracingInspector };
|
|
11
|
+
export { ActorExit, type ActorRef, type ActorRefSync, ActorStoppedError, type ActorSystem, Default as ActorSystemDefault, ActorSystem as ActorSystemService, type AnyInspectionEvent, AssertionError, type BackgroundEffect, type CellPhase, type DefectPhase, type DeferReplyResult, DuplicateActorError, type Durability, type DurabilityCommit, type EffectEvent, type ErrorEvent, Event, type EventReceivedEvent, type HandlerContext, type HasSlotKeys, type InspectionEvent, type Inspector, type InspectorHandler, Inspector as InspectorService, InvalidSchemaError, type Lifecycle, machine_d_exports as Machine, type MachineContext, type MachineEventSchema, type MachineRef, type MachineStateSchema, type Machine as MachineType, type MakeConfig, MissingMatchHandlerError, MissingSchemaError, NoReplyError, PersistenceError, type ProcessEventResult, type ProvideSlots, ProvisionValidationError, type Recovery, type RecoveryContext, type ReplyFields, type ReplyResult, type SimulationResult, Slot, type SlotCall, type SlotCalls, SlotCodecError, type SlotFnDef, type SlotHandler, type SlotInvocation, SlotProvisionError, type SlotRequest, type SlotResult, type SlotsDef, type SlotsSchema, type SpawnEffect, type SpawnEvent, State, type StateHandlerContext, type StopEvent, Supervision, type SystemEvent, type SystemEventListener, type TaskEvent, type TaskOptions, type TestHarness, type TestHarnessOptions, type TimeoutConfig, type TracingInspectorOptions, type Transition, type TransitionEvent, type TransitionInfo, VersionConflictError, assertNeverReaches, assertPath, assertReaches, collectingInspector, combineInspectors, consoleInspector, createTestHarness, makeInspector, makeInspectorEffect, simulate, tracingInspector };
|
|
@@ -55,6 +55,12 @@ interface RuntimeHandle<S, E> {
|
|
|
55
55
|
readonly isStopped: Effect.Effect<boolean>;
|
|
56
56
|
/** Stop the runtime (interrupt event loop, clean up) */
|
|
57
57
|
readonly stop: Effect.Effect<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Start the runtime — fork event loop, background effects, spawn effects.
|
|
60
|
+
* Idempotent: first caller runs initialization, subsequent callers await completion.
|
|
61
|
+
* Events sent before start() are queued and processed when start() runs.
|
|
62
|
+
*/
|
|
63
|
+
readonly start: Effect.Effect<void>;
|
|
58
64
|
/** @internal — raw event queue for direct enqueue (actor.ts uses this for pendingReplies tracking) */
|
|
59
65
|
readonly _queue: Queue.Queue<RuntimeQueuedEvent<E>>;
|
|
60
66
|
/** @internal — stopped ref for direct access */
|
|
@@ -137,6 +143,19 @@ declare const createRuntime: <S extends {
|
|
|
137
143
|
readonly _tag: string;
|
|
138
144
|
}, E extends {
|
|
139
145
|
readonly _tag: string;
|
|
140
|
-
}, R, SD extends SlotsDef>(machine: Machine<S, E, R, any, any, SD>, system: ActorSystem, config: RuntimeConfig<S, E>) => Effect.Effect<
|
|
146
|
+
}, R, SD extends SlotsDef>(machine: Machine<S, E, R, any, any, SD>, system: ActorSystem, config: RuntimeConfig<S, E>) => Effect.Effect<{
|
|
147
|
+
stop: Effect.Effect<void, never, never>;
|
|
148
|
+
start: Effect.Effect<void, unknown, Exclude<R, MachineContext<S, E, MachineRef<E>>> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>>;
|
|
149
|
+
send: (event: E) => Effect.Effect<void>;
|
|
150
|
+
sendWait: (event: E) => Effect.Effect<void, unknown>;
|
|
151
|
+
ask: (event: E) => Effect.Effect<unknown, NoReplyError>;
|
|
152
|
+
getState: Effect.Effect<S, never, never>;
|
|
153
|
+
stateRef: SubscriptionRef.SubscriptionRef<S>;
|
|
154
|
+
isStopped: Effect.Effect<boolean>;
|
|
155
|
+
_queue: Queue.Queue<RuntimeQueuedEvent<E>>;
|
|
156
|
+
_stoppedRef: Ref.Ref<boolean>;
|
|
157
|
+
exitDeferred: Deferred.Deferred<ActorExit<S>, never>;
|
|
158
|
+
actorScope: Scope.CloseableScope;
|
|
159
|
+
}, never, Scope.Scope>;
|
|
141
160
|
//#endregion
|
|
142
161
|
export { ProcessQueuedResult, RuntimeCellResources, RuntimeConfig, RuntimeHandle, RuntimeLifecycleHooks, RuntimeQueuedEvent, createRuntime };
|
|
@@ -72,7 +72,6 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
|
|
|
72
72
|
})
|
|
73
73
|
};
|
|
74
74
|
const stateScopeRef = { current: yield* Scope.make() };
|
|
75
|
-
const backgroundFibers = [];
|
|
76
75
|
const initEvent = { _tag: INTERNAL_INIT_EVENT };
|
|
77
76
|
const ctx = {
|
|
78
77
|
actorId,
|
|
@@ -82,59 +81,71 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
|
|
|
82
81
|
system
|
|
83
82
|
};
|
|
84
83
|
const slots = machine._slots;
|
|
85
|
-
for (const bg of machine.backgroundEffects) {
|
|
86
|
-
const fiber = yield* bg.handler({
|
|
87
|
-
actorId,
|
|
88
|
-
state: machine.initial,
|
|
89
|
-
event: initEvent,
|
|
90
|
-
self,
|
|
91
|
-
slots,
|
|
92
|
-
system
|
|
93
|
-
}).pipe(Effect.provideService(machine.Context, ctx), Effect.forkIn(actorScope));
|
|
94
|
-
backgroundFibers.push(fiber);
|
|
95
|
-
}
|
|
96
|
-
if (lifecycle?.onInitialSpawnEffects !== void 0) yield* lifecycle.onInitialSpawnEffects(machine.initial);
|
|
97
84
|
const loopFiberRef = { current: void 0 };
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
85
|
+
/** Set the exit deferred exactly once. */
|
|
86
|
+
const setExit = (exit) => Deferred.succeed(exitDeferred, exit).pipe(Effect.asVoid);
|
|
87
|
+
const startDeferred = yield* Deferred.make();
|
|
88
|
+
const startedRef = yield* Ref.make(false);
|
|
89
|
+
const start = Effect.gen(function* () {
|
|
90
|
+
if (yield* Ref.getAndSet(startedRef, true)) {
|
|
91
|
+
yield* Deferred.await(startDeferred);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const backgroundFibers = [];
|
|
95
|
+
for (const bg of machine.backgroundEffects) {
|
|
96
|
+
const fiber = yield* bg.handler({
|
|
97
|
+
actorId,
|
|
98
|
+
state: machine.initial,
|
|
99
|
+
event: initEvent,
|
|
100
|
+
self,
|
|
101
|
+
slots,
|
|
102
|
+
system
|
|
103
|
+
}).pipe(Effect.provideService(machine.Context, ctx), Effect.forkIn(actorScope));
|
|
104
|
+
backgroundFibers.push(fiber);
|
|
105
|
+
}
|
|
106
|
+
if (lifecycle?.onInitialSpawnEffects !== void 0) yield* lifecycle.onInitialSpawnEffects(machine.initial);
|
|
107
|
+
const initialSpawnDefectSignal = (cause) => Deferred.succeed(exitDeferred, ActorExit.Defect(cause, "initial-spawn")).pipe(Effect.andThen(Ref.set(stoppedRef, true)), Effect.andThen(Effect.suspend(() => loopFiberRef.current !== void 0 ? Fiber.interrupt(loopFiberRef.current) : Effect.void)), Effect.asVoid);
|
|
108
|
+
yield* runSpawnEffects(machine, machine.initial, initEvent, self, stateScopeRef.current, system, actorId, hooks?.onError, initialSpawnDefectSignal).pipe(Effect.catchAllCause((cause) => {
|
|
109
|
+
return Effect.gen(function* () {
|
|
110
|
+
yield* Ref.set(stoppedRef, true);
|
|
111
|
+
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
112
|
+
yield* Scope.close(actorScope, Exit.void);
|
|
113
|
+
yield* Deferred.succeed(exitDeferred, ActorExit.Defect(cause, "initial-spawn"));
|
|
114
|
+
return yield* Effect.failCause(cause);
|
|
115
|
+
});
|
|
116
|
+
}));
|
|
117
|
+
if (machine.finalStates.has(machine.initial._tag)) {
|
|
118
|
+
if (lifecycle?.onFinal !== void 0) yield* lifecycle.onFinal(machine.initial);
|
|
101
119
|
yield* Ref.set(stoppedRef, true);
|
|
102
120
|
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
103
121
|
yield* Scope.close(actorScope, Exit.void);
|
|
104
|
-
yield*
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
yield*
|
|
113
|
-
|
|
114
|
-
yield*
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (exit._tag === "Failure" && !Cause.isInterruptedOnly(exit.cause)) return setExit(ActorExit.Defect(exit.cause, "background")).pipe(Effect.andThen(Ref.set(stoppedRef, true)), Effect.andThen(Fiber.interrupt(loopFiber)));
|
|
126
|
-
return Effect.never;
|
|
127
|
-
})))).pipe(Effect.forkIn(actorScope));
|
|
128
|
-
yield* Effect.forkDaemon(Effect.gen(function* () {
|
|
129
|
-
const loopExit = yield* Fiber.await(loopFiber);
|
|
130
|
-
if (loopExit._tag === "Success") yield* Scope.close(actorScope, Exit.void);
|
|
131
|
-
else yield* Scope.close(actorScope, loopExit);
|
|
132
|
-
}));
|
|
122
|
+
yield* setExit(ActorExit.Final(machine.initial));
|
|
123
|
+
yield* Deferred.succeed(startDeferred, void 0);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const augmentedHooks = {
|
|
127
|
+
...hooks,
|
|
128
|
+
onSpawnDefect: (cause) => Deferred.succeed(exitDeferred, ActorExit.Defect(cause, "spawn")).pipe(Effect.andThen(Ref.set(stoppedRef, true)), Effect.andThen(Effect.suspend(() => loopFiberRef.current !== void 0 ? Fiber.interrupt(loopFiberRef.current) : Effect.void)), Effect.asVoid)
|
|
129
|
+
};
|
|
130
|
+
const loopFiber = yield* Effect.forkDaemon(runtimeEventLoop(machine, stateRef, eventQueue, stoppedRef, self, stateScopeRef, actorId, system, exitDeferred, augmentedHooks, deferredReplyRef, lifecycle, config.wrapProcess, fork));
|
|
131
|
+
loopFiberRef.current = loopFiber;
|
|
132
|
+
if (backgroundFibers.length > 0) yield* Effect.raceAll(backgroundFibers.map((fiber) => Fiber.await(fiber).pipe(Effect.flatMap((exit) => {
|
|
133
|
+
if (exit._tag === "Failure" && !Cause.isInterruptedOnly(exit.cause)) return setExit(ActorExit.Defect(exit.cause, "background")).pipe(Effect.andThen(Ref.set(stoppedRef, true)), Effect.andThen(Fiber.interrupt(loopFiber)));
|
|
134
|
+
return Effect.never;
|
|
135
|
+
})))).pipe(Effect.forkIn(actorScope));
|
|
136
|
+
yield* Effect.forkDaemon(Effect.gen(function* () {
|
|
137
|
+
const loopExit = yield* Fiber.await(loopFiber);
|
|
138
|
+
if (loopExit._tag === "Success") yield* Scope.close(actorScope, Exit.void);
|
|
139
|
+
else yield* Scope.close(actorScope, loopExit);
|
|
140
|
+
}));
|
|
141
|
+
yield* Deferred.succeed(startDeferred, void 0);
|
|
142
|
+
}).pipe(Effect.catchAllCause((cause) => Deferred.failCause(startDeferred, cause).pipe(Effect.andThen(Effect.failCause(cause)))));
|
|
133
143
|
const stop = Effect.gen(function* () {
|
|
134
144
|
if (yield* Ref.get(stoppedRef)) return;
|
|
135
145
|
if (lifecycle?.onShutdown !== void 0) yield* lifecycle.onShutdown();
|
|
136
146
|
yield* Ref.set(stoppedRef, true);
|
|
137
|
-
|
|
147
|
+
const loopFiber = loopFiberRef.current;
|
|
148
|
+
if (loopFiber !== void 0) yield* Fiber.interrupt(loopFiber);
|
|
138
149
|
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
139
150
|
yield* Scope.close(actorScope, Exit.void);
|
|
140
151
|
yield* setExit(ActorExit.Stopped);
|
|
@@ -142,7 +153,8 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
|
|
|
142
153
|
if (config.skipFinalizer !== true) yield* Effect.addFinalizer(() => stop);
|
|
143
154
|
return {
|
|
144
155
|
...makeHandle(stateRef, stoppedRef, eventQueue, exitDeferred, actorScope),
|
|
145
|
-
stop
|
|
156
|
+
stop,
|
|
157
|
+
start
|
|
146
158
|
};
|
|
147
159
|
});
|
|
148
160
|
/**
|
|
@@ -184,6 +196,7 @@ const makeHandle = (stateRef, stoppedRef, eventQueue, exitDeferred, actorScope)
|
|
|
184
196
|
stateRef,
|
|
185
197
|
isStopped: Ref.get(stoppedRef),
|
|
186
198
|
stop: Effect.void,
|
|
199
|
+
start: Effect.void,
|
|
187
200
|
_queue: eventQueue,
|
|
188
201
|
_stoppedRef: stoppedRef,
|
|
189
202
|
exitDeferred,
|
package/v3/dist/machine.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { Cause, Context, Duration, Effect, Option, Schema, Scope } from "effect"
|
|
|
10
10
|
|
|
11
11
|
//#region src/machine.d.ts
|
|
12
12
|
declare namespace machine_d_exports {
|
|
13
|
-
export { BackgroundEffect, DeferReplyResult, HandlerContext, Machine, MachineRef, MakeConfig,
|
|
13
|
+
export { BackgroundEffect, DeferReplyResult, Durability, DurabilityCommit, HandlerContext, Lifecycle, Machine, MachineRef, MakeConfig, Recovery, RecoveryContext, ReplyResult, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, TransitionHandler, deferReply, findTransitions, make, materializeMachine, replay, reply, spawn };
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
16
|
* Self reference for sending events back to the machine
|
|
@@ -88,32 +88,39 @@ interface TaskOptions<State, Event, SD extends SlotsDef, A, E1, ES, EF> {
|
|
|
88
88
|
readonly name?: string;
|
|
89
89
|
}
|
|
90
90
|
/**
|
|
91
|
-
*
|
|
91
|
+
* Recovery resolves the initial state for a generation. Runs during actor.start.
|
|
92
92
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
93
|
+
* For initial start (generation 0): loads persisted state.
|
|
94
|
+
* For supervision restart (generation 1+): reloads state after crash.
|
|
95
95
|
*/
|
|
96
|
-
interface
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
readonly
|
|
101
|
-
|
|
96
|
+
interface Recovery<S> {
|
|
97
|
+
readonly resolve: (ctx: RecoveryContext<S>) => Effect.Effect<Option.Option<S>>;
|
|
98
|
+
}
|
|
99
|
+
interface RecoveryContext<S> {
|
|
100
|
+
readonly actorId: string;
|
|
101
|
+
readonly generation: number;
|
|
102
|
+
readonly machineInitial: S;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Durability saves state after committed transitions. Runs during runtime.
|
|
106
|
+
*/
|
|
107
|
+
interface Durability<S, E> {
|
|
108
|
+
readonly save: (commit: DurabilityCommit<S, E>) => Effect.Effect<void>;
|
|
102
109
|
readonly shouldSave?: (state: S, previousState: S) => boolean;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
110
|
+
}
|
|
111
|
+
interface DurabilityCommit<S, E> {
|
|
112
|
+
readonly actorId: string;
|
|
113
|
+
readonly generation: number;
|
|
114
|
+
readonly previousState: S;
|
|
115
|
+
readonly nextState: S;
|
|
116
|
+
readonly event: E;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Actor lifecycle configuration.
|
|
120
|
+
*/
|
|
121
|
+
interface Lifecycle<S, E> {
|
|
122
|
+
readonly recovery?: Recovery<S>;
|
|
123
|
+
readonly durability?: Durability<S, E>;
|
|
117
124
|
}
|
|
118
125
|
/**
|
|
119
126
|
* Configuration for `.timeout()` — gen_statem-style state timeouts.
|
|
@@ -335,11 +342,11 @@ declare const make: typeof Machine.make;
|
|
|
335
342
|
* slots: { canRetry: ({ max }) => attempts < max },
|
|
336
343
|
* });
|
|
337
344
|
*
|
|
338
|
-
* // With
|
|
345
|
+
* // With lifecycle (recovery + durability)
|
|
339
346
|
* const actor = yield* Machine.spawn(machine, {
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
* save: (
|
|
347
|
+
* lifecycle: {
|
|
348
|
+
* recovery: { resolve: ({ machineInitial }) => storage.get("actor-state") },
|
|
349
|
+
* durability: { save: ({ nextState }) => storage.set("actor-state", nextState) },
|
|
343
350
|
* },
|
|
344
351
|
* });
|
|
345
352
|
* ```
|
|
@@ -353,7 +360,7 @@ declare const spawn: <S extends {
|
|
|
353
360
|
hydrate?: S;
|
|
354
361
|
slots?: ProvideSlots<SD, any>;
|
|
355
362
|
supervision?: Supervision.Policy;
|
|
356
|
-
|
|
363
|
+
lifecycle?: Lifecycle<S, E>;
|
|
357
364
|
}) => Effect.Effect<ActorRef<S, E>, never, R>;
|
|
358
365
|
declare const replay: {
|
|
359
366
|
<S extends {
|
|
@@ -368,4 +375,4 @@ declare const replay: {
|
|
|
368
375
|
declare const reply: <State, Reply>(state: State, reply: Reply) => ReplyResult<State, Reply>;
|
|
369
376
|
declare const deferReply: <State>(state: State) => DeferReplyResult<State>;
|
|
370
377
|
//#endregion
|
|
371
|
-
export { BackgroundEffect, type DeferReplyResult, HandlerContext, Machine, MachineRef, MakeConfig,
|
|
378
|
+
export { BackgroundEffect, type DeferReplyResult, Durability, DurabilityCommit, HandlerContext, Lifecycle, Machine, MachineRef, MakeConfig, Recovery, RecoveryContext, type ReplyResult, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, TransitionHandler, deferReply, findTransitions, machine_d_exports, make, materializeMachine, replay, reply, spawn };
|