effect-machine 0.14.0 → 0.15.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 +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 +12 -5
- 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
|
@@ -12,14 +12,19 @@ type ReplySchemaSymbol = typeof ReplySchemaSymbol;
|
|
|
12
12
|
type ReplyFields<F extends Schema.Struct.Fields, RS extends Schema.Schema<unknown>> = F & {
|
|
13
13
|
readonly [ReplySchemaSymbol]: RS;
|
|
14
14
|
};
|
|
15
|
+
/**
|
|
16
|
+
* Payload fields that actually flow through constructors and runtime values.
|
|
17
|
+
* Reply schema metadata is type-only and must not leak into payload shapes.
|
|
18
|
+
*/
|
|
19
|
+
type PayloadFields<F extends Schema.Struct.Fields> = { readonly [K in keyof F as K extends ReplySchemaSymbol ? never : K]: F[K] };
|
|
15
20
|
/**
|
|
16
21
|
* Extract the TypeScript type from a TaggedStruct schema
|
|
17
22
|
*/
|
|
18
|
-
type TaggedStructType<Tag extends string, Fields extends Schema.Struct.Fields> = Schema.Schema.Type<Schema.TaggedStruct<Tag, Fields
|
|
23
|
+
type TaggedStructType<Tag extends string, Fields extends Schema.Struct.Fields> = Schema.Schema.Type<Schema.TaggedStruct<Tag, PayloadFields<Fields>>>;
|
|
19
24
|
/**
|
|
20
25
|
* Build variant schemas type from definition
|
|
21
26
|
*/
|
|
22
|
-
type VariantSchemas<D extends Record<string, Schema.Struct.Fields>> = { readonly [K in keyof D & string]: Schema.TaggedStruct<K, D[K]
|
|
27
|
+
type VariantSchemas<D extends Record<string, Schema.Struct.Fields>> = { readonly [K in keyof D & string]: Schema.TaggedStruct<K, PayloadFields<D[K]>> };
|
|
23
28
|
/**
|
|
24
29
|
* Build union type from variant schemas.
|
|
25
30
|
* Reply-bearing variants carry ReplyTypeBrand<R> for ask() inference.
|
|
@@ -50,8 +55,8 @@ type VariantReplyBrand<Fields extends Schema.Struct.Fields> = Fields extends {
|
|
|
50
55
|
*/
|
|
51
56
|
type VariantConstructors<D extends Record<string, Schema.Struct.Fields>, Brand> = { readonly [K in keyof D & string]: IsEmptyFields<D[K]> extends true ? TaggedStructType<K, D[K]> & Brand & VariantReplyBrand<D[K]> & {
|
|
52
57
|
readonly derive: (source: object) => TaggedStructType<K, D[K]> & Brand;
|
|
53
|
-
} : ((args: Schema.Struct.Type<D[K]
|
|
54
|
-
readonly derive: (source: object, partial?: Partial<Schema.Struct.Type<D[K]
|
|
58
|
+
} : ((args: Schema.Struct.Type<PayloadFields<D[K]>>) => TaggedStructType<K, D[K]> & Brand & VariantReplyBrand<D[K]>) & {
|
|
59
|
+
readonly derive: (source: object, partial?: Partial<Schema.Struct.Type<PayloadFields<D[K]>>>) => TaggedStructType<K, D[K]> & Brand;
|
|
55
60
|
readonly _tag: K;
|
|
56
61
|
} };
|
|
57
62
|
/**
|
|
@@ -111,7 +116,9 @@ interface MachineSchemaBase<D extends Record<string, Schema.Struct.Fields>, Bran
|
|
|
111
116
|
* The D type parameter captures the definition, creating a unique brand
|
|
112
117
|
* per distinct schema definition shape.
|
|
113
118
|
*/
|
|
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
|
|
119
|
+
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>> & {
|
|
120
|
+
/** Unbranded schema for persistence — same structure without FullStateBrand. */readonly plain: Schema.Schema<VariantsUnion<D>>;
|
|
121
|
+
};
|
|
115
122
|
/**
|
|
116
123
|
* Schema-first event definition (same structure as state, different brand)
|
|
117
124
|
*
|
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,
|