effect-machine 0.12.0 → 0.13.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 +128 -324
- package/dist/actor.d.ts +42 -27
- package/dist/actor.js +212 -305
- package/dist/cluster/entity-machine.js +20 -9
- package/dist/cluster/to-entity.d.ts +2 -2
- package/dist/errors.d.ts +16 -19
- package/dist/errors.js +3 -5
- package/dist/index.d.ts +5 -4
- package/dist/index.js +4 -3
- package/dist/internal/runtime.d.ts +81 -6
- package/dist/internal/runtime.js +166 -57
- package/dist/internal/transition.d.ts +5 -4
- package/dist/internal/transition.js +9 -9
- package/dist/machine.d.ts +65 -44
- package/dist/machine.js +68 -67
- package/dist/schema.js +1 -1
- package/dist/supervision.d.ts +97 -0
- package/dist/supervision.js +42 -0
- package/dist/testing.d.ts +17 -8
- package/dist/testing.js +22 -25
- package/package.json +5 -5
- package/v3/dist/actor.d.ts +50 -34
- package/v3/dist/actor.js +209 -289
- package/v3/dist/cluster/entity-machine.js +5 -5
- package/v3/dist/errors.d.ts +3 -8
- package/v3/dist/errors.js +2 -4
- package/v3/dist/index.d.ts +5 -4
- package/v3/dist/index.js +3 -2
- package/v3/dist/internal/runtime.d.ts +82 -5
- package/v3/dist/internal/runtime.js +147 -48
- package/v3/dist/internal/transition.d.ts +5 -4
- package/v3/dist/internal/transition.js +8 -8
- package/v3/dist/machine.d.ts +18 -36
- package/v3/dist/machine.js +54 -64
- package/v3/dist/supervision.d.ts +97 -0
- package/v3/dist/supervision.js +42 -0
- package/v3/dist/testing.d.ts +18 -9
- package/v3/dist/testing.js +21 -22
package/v3/dist/machine.d.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Supervision } from "./supervision.js";
|
|
2
2
|
import { ReplyResult, TransitionResult } from "./internal/utils.js";
|
|
3
3
|
import { BrandedEvent, BrandedState, ExtractReply, TaggedOrConstructor } from "./internal/brands.js";
|
|
4
4
|
import { MachineEventSchema, MachineStateSchema, VariantsUnion } from "./schema.js";
|
|
5
5
|
import { DuplicateActorError } from "./errors.js";
|
|
6
|
+
import { EffectHandlers, EffectSlots, EffectsDef, EffectsSchema, GuardHandlers, GuardSlots, GuardsDef, GuardsSchema, MachineContext } from "./slot.js";
|
|
6
7
|
import { findTransitions } from "./internal/transition.js";
|
|
7
8
|
import { ActorRef, ActorSystem } from "./actor.js";
|
|
8
9
|
import { Cause, Context, Duration, Effect, Schema, Scope } from "effect";
|
|
9
10
|
|
|
10
11
|
//#region src/machine.d.ts
|
|
11
12
|
declare namespace machine_d_exports {
|
|
12
|
-
export { BackgroundEffect,
|
|
13
|
+
export { BackgroundEffect, HandlerContext, Machine, MachineRef, MakeConfig, ProvideHandlers, ReplyResult, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, TransitionHandler, findTransitions, make, materializeMachine, replay, reply, spawn };
|
|
13
14
|
}
|
|
14
15
|
/**
|
|
15
16
|
* Self reference for sending events back to the machine
|
|
@@ -22,7 +23,7 @@ interface MachineRef<Event> {
|
|
|
22
23
|
readonly _tag: string;
|
|
23
24
|
}, E2 extends {
|
|
24
25
|
readonly _tag: string;
|
|
25
|
-
}, R2>(id: string, machine:
|
|
26
|
+
}, R2>(id: string, machine: Machine<S2, E2, R2, any, any, any, any>) => Effect.Effect<ActorRef<S2, E2>, DuplicateActorError, R2>;
|
|
26
27
|
}
|
|
27
28
|
/**
|
|
28
29
|
* Handler context passed to transition handlers
|
|
@@ -93,9 +94,6 @@ interface TimeoutConfig<State, Event> {
|
|
|
93
94
|
/** Event to send when the timer fires. Static or derived from current state. */
|
|
94
95
|
readonly event: Event | ((state: State) => Event);
|
|
95
96
|
}
|
|
96
|
-
type IsAny<T> = 0 extends 1 & T ? true : false;
|
|
97
|
-
type IsUnknown<T> = unknown extends T ? ([T] extends [unknown] ? true : false) : false;
|
|
98
|
-
type NormalizeR<T> = IsAny<T> extends true ? T : IsUnknown<T> extends true ? never : T;
|
|
99
97
|
interface MakeConfig<SD extends Record<string, Schema.Struct.Fields>, ED extends Record<string, Schema.Struct.Fields>, S extends BrandedState, E extends BrandedEvent, GD extends GuardsDef, EFD extends EffectsDef> {
|
|
100
98
|
readonly state: MachineStateSchema<SD> & {
|
|
101
99
|
Type: S;
|
|
@@ -115,22 +113,14 @@ type HasEffectKeys<EFD extends EffectsDef> = [keyof EFD] extends [never] ? false
|
|
|
115
113
|
type SlotContext<State, Event> = MachineContext<State, Event, MachineRef<Event>>;
|
|
116
114
|
/** Combined handlers for build() - guards and effects only */
|
|
117
115
|
type ProvideHandlers<State, Event, GD extends GuardsDef, EFD extends EffectsDef, R> = (HasGuardKeys<GD> extends true ? GuardHandlers<GD, SlotContext<State, Event>, R> : object) & (HasEffectKeys<EFD> extends true ? EffectHandlers<EFD, SlotContext<State, Event>, R> : object);
|
|
118
|
-
/** Whether the machine has any guard or effect slots */
|
|
119
|
-
type HasSlots<GD extends GuardsDef, EFD extends EffectsDef> = HasGuardKeys<GD> extends true ? true : HasEffectKeys<EFD>;
|
|
120
116
|
/**
|
|
121
|
-
*
|
|
117
|
+
* Bind slot handlers to a machine, returning a fresh copy with handlers installed.
|
|
118
|
+
* If no handlers provided and machine has no slots, returns the machine as-is.
|
|
119
|
+
* Validates that all required slots are provided and no extra slots are given.
|
|
122
120
|
*
|
|
123
|
-
*
|
|
124
|
-
* accepted by `Machine.spawn` and `ActorSystem.spawn` (regular overload).
|
|
125
|
-
* Testing utilities (`simulate`, `createTestHarness`, etc.) still accept `Machine`.
|
|
121
|
+
* @internal — used by spawn, replay, simulate, test harness, entity-machine
|
|
126
122
|
*/
|
|
127
|
-
declare
|
|
128
|
-
/** @internal */
|
|
129
|
-
readonly _inner: Machine<State, Event, R, any, any, any, any>;
|
|
130
|
-
/** @internal */
|
|
131
|
-
constructor(machine: Machine<State, Event, R, any, any, any, any>);
|
|
132
|
-
get initial(): State;
|
|
133
|
-
}
|
|
123
|
+
declare const materializeMachine: <S, E, R, GD extends GuardsDef, EFD extends EffectsDef>(machine: Machine<S, E, R, any, any, GD, EFD>, handlers?: Record<string, any>) => Machine<S, E, never, any, any, GD, EFD>;
|
|
134
124
|
/**
|
|
135
125
|
* Machine definition with fluent builder API.
|
|
136
126
|
*
|
|
@@ -306,13 +296,6 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
|
|
|
306
296
|
*/
|
|
307
297
|
postpone<NS extends VariantsUnion<_SD> & BrandedState>(state: TaggedOrConstructor<NS>, events: TaggedOrConstructor<VariantsUnion<_ED> & BrandedEvent> | ReadonlyArray<TaggedOrConstructor<VariantsUnion<_ED> & BrandedEvent>>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
|
|
308
298
|
final<NS extends VariantsUnion<_SD> & BrandedState>(state: TaggedOrConstructor<NS>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
|
|
309
|
-
/**
|
|
310
|
-
* Finalize the machine. Returns a `BuiltMachine` — the only type accepted by `Machine.spawn`.
|
|
311
|
-
*
|
|
312
|
-
* - Machines with slots: pass implementations as the first argument.
|
|
313
|
-
* - Machines without slots: call with no arguments.
|
|
314
|
-
*/
|
|
315
|
-
build<R2 = never>(...args: HasSlots<GD, EFD> extends true ? [handlers: ProvideHandlers<State, Event, GD, EFD, R2>] : [handlers?: ProvideHandlers<State, Event, GD, EFD, R2>]): BuiltMachine<State, Event, R | NormalizeR<R2>>;
|
|
316
299
|
static make<SD extends Record<string, Schema.Struct.Fields>, ED extends Record<string, Schema.Struct.Fields>, S extends BrandedState, E extends BrandedEvent, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(config: MakeConfig<SD, ED, S, E, GD, EFD>): Machine<S, E, never, SD, ED, GD, EFD>;
|
|
317
300
|
}
|
|
318
301
|
declare class TransitionScope<State, Event, R, _SD extends Record<string, Schema.Struct.Fields>, _ED extends Record<string, Schema.Struct.Fields>, GD extends GuardsDef, EFD extends EffectsDef, SelectedState extends VariantsUnion<_SD> & BrandedState> {
|
|
@@ -323,34 +306,33 @@ declare class TransitionScope<State, Event, R, _SD extends Record<string, Schema
|
|
|
323
306
|
reenter<NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(event: TaggedOrConstructor<NE>, handler: TransitionHandler<SelectedState, NE, RS, GD, EFD, never, ExtractReply<NE>>): TransitionScope<State, Event, R, _SD, _ED, GD, EFD, SelectedState>;
|
|
324
307
|
}
|
|
325
308
|
declare const make: typeof Machine.make;
|
|
309
|
+
type AnyMachine<S, E, R> = Machine<S, E, R, any, any, any, any>;
|
|
326
310
|
/**
|
|
327
|
-
* Spawn an actor from a
|
|
311
|
+
* Spawn an actor from a machine.
|
|
328
312
|
*
|
|
329
313
|
* Options:
|
|
330
314
|
* - `id` — custom actor ID (default: random)
|
|
331
315
|
* - `hydrate` — restore from a previously-saved state snapshot.
|
|
332
|
-
*
|
|
333
|
-
* for that state (timers, scoped resources, etc.). Transition history
|
|
334
|
-
* is not replayed — only the current state's entry effects run.
|
|
335
|
-
*
|
|
336
|
-
* Persistence is composed in userland by observing `actor.changes`
|
|
337
|
-
* and saving snapshots to your own storage.
|
|
316
|
+
* - `slots` — slot handler implementations for slotful machines.
|
|
338
317
|
*/
|
|
339
318
|
declare const spawn: <S extends {
|
|
340
319
|
readonly _tag: string;
|
|
341
320
|
}, E extends {
|
|
342
321
|
readonly _tag: string;
|
|
343
|
-
}, R>(machine:
|
|
322
|
+
}, R>(machine: AnyMachine<S, E, R>, options?: string | {
|
|
344
323
|
id?: string;
|
|
345
324
|
hydrate?: S;
|
|
325
|
+
slots?: Record<string, any>;
|
|
326
|
+
supervision?: Supervision.Policy;
|
|
346
327
|
}) => Effect.Effect<ActorRef<S, E>, never, R>;
|
|
347
328
|
declare const replay: <S extends {
|
|
348
329
|
readonly _tag: string;
|
|
349
330
|
}, E extends {
|
|
350
331
|
readonly _tag: string;
|
|
351
|
-
}, R>(machine:
|
|
332
|
+
}, R>(machine: AnyMachine<S, E, R>, events: ReadonlyArray<E>, options?: {
|
|
352
333
|
from?: S;
|
|
334
|
+
slots?: Record<string, any>;
|
|
353
335
|
}) => Effect.Effect<S, never, R>;
|
|
354
336
|
declare const reply: <State, Reply>(state: State, reply: Reply) => ReplyResult<State, Reply>;
|
|
355
337
|
//#endregion
|
|
356
|
-
export { BackgroundEffect,
|
|
338
|
+
export { BackgroundEffect, HandlerContext, Machine, MachineRef, MakeConfig, ProvideHandlers, type ReplyResult, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, TransitionHandler, findTransitions, machine_d_exports, make, materializeMachine, replay, reply, spawn };
|
package/v3/dist/machine.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { __exportAll } from "./_virtual/_rolldown/runtime.js";
|
|
2
|
-
import { Inspector } from "./inspection.js";
|
|
3
2
|
import { getTag, makeReply, stubSystem } from "./internal/utils.js";
|
|
4
3
|
import { ProvisionValidationError, SlotProvisionError } from "./errors.js";
|
|
4
|
+
import { findTransitions, invalidateIndex, resolveTransition, runTransitionHandler, shouldPostpone } from "./internal/transition.js";
|
|
5
5
|
import { emitWithTimestamp } from "./internal/inspection.js";
|
|
6
|
+
import { Inspector } from "./inspection.js";
|
|
6
7
|
import { MachineContextTag } from "./slot.js";
|
|
7
|
-
import { findTransitions, invalidateIndex, resolveTransition, runTransitionHandler, shouldPostpone } from "./internal/transition.js";
|
|
8
8
|
import { createActor } from "./actor.js";
|
|
9
9
|
import { Cause, Effect, Exit, Option, Scope } from "effect";
|
|
10
10
|
//#region src/machine.ts
|
|
11
11
|
var machine_exports = /* @__PURE__ */ __exportAll({
|
|
12
|
-
BuiltMachine: () => BuiltMachine,
|
|
13
12
|
Machine: () => Machine,
|
|
14
13
|
findTransitions: () => findTransitions,
|
|
15
14
|
make: () => make,
|
|
15
|
+
materializeMachine: () => materializeMachine,
|
|
16
16
|
replay: () => replay,
|
|
17
17
|
reply: () => reply,
|
|
18
18
|
spawn: () => spawn
|
|
@@ -27,22 +27,49 @@ const emitTaskInspection = (input) => Effect.flatMap(Effect.serviceOptional(Insp
|
|
|
27
27
|
timestamp
|
|
28
28
|
})));
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
30
|
+
* Bind slot handlers to a machine, returning a fresh copy with handlers installed.
|
|
31
|
+
* If no handlers provided and machine has no slots, returns the machine as-is.
|
|
32
|
+
* Validates that all required slots are provided and no extra slots are given.
|
|
31
33
|
*
|
|
32
|
-
*
|
|
33
|
-
* accepted by `Machine.spawn` and `ActorSystem.spawn` (regular overload).
|
|
34
|
-
* Testing utilities (`simulate`, `createTestHarness`, etc.) still accept `Machine`.
|
|
34
|
+
* @internal — used by spawn, replay, simulate, test harness, entity-machine
|
|
35
35
|
*/
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
const materializeMachine = (machine, handlers) => {
|
|
37
|
+
if (handlers === void 0) {
|
|
38
|
+
const hasGuards = machine._guardsSchema !== void 0 && Object.keys(machine._guardsSchema.definitions).length > 0;
|
|
39
|
+
const hasEffects = machine._effectsSchema !== void 0 && Object.keys(machine._effectsSchema.definitions).length > 0;
|
|
40
|
+
if (hasGuards || hasEffects) {
|
|
41
|
+
const missing = [];
|
|
42
|
+
if (machine._guardsSchema !== void 0) missing.push(...Object.keys(machine._guardsSchema.definitions));
|
|
43
|
+
if (machine._effectsSchema !== void 0) missing.push(...Object.keys(machine._effectsSchema.definitions));
|
|
44
|
+
throw new ProvisionValidationError({
|
|
45
|
+
missing,
|
|
46
|
+
extra: []
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return machine;
|
|
45
50
|
}
|
|
51
|
+
const requiredSlots = /* @__PURE__ */ new Set();
|
|
52
|
+
if (machine._guardsSchema !== void 0) for (const name of Object.keys(machine._guardsSchema.definitions)) requiredSlots.add(name);
|
|
53
|
+
if (machine._effectsSchema !== void 0) for (const name of Object.keys(machine._effectsSchema.definitions)) requiredSlots.add(name);
|
|
54
|
+
const providedSlots = new Set(Object.keys(handlers));
|
|
55
|
+
const missing = [];
|
|
56
|
+
const extra = [];
|
|
57
|
+
for (const name of requiredSlots) if (!providedSlots.has(name)) missing.push(name);
|
|
58
|
+
for (const name of providedSlots) if (!requiredSlots.has(name)) extra.push(name);
|
|
59
|
+
if (missing.length > 0 || extra.length > 0) throw new ProvisionValidationError({
|
|
60
|
+
missing,
|
|
61
|
+
extra
|
|
62
|
+
});
|
|
63
|
+
const result = new Machine(machine.initial, machine.stateSchema, machine.eventSchema, machine._guardsSchema, machine._effectsSchema);
|
|
64
|
+
result._transitions = [...machine._transitions];
|
|
65
|
+
result._finalStates = new Set(machine._finalStates);
|
|
66
|
+
result._spawnEffects = [...machine._spawnEffects];
|
|
67
|
+
result._backgroundEffects = [...machine._backgroundEffects];
|
|
68
|
+
result._postponeRules = [...machine._postponeRules];
|
|
69
|
+
result._replySchemas = machine._replySchemas;
|
|
70
|
+
if (machine._guardsSchema !== void 0) for (const name of Object.keys(machine._guardsSchema.definitions)) result._guardHandlers.set(name, handlers[name]);
|
|
71
|
+
if (machine._effectsSchema !== void 0) for (const name of Object.keys(machine._effectsSchema.definitions)) result._effectHandlers.set(name, handlers[name]);
|
|
72
|
+
return result;
|
|
46
73
|
};
|
|
47
74
|
/**
|
|
48
75
|
* Machine definition with fluent builder API.
|
|
@@ -350,41 +377,6 @@ var Machine = class Machine {
|
|
|
350
377
|
this._finalStates.add(stateTag);
|
|
351
378
|
return this;
|
|
352
379
|
}
|
|
353
|
-
/**
|
|
354
|
-
* Finalize the machine. Returns a `BuiltMachine` — the only type accepted by `Machine.spawn`.
|
|
355
|
-
*
|
|
356
|
-
* - Machines with slots: pass implementations as the first argument.
|
|
357
|
-
* - Machines without slots: call with no arguments.
|
|
358
|
-
*/
|
|
359
|
-
build(...args) {
|
|
360
|
-
const handlers = args[0];
|
|
361
|
-
if (handlers !== void 0) {
|
|
362
|
-
const requiredSlots = /* @__PURE__ */ new Set();
|
|
363
|
-
if (this._guardsSchema !== void 0) for (const name of Object.keys(this._guardsSchema.definitions)) requiredSlots.add(name);
|
|
364
|
-
if (this._effectsSchema !== void 0) for (const name of Object.keys(this._effectsSchema.definitions)) requiredSlots.add(name);
|
|
365
|
-
const providedSlots = new Set(Object.keys(handlers));
|
|
366
|
-
const missing = [];
|
|
367
|
-
const extra = [];
|
|
368
|
-
for (const name of requiredSlots) if (!providedSlots.has(name)) missing.push(name);
|
|
369
|
-
for (const name of providedSlots) if (!requiredSlots.has(name)) extra.push(name);
|
|
370
|
-
if (missing.length > 0 || extra.length > 0) throw new ProvisionValidationError({
|
|
371
|
-
missing,
|
|
372
|
-
extra
|
|
373
|
-
});
|
|
374
|
-
const result = new Machine(this.initial, this.stateSchema, this.eventSchema, this._guardsSchema, this._effectsSchema);
|
|
375
|
-
result._transitions = [...this._transitions];
|
|
376
|
-
result._finalStates = new Set(this._finalStates);
|
|
377
|
-
result._spawnEffects = [...this._spawnEffects];
|
|
378
|
-
result._backgroundEffects = [...this._backgroundEffects];
|
|
379
|
-
result._postponeRules = [...this._postponeRules];
|
|
380
|
-
result._replySchemas = this._replySchemas;
|
|
381
|
-
const anyHandlers = handlers;
|
|
382
|
-
if (this._guardsSchema !== void 0) for (const name of Object.keys(this._guardsSchema.definitions)) result._guardHandlers.set(name, anyHandlers[name]);
|
|
383
|
-
if (this._effectsSchema !== void 0) for (const name of Object.keys(this._effectsSchema.definitions)) result._effectHandlers.set(name, anyHandlers[name]);
|
|
384
|
-
return new BuiltMachine(result);
|
|
385
|
-
}
|
|
386
|
-
return new BuiltMachine(this);
|
|
387
|
-
}
|
|
388
380
|
static make(config) {
|
|
389
381
|
return new Machine(config.initial, config.state, config.event, config.guards, config.effects);
|
|
390
382
|
}
|
|
@@ -405,27 +397,25 @@ var TransitionScope = class {
|
|
|
405
397
|
};
|
|
406
398
|
const make = Machine.make;
|
|
407
399
|
/**
|
|
408
|
-
* Spawn an actor from a
|
|
400
|
+
* Spawn an actor from a machine.
|
|
409
401
|
*
|
|
410
402
|
* Options:
|
|
411
403
|
* - `id` — custom actor ID (default: random)
|
|
412
404
|
* - `hydrate` — restore from a previously-saved state snapshot.
|
|
413
|
-
*
|
|
414
|
-
* for that state (timers, scoped resources, etc.). Transition history
|
|
415
|
-
* is not replayed — only the current state's entry effects run.
|
|
416
|
-
*
|
|
417
|
-
* Persistence is composed in userland by observing `actor.changes`
|
|
418
|
-
* and saving snapshots to your own storage.
|
|
405
|
+
* - `slots` — slot handler implementations for slotful machines.
|
|
419
406
|
*/
|
|
420
|
-
const spawn = Effect.fn("effect-machine.spawn")(function* (
|
|
421
|
-
const opts = typeof
|
|
422
|
-
const actor = yield* createActor(opts?.id ?? `actor-${Math.random().toString(36).slice(2)}`,
|
|
407
|
+
const spawn = Effect.fn("effect-machine.spawn")(function* (machine, options) {
|
|
408
|
+
const opts = typeof options === "string" ? { id: options } : options;
|
|
409
|
+
const actor = yield* createActor(opts?.id ?? `actor-${Math.random().toString(36).slice(2)}`, materializeMachine(machine, opts?.slots), {
|
|
410
|
+
initialState: opts?.hydrate,
|
|
411
|
+
supervision: opts?.supervision
|
|
412
|
+
});
|
|
423
413
|
const maybeScope = yield* Effect.serviceOption(Scope.Scope);
|
|
424
414
|
if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, actor.stop);
|
|
425
415
|
return actor;
|
|
426
416
|
});
|
|
427
|
-
const replay = Effect.fn("effect-machine.replay")(function* (
|
|
428
|
-
const machine =
|
|
417
|
+
const replay = Effect.fn("effect-machine.replay")(function* (input, events, options) {
|
|
418
|
+
const machine = materializeMachine(input, options?.slots);
|
|
429
419
|
let state = options?.from ?? machine.initial;
|
|
430
420
|
const hasPostponeRules = machine.postponeRules.length > 0;
|
|
431
421
|
const postponed = [];
|
|
@@ -469,4 +459,4 @@ const replay = Effect.fn("effect-machine.replay")(function* (built, events, opti
|
|
|
469
459
|
});
|
|
470
460
|
const reply = makeReply;
|
|
471
461
|
//#endregion
|
|
472
|
-
export {
|
|
462
|
+
export { Machine, findTransitions, machine_exports, make, materializeMachine, replay, reply, spawn };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Cause, Duration, Schedule } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/supervision.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Where in the actor lifecycle a defect occurred.
|
|
6
|
+
*
|
|
7
|
+
* - `transition` — during event handler execution
|
|
8
|
+
* - `spawn` — during state spawn effect execution
|
|
9
|
+
* - `background` — in a background effect fiber
|
|
10
|
+
* - `initial-spawn` — during initial state spawn effects (before event loop)
|
|
11
|
+
*/
|
|
12
|
+
type DefectPhase = "transition" | "spawn" | "background" | "initial-spawn";
|
|
13
|
+
/**
|
|
14
|
+
* Terminal exit reason for an actor generation.
|
|
15
|
+
*
|
|
16
|
+
* - `Final` — machine reached a final state normally
|
|
17
|
+
* - `Stopped` — explicit `actor.stop` or `actor.drain`
|
|
18
|
+
* - `Defect` — unhandled error in the runtime
|
|
19
|
+
*/
|
|
20
|
+
type ActorExit<S> = {
|
|
21
|
+
readonly _tag: "Final";
|
|
22
|
+
readonly state: S;
|
|
23
|
+
} | {
|
|
24
|
+
readonly _tag: "Stopped";
|
|
25
|
+
} | {
|
|
26
|
+
readonly _tag: "Defect";
|
|
27
|
+
readonly cause: Cause.Cause<unknown>;
|
|
28
|
+
readonly phase: DefectPhase;
|
|
29
|
+
};
|
|
30
|
+
/** Constructors for ActorExit */
|
|
31
|
+
declare const ActorExit: {
|
|
32
|
+
readonly Final: <S>(state: S) => ActorExit<S>;
|
|
33
|
+
readonly Stopped: ActorExit<never>;
|
|
34
|
+
readonly Defect: <S = never>(cause: Cause.Cause<unknown>, phase: DefectPhase) => ActorExit<S>;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Phase state for supervised actors. Serializes concurrent stop/restart/drain.
|
|
38
|
+
*
|
|
39
|
+
* Transitions:
|
|
40
|
+
* - `Running` → crash → `Restarting` → new runtime → `Running`
|
|
41
|
+
* - `Running` → explicit stop/drain → `Stopping` → `Terminated`
|
|
42
|
+
* - `Restarting` → explicit stop → `Stopping` → `Terminated`
|
|
43
|
+
*
|
|
44
|
+
* @internal
|
|
45
|
+
*/
|
|
46
|
+
type CellPhase<S> = {
|
|
47
|
+
readonly _tag: "Running";
|
|
48
|
+
readonly generation: number;
|
|
49
|
+
} | {
|
|
50
|
+
readonly _tag: "Restarting";
|
|
51
|
+
readonly generation: number;
|
|
52
|
+
} | {
|
|
53
|
+
readonly _tag: "Stopping";
|
|
54
|
+
} | {
|
|
55
|
+
readonly _tag: "Terminated";
|
|
56
|
+
readonly exit: ActorExit<S>;
|
|
57
|
+
};
|
|
58
|
+
declare namespace Supervision {
|
|
59
|
+
/**
|
|
60
|
+
* Supervision policy for actor restart behavior.
|
|
61
|
+
*
|
|
62
|
+
* `schedule` controls restart timing and budget — schedule exhaustion means terminal stop.
|
|
63
|
+
* `shouldRestart` optionally classifies defects — return `false` to stop immediately
|
|
64
|
+
* without consuming the schedule.
|
|
65
|
+
*/
|
|
66
|
+
interface Policy {
|
|
67
|
+
/** Schedule that controls restart timing. Exhaustion = terminal stop. */
|
|
68
|
+
readonly schedule: Schedule.Schedule<unknown>;
|
|
69
|
+
/**
|
|
70
|
+
* Optional classifier: given a defect exit, decide whether to restart or stop immediately.
|
|
71
|
+
* Default: always restart (let schedule handle budget).
|
|
72
|
+
*/
|
|
73
|
+
readonly shouldRestart?: (exit: Extract<ActorExit<unknown>, {
|
|
74
|
+
readonly _tag: "Defect";
|
|
75
|
+
}>) => boolean;
|
|
76
|
+
}
|
|
77
|
+
/** No supervision — crashes are terminal. */
|
|
78
|
+
const none: Policy;
|
|
79
|
+
/**
|
|
80
|
+
* Restart on defect with max restarts within a window, optional backoff.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```ts
|
|
84
|
+
* Supervision.restart() // unlimited restarts, no backoff
|
|
85
|
+
* Supervision.restart({ maxRestarts: 3 }) // 3 restarts then terminal
|
|
86
|
+
* Supervision.restart({ maxRestarts: 3, within: "1 minute" }) // 3 within 1 min
|
|
87
|
+
* Supervision.restart({ backoff: Schedule.exponential("100 millis") })
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
const restart: (options?: {
|
|
91
|
+
readonly maxRestarts?: number;
|
|
92
|
+
readonly within?: Duration.DurationInput;
|
|
93
|
+
readonly backoff?: Schedule.Schedule<unknown>;
|
|
94
|
+
}) => Policy;
|
|
95
|
+
}
|
|
96
|
+
//#endregion
|
|
97
|
+
export { ActorExit, CellPhase, DefectPhase, Supervision };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Schedule } from "effect";
|
|
2
|
+
//#region src/supervision.ts
|
|
3
|
+
/**
|
|
4
|
+
* Supervision types for actor lifecycle management.
|
|
5
|
+
*
|
|
6
|
+
* Core concepts:
|
|
7
|
+
* - `ActorExit<S>` — why an actor stopped (final, explicit stop, or defect)
|
|
8
|
+
* - `DefectPhase` — where in the lifecycle a defect occurred
|
|
9
|
+
* - `Supervision.Policy` — Schedule-based restart policy
|
|
10
|
+
* - `CellPhase<S>` — internal phase machine for serializing stop/restart/drain
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
/** Constructors for ActorExit */
|
|
15
|
+
const ActorExit = {
|
|
16
|
+
Final: (state) => ({
|
|
17
|
+
_tag: "Final",
|
|
18
|
+
state
|
|
19
|
+
}),
|
|
20
|
+
Stopped: { _tag: "Stopped" },
|
|
21
|
+
Defect: (cause, phase) => ({
|
|
22
|
+
_tag: "Defect",
|
|
23
|
+
cause,
|
|
24
|
+
phase
|
|
25
|
+
})
|
|
26
|
+
};
|
|
27
|
+
let Supervision;
|
|
28
|
+
(function(_Supervision) {
|
|
29
|
+
_Supervision.none = { schedule: Schedule.recurs(0) };
|
|
30
|
+
_Supervision.restart = (options) => {
|
|
31
|
+
let schedule = Schedule.forever;
|
|
32
|
+
if (options?.maxRestarts !== void 0) {
|
|
33
|
+
const recurs = Schedule.recurs(options.maxRestarts);
|
|
34
|
+
if (options.within !== void 0) schedule = Schedule.intersect(recurs, Schedule.windowed(options.within));
|
|
35
|
+
else schedule = recurs;
|
|
36
|
+
}
|
|
37
|
+
if (options?.backoff !== void 0) schedule = Schedule.intersect(schedule, options.backoff);
|
|
38
|
+
return { schedule };
|
|
39
|
+
};
|
|
40
|
+
})(Supervision || (Supervision = {}));
|
|
41
|
+
//#endregion
|
|
42
|
+
export { ActorExit, Supervision };
|
package/v3/dist/testing.d.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { EffectsDef, GuardsDef, MachineContext } from "./slot.js";
|
|
2
1
|
import { AssertionError } from "./errors.js";
|
|
3
|
-
import {
|
|
2
|
+
import { EffectsDef, GuardsDef, MachineContext } from "./slot.js";
|
|
3
|
+
import { Machine, MachineRef } from "./machine.js";
|
|
4
4
|
import { Effect, SubscriptionRef } from "effect";
|
|
5
5
|
|
|
6
6
|
//#region src/testing.d.ts
|
|
7
|
-
|
|
8
|
-
type MachineInput<S, E, R, GD extends GuardsDef, EFD extends EffectsDef> = Machine<S, E, R, any, any, GD, EFD> | BuiltMachine<S, E, R>;
|
|
7
|
+
type MachineInput<S, E, R, GD extends GuardsDef, EFD extends EffectsDef> = Machine<S, E, R, any, any, GD, EFD>;
|
|
9
8
|
/**
|
|
10
9
|
* Result of simulating events through a machine
|
|
11
10
|
*/
|
|
@@ -37,7 +36,9 @@ declare const simulate: <S extends {
|
|
|
37
36
|
readonly _tag: string;
|
|
38
37
|
}, E extends {
|
|
39
38
|
readonly _tag: string;
|
|
40
|
-
}, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[]
|
|
39
|
+
}, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], options?: {
|
|
40
|
+
slots?: Record<string, any>;
|
|
41
|
+
} | undefined) => Effect.Effect<{
|
|
41
42
|
states: S[];
|
|
42
43
|
finalState: S;
|
|
43
44
|
}, never, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
|
|
@@ -48,7 +49,9 @@ declare const assertReaches: <S extends {
|
|
|
48
49
|
readonly _tag: string;
|
|
49
50
|
}, E extends {
|
|
50
51
|
readonly _tag: string;
|
|
51
|
-
}, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], expectedTag: string
|
|
52
|
+
}, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], expectedTag: string, options?: {
|
|
53
|
+
slots?: Record<string, any>;
|
|
54
|
+
} | undefined) => Effect.Effect<S, AssertionError, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
|
|
52
55
|
/**
|
|
53
56
|
* Assert that a machine follows a specific path of state tags
|
|
54
57
|
*
|
|
@@ -65,7 +68,9 @@ declare const assertPath: <S extends {
|
|
|
65
68
|
readonly _tag: string;
|
|
66
69
|
}, E extends {
|
|
67
70
|
readonly _tag: string;
|
|
68
|
-
}, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], expectedPath: readonly string[]
|
|
71
|
+
}, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], expectedPath: readonly string[], options?: {
|
|
72
|
+
slots?: Record<string, any>;
|
|
73
|
+
} | undefined) => Effect.Effect<{
|
|
69
74
|
states: S[];
|
|
70
75
|
finalState: S;
|
|
71
76
|
}, AssertionError, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
|
|
@@ -86,7 +91,9 @@ declare const assertNeverReaches: <S extends {
|
|
|
86
91
|
readonly _tag: string;
|
|
87
92
|
}, E extends {
|
|
88
93
|
readonly _tag: string;
|
|
89
|
-
}, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], forbiddenTag: string
|
|
94
|
+
}, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], forbiddenTag: string, options?: {
|
|
95
|
+
slots?: Record<string, any>;
|
|
96
|
+
} | undefined) => Effect.Effect<{
|
|
90
97
|
states: S[];
|
|
91
98
|
finalState: S;
|
|
92
99
|
}, AssertionError, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
|
|
@@ -133,7 +140,9 @@ declare const createTestHarness: <S extends {
|
|
|
133
140
|
readonly _tag: string;
|
|
134
141
|
}, E extends {
|
|
135
142
|
readonly _tag: string;
|
|
136
|
-
}, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, options?: TestHarnessOptions<S, E>
|
|
143
|
+
}, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, options?: (TestHarnessOptions<S, E> & {
|
|
144
|
+
slots?: Record<string, any>;
|
|
145
|
+
}) | undefined) => Effect.Effect<{
|
|
137
146
|
state: SubscriptionRef.SubscriptionRef<S>;
|
|
138
147
|
send: (event: E) => Effect.Effect<S, never, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
|
|
139
148
|
getState: Effect.Effect<S, never, never>;
|
package/v3/dist/testing.js
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { stubSystem } from "./internal/utils.js";
|
|
2
2
|
import { AssertionError } from "./errors.js";
|
|
3
|
-
import { BuiltMachine } from "./machine.js";
|
|
4
3
|
import { executeTransition, shouldPostpone } from "./internal/transition.js";
|
|
4
|
+
import { materializeMachine } from "./machine.js";
|
|
5
5
|
import { Effect, SubscriptionRef } from "effect";
|
|
6
6
|
//#region src/testing.ts
|
|
7
|
+
/** Create a dummy MachineRef for testing utilities (no real send/spawn). */
|
|
8
|
+
const makeDummySelf = (label) => {
|
|
9
|
+
const dummySend = Effect.fn(`effect-machine.testing.${label}.send`)((_event) => Effect.void);
|
|
10
|
+
return {
|
|
11
|
+
send: dummySend,
|
|
12
|
+
cast: dummySend,
|
|
13
|
+
spawn: () => Effect.die(`spawn not supported in ${label}`)
|
|
14
|
+
};
|
|
15
|
+
};
|
|
7
16
|
/**
|
|
8
17
|
* Simulate a sequence of events through a machine without running an actor.
|
|
9
18
|
* Useful for testing state transitions in isolation.
|
|
@@ -24,14 +33,9 @@ import { Effect, SubscriptionRef } from "effect";
|
|
|
24
33
|
* expect(result.states).toHaveLength(3) // Idle -> Loading -> Success
|
|
25
34
|
* ```
|
|
26
35
|
*/
|
|
27
|
-
const simulate = Effect.fn("effect-machine.simulate")(function* (input, events) {
|
|
28
|
-
const machine = input
|
|
29
|
-
const
|
|
30
|
-
const dummySelf = {
|
|
31
|
-
send: dummySend,
|
|
32
|
-
cast: dummySend,
|
|
33
|
-
spawn: () => Effect.die("spawn not supported in simulation")
|
|
34
|
-
};
|
|
36
|
+
const simulate = Effect.fn("effect-machine.simulate")(function* (input, events, options) {
|
|
37
|
+
const machine = materializeMachine(input, options?.slots);
|
|
38
|
+
const dummySelf = makeDummySelf("simulate");
|
|
35
39
|
let currentState = machine.initial;
|
|
36
40
|
const states = [currentState];
|
|
37
41
|
const hasPostponeRules = machine.postponeRules.length > 0;
|
|
@@ -73,8 +77,8 @@ const simulate = Effect.fn("effect-machine.simulate")(function* (input, events)
|
|
|
73
77
|
/**
|
|
74
78
|
* Assert that a machine can reach a specific state given a sequence of events
|
|
75
79
|
*/
|
|
76
|
-
const assertReaches = Effect.fn("effect-machine.assertReaches")(function* (input, events, expectedTag) {
|
|
77
|
-
const result = yield* simulate(input, events);
|
|
80
|
+
const assertReaches = Effect.fn("effect-machine.assertReaches")(function* (input, events, expectedTag, options) {
|
|
81
|
+
const result = yield* simulate(input, events, options);
|
|
78
82
|
if (result.finalState._tag !== expectedTag) return yield* new AssertionError({ message: `Expected final state "${expectedTag}" but got "${result.finalState._tag}". States visited: ${result.states.map((s) => s._tag).join(" -> ")}` });
|
|
79
83
|
return result.finalState;
|
|
80
84
|
});
|
|
@@ -90,8 +94,8 @@ const assertReaches = Effect.fn("effect-machine.assertReaches")(function* (input
|
|
|
90
94
|
* )
|
|
91
95
|
* ```
|
|
92
96
|
*/
|
|
93
|
-
const assertPath = Effect.fn("effect-machine.assertPath")(function* (input, events, expectedPath) {
|
|
94
|
-
const result = yield* simulate(input, events);
|
|
97
|
+
const assertPath = Effect.fn("effect-machine.assertPath")(function* (input, events, expectedPath, options) {
|
|
98
|
+
const result = yield* simulate(input, events, options);
|
|
95
99
|
const actualPath = result.states.map((s) => s._tag);
|
|
96
100
|
if (actualPath.length !== expectedPath.length) return yield* new AssertionError({ message: `Path length mismatch. Expected ${expectedPath.length} states but got ${actualPath.length}.\nExpected: ${expectedPath.join(" -> ")}\nActual: ${actualPath.join(" -> ")}` });
|
|
97
101
|
for (let i = 0; i < expectedPath.length; i++) if (actualPath[i] !== expectedPath[i]) return yield* new AssertionError({ message: `Path mismatch at position ${i}. Expected "${expectedPath[i]}" but got "${actualPath[i]}".\nExpected: ${expectedPath.join(" -> ")}\nActual: ${actualPath.join(" -> ")}` });
|
|
@@ -110,8 +114,8 @@ const assertPath = Effect.fn("effect-machine.assertPath")(function* (input, even
|
|
|
110
114
|
* )
|
|
111
115
|
* ```
|
|
112
116
|
*/
|
|
113
|
-
const assertNeverReaches = Effect.fn("effect-machine.assertNeverReaches")(function* (input, events, forbiddenTag) {
|
|
114
|
-
const result = yield* simulate(input, events);
|
|
117
|
+
const assertNeverReaches = Effect.fn("effect-machine.assertNeverReaches")(function* (input, events, forbiddenTag, options) {
|
|
118
|
+
const result = yield* simulate(input, events, options);
|
|
115
119
|
const visitedIndex = result.states.findIndex((s) => s._tag === forbiddenTag);
|
|
116
120
|
if (visitedIndex !== -1) return yield* new AssertionError({ message: `Machine reached forbidden state "${forbiddenTag}" at position ${visitedIndex}.\nStates visited: ${result.states.map((s) => s._tag).join(" -> ")}` });
|
|
117
121
|
return result;
|
|
@@ -138,13 +142,8 @@ const assertNeverReaches = Effect.fn("effect-machine.assertNeverReaches")(functi
|
|
|
138
142
|
* ```
|
|
139
143
|
*/
|
|
140
144
|
const createTestHarness = Effect.fn("effect-machine.createTestHarness")(function* (input, options) {
|
|
141
|
-
const machine = input
|
|
142
|
-
const
|
|
143
|
-
const dummySelf = {
|
|
144
|
-
send: dummySend,
|
|
145
|
-
cast: dummySend,
|
|
146
|
-
spawn: () => Effect.die("spawn not supported in test harness")
|
|
147
|
-
};
|
|
145
|
+
const machine = materializeMachine(input, options?.slots);
|
|
146
|
+
const dummySelf = makeDummySelf("harness");
|
|
148
147
|
const stateRef = yield* SubscriptionRef.make(machine.initial);
|
|
149
148
|
const hasPostponeRules = machine.postponeRules.length > 0;
|
|
150
149
|
const postponed = [];
|