effect-machine 0.9.0 → 0.11.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 +118 -55
- package/dist/actor.d.ts +77 -179
- package/dist/actor.js +161 -113
- package/dist/cluster/entity-machine.js +5 -3
- package/dist/errors.d.ts +12 -1
- package/dist/errors.js +8 -1
- package/dist/index.d.ts +4 -8
- package/dist/index.js +2 -7
- package/dist/internal/transition.d.ts +27 -3
- package/dist/internal/transition.js +38 -9
- package/dist/internal/utils.d.ts +7 -2
- package/dist/internal/utils.js +1 -5
- package/dist/machine.d.ts +94 -35
- package/dist/machine.js +128 -13
- package/dist/testing.js +57 -3
- package/package.json +10 -9
- package/v3/dist/actor.d.ts +210 -0
- package/{dist-v3 → v3/dist}/actor.js +198 -117
- package/{dist-v3 → v3/dist}/cluster/entity-machine.d.ts +1 -1
- package/{dist-v3 → v3/dist}/cluster/entity-machine.js +8 -6
- package/{dist-v3 → v3/dist}/cluster/to-entity.d.ts +1 -1
- package/{dist-v3 → v3/dist}/cluster/to-entity.js +1 -1
- package/v3/dist/errors.d.ts +76 -0
- package/{dist-v3 → v3/dist}/errors.js +9 -2
- package/v3/dist/index.d.ts +9 -0
- package/v3/dist/index.js +8 -0
- package/{dist-v3 → v3/dist}/inspection.d.ts +53 -8
- package/v3/dist/inspection.js +156 -0
- package/{dist-v3 → v3/dist}/internal/brands.d.ts +1 -1
- package/{dist-v3 → v3/dist}/internal/inspection.d.ts +1 -1
- package/v3/dist/internal/inspection.js +20 -0
- package/{dist-v3 → v3/dist}/internal/transition.d.ts +35 -11
- package/{dist-v3 → v3/dist}/internal/transition.js +47 -15
- package/{dist-v3 → v3/dist}/internal/utils.d.ts +9 -4
- package/{dist-v3 → v3/dist}/internal/utils.js +2 -6
- package/{dist-v3 → v3/dist}/machine.d.ts +113 -40
- package/{dist-v3 → v3/dist}/machine.js +191 -15
- package/{dist-v3 → v3/dist}/schema.d.ts +1 -1
- package/{dist-v3 → v3/dist}/schema.js +5 -2
- package/{dist-v3 → v3/dist}/slot.d.ts +4 -3
- package/{dist-v3 → v3/dist}/slot.js +1 -1
- package/{dist-v3 → v3/dist}/testing.d.ts +14 -8
- package/{dist-v3 → v3/dist}/testing.js +60 -6
- package/dist/persistence/adapter.d.ts +0 -135
- package/dist/persistence/adapter.js +0 -25
- package/dist/persistence/adapters/in-memory.d.ts +0 -32
- package/dist/persistence/adapters/in-memory.js +0 -174
- package/dist/persistence/index.d.ts +0 -5
- package/dist/persistence/index.js +0 -5
- package/dist/persistence/persistent-actor.d.ts +0 -50
- package/dist/persistence/persistent-actor.js +0 -368
- package/dist/persistence/persistent-machine.d.ts +0 -105
- package/dist/persistence/persistent-machine.js +0 -22
- package/dist-v3/actor.d.ts +0 -291
- package/dist-v3/errors.d.ts +0 -27
- package/dist-v3/index.d.ts +0 -12
- package/dist-v3/index.js +0 -13
- package/dist-v3/inspection.js +0 -48
- package/dist-v3/internal/inspection.js +0 -13
- package/dist-v3/persistence/adapter.d.ts +0 -125
- package/dist-v3/persistence/adapter.js +0 -25
- package/dist-v3/persistence/adapters/in-memory.d.ts +0 -32
- package/dist-v3/persistence/adapters/in-memory.js +0 -174
- package/dist-v3/persistence/index.d.ts +0 -5
- package/dist-v3/persistence/index.js +0 -5
- package/dist-v3/persistence/persistent-actor.d.ts +0 -49
- package/dist-v3/persistence/persistent-actor.js +0 -365
- package/dist-v3/persistence/persistent-machine.d.ts +0 -105
- package/dist-v3/persistence/persistent-machine.js +0 -22
- /package/{dist-v3 → v3/dist}/_virtual/_rolldown/runtime.js +0 -0
- /package/{dist-v3 → v3/dist}/cluster/index.d.ts +0 -0
- /package/{dist-v3 → v3/dist}/cluster/index.js +0 -0
- /package/{dist-v3 → v3/dist}/internal/brands.js +0 -0
|
@@ -18,7 +18,7 @@ import { Cause, Effect, Exit, Scope } from "effect";
|
|
|
18
18
|
*
|
|
19
19
|
* Used by:
|
|
20
20
|
* - executeTransition (actor event loop, testing)
|
|
21
|
-
* -
|
|
21
|
+
* - Machine.replay (event sourcing restore)
|
|
22
22
|
*
|
|
23
23
|
* @internal
|
|
24
24
|
*/
|
|
@@ -37,8 +37,18 @@ const runTransitionHandler = Effect.fn("effect-machine.runTransitionHandler")(fu
|
|
|
37
37
|
guards,
|
|
38
38
|
effects
|
|
39
39
|
};
|
|
40
|
-
const
|
|
41
|
-
|
|
40
|
+
const raw = transition.handler(handlerCtx);
|
|
41
|
+
const resolved = isEffect(raw) ? yield* raw.pipe(Effect.provideService(machine.Context, ctx)) : raw;
|
|
42
|
+
if (resolved !== null && typeof resolved === "object" && "state" in resolved && "reply" in resolved && !("_tag" in resolved)) return {
|
|
43
|
+
newState: resolved.state,
|
|
44
|
+
hasReply: true,
|
|
45
|
+
reply: resolved.reply
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
newState: resolved,
|
|
49
|
+
hasReply: false,
|
|
50
|
+
reply: void 0
|
|
51
|
+
};
|
|
42
52
|
});
|
|
43
53
|
/**
|
|
44
54
|
* Execute a transition for a given state and event.
|
|
@@ -56,15 +66,28 @@ const executeTransition = Effect.fn("effect-machine.executeTransition")(function
|
|
|
56
66
|
if (transition === void 0) return {
|
|
57
67
|
newState: currentState,
|
|
58
68
|
transitioned: false,
|
|
59
|
-
reenter: false
|
|
69
|
+
reenter: false,
|
|
70
|
+
hasReply: false,
|
|
71
|
+
reply: void 0
|
|
60
72
|
};
|
|
73
|
+
const { newState, hasReply, reply } = yield* runTransitionHandler(machine, transition, currentState, event, self, system, actorId);
|
|
61
74
|
return {
|
|
62
|
-
newState
|
|
75
|
+
newState,
|
|
63
76
|
transitioned: true,
|
|
64
|
-
reenter: transition.reenter === true
|
|
77
|
+
reenter: transition.reenter === true,
|
|
78
|
+
hasReply,
|
|
79
|
+
reply
|
|
65
80
|
};
|
|
66
81
|
});
|
|
67
82
|
/**
|
|
83
|
+
* Check if an event should be postponed in the current state.
|
|
84
|
+
* @internal
|
|
85
|
+
*/
|
|
86
|
+
const shouldPostpone = (machine, stateTag, eventTag) => {
|
|
87
|
+
for (const rule of machine.postponeRules) if (rule.stateTag === stateTag && rule.eventTag === eventTag) return true;
|
|
88
|
+
return false;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
68
91
|
* Process a single event through the machine.
|
|
69
92
|
*
|
|
70
93
|
* Handles:
|
|
@@ -93,7 +116,10 @@ const processEventCore = Effect.fn("effect-machine.processEventCore")(function*
|
|
|
93
116
|
previousState: currentState,
|
|
94
117
|
transitioned: false,
|
|
95
118
|
lifecycleRan: false,
|
|
96
|
-
isFinal: false
|
|
119
|
+
isFinal: false,
|
|
120
|
+
hasReply: false,
|
|
121
|
+
reply: void 0,
|
|
122
|
+
postponed: false
|
|
97
123
|
};
|
|
98
124
|
const newState = result.newState;
|
|
99
125
|
const runLifecycle = newState._tag !== currentState._tag || result.reenter;
|
|
@@ -109,7 +135,10 @@ const processEventCore = Effect.fn("effect-machine.processEventCore")(function*
|
|
|
109
135
|
previousState: currentState,
|
|
110
136
|
transitioned: true,
|
|
111
137
|
lifecycleRan: runLifecycle,
|
|
112
|
-
isFinal: machine.finalStates.has(newState._tag)
|
|
138
|
+
isFinal: machine.finalStates.has(newState._tag),
|
|
139
|
+
hasReply: result.hasReply,
|
|
140
|
+
reply: result.reply,
|
|
141
|
+
postponed: false
|
|
113
142
|
};
|
|
114
143
|
});
|
|
115
144
|
/**
|
|
@@ -236,4 +265,4 @@ const findSpawnEffects = (machine, stateTag) => {
|
|
|
236
265
|
return getIndex(machine).spawn.get(stateTag) ?? [];
|
|
237
266
|
};
|
|
238
267
|
//#endregion
|
|
239
|
-
export { executeTransition, findSpawnEffects, findTransitions, invalidateIndex, processEventCore, resolveTransition, runSpawnEffects, runTransitionHandler };
|
|
268
|
+
export { executeTransition, findSpawnEffects, findTransitions, invalidateIndex, processEventCore, resolveTransition, runSpawnEffects, runTransitionHandler, shouldPostpone };
|
package/dist/internal/utils.d.ts
CHANGED
|
@@ -26,7 +26,12 @@ type TaggedConstructor<T extends {
|
|
|
26
26
|
/**
|
|
27
27
|
* Transition handler result - either a new state or Effect producing one
|
|
28
28
|
*/
|
|
29
|
-
|
|
29
|
+
/** Reply tuple returned from transition handlers for ask support */
|
|
30
|
+
interface TransitionReply<State> {
|
|
31
|
+
readonly state: State;
|
|
32
|
+
readonly reply: unknown;
|
|
33
|
+
}
|
|
34
|
+
type TransitionResult<State, R> = State | TransitionReply<State> | Effect.Effect<State | TransitionReply<State>, never, R>;
|
|
30
35
|
/**
|
|
31
36
|
* Internal event tags used for lifecycle effect contexts.
|
|
32
37
|
* Prefixed with $ to distinguish from user events.
|
|
@@ -57,4 +62,4 @@ declare const isEffect: (value: unknown) => value is Effect.Effect<unknown, unkn
|
|
|
57
62
|
*/
|
|
58
63
|
declare const stubSystem: ActorSystem;
|
|
59
64
|
//#endregion
|
|
60
|
-
export { ArgsOf, INTERNAL_ENTER_EVENT, INTERNAL_INIT_EVENT, InstanceOf, TagOf, TaggedConstructor, TransitionResult, getTag, isEffect, stubSystem };
|
|
65
|
+
export { ArgsOf, INTERNAL_ENTER_EVENT, INTERNAL_INIT_EVENT, InstanceOf, TagOf, TaggedConstructor, TransitionReply, TransitionResult, getTag, isEffect, stubSystem };
|
package/dist/internal/utils.js
CHANGED
|
@@ -37,17 +37,13 @@ const isEffect = Effect.isEffect;
|
|
|
37
37
|
*/
|
|
38
38
|
const stubSystem = {
|
|
39
39
|
spawn: () => Effect.die("spawn not supported in stub system"),
|
|
40
|
-
restore: () => Effect.die("restore not supported in stub system"),
|
|
41
40
|
get: () => Effect.die("get not supported in stub system"),
|
|
42
41
|
stop: () => Effect.die("stop not supported in stub system"),
|
|
43
42
|
events: Stream.empty,
|
|
44
43
|
get actors() {
|
|
45
44
|
return /* @__PURE__ */ new Map();
|
|
46
45
|
},
|
|
47
|
-
subscribe: () => () => {}
|
|
48
|
-
listPersisted: () => Effect.die("listPersisted not supported in stub system"),
|
|
49
|
-
restoreMany: () => Effect.die("restoreMany not supported in stub system"),
|
|
50
|
-
restoreAll: () => Effect.die("restoreAll not supported in stub system")
|
|
46
|
+
subscribe: () => () => {}
|
|
51
47
|
};
|
|
52
48
|
//#endregion
|
|
53
49
|
export { INTERNAL_ENTER_EVENT, INTERNAL_INIT_EVENT, getTag, isEffect, stubSystem };
|
package/dist/machine.d.ts
CHANGED
|
@@ -2,21 +2,22 @@ import { EffectHandlers, EffectSlots, EffectsDef, EffectsSchema, GuardHandlers,
|
|
|
2
2
|
import { TransitionResult } from "./internal/utils.js";
|
|
3
3
|
import { BrandedEvent, BrandedState, TaggedOrConstructor } from "./internal/brands.js";
|
|
4
4
|
import { MachineEventSchema, MachineStateSchema, VariantsUnion } from "./schema.js";
|
|
5
|
-
import { PersistenceConfig, PersistentMachine } from "./persistence/persistent-machine.js";
|
|
6
5
|
import { DuplicateActorError } from "./errors.js";
|
|
7
6
|
import { findTransitions } from "./internal/transition.js";
|
|
8
7
|
import { ActorRef, ActorSystem } from "./actor.js";
|
|
9
|
-
import { Cause,
|
|
8
|
+
import { Cause, Duration, Effect, Schema, Scope, ServiceMap } from "effect";
|
|
10
9
|
|
|
11
10
|
//#region src/machine.d.ts
|
|
12
11
|
declare namespace machine_d_exports {
|
|
13
|
-
export { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig,
|
|
12
|
+
export { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, ProvideHandlers, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, TransitionHandler, findTransitions, make, replay, spawn };
|
|
14
13
|
}
|
|
15
14
|
/**
|
|
16
15
|
* Self reference for sending events back to the machine
|
|
17
16
|
*/
|
|
18
17
|
interface MachineRef<Event> {
|
|
19
18
|
readonly send: (event: Event) => Effect.Effect<void>;
|
|
19
|
+
/** Fire-and-forget alias for send (OTP gen_server:cast). */
|
|
20
|
+
readonly cast: (event: Event) => Effect.Effect<void>;
|
|
20
21
|
readonly spawn: <S2 extends {
|
|
21
22
|
readonly _tag: string;
|
|
22
23
|
}, E2 extends {
|
|
@@ -73,19 +74,23 @@ interface SpawnEffect<State, Event, ED extends EffectsDef, R> {
|
|
|
73
74
|
interface BackgroundEffect<State, Event, ED extends EffectsDef, R> {
|
|
74
75
|
readonly handler: StateEffectHandler<State, Event, ED, R>;
|
|
75
76
|
}
|
|
76
|
-
/** Options for `persist` */
|
|
77
|
-
interface PersistOptions {
|
|
78
|
-
readonly snapshotSchedule: Schedule.Schedule<unknown, {
|
|
79
|
-
readonly _tag: string;
|
|
80
|
-
}>;
|
|
81
|
-
readonly journalEvents: boolean;
|
|
82
|
-
readonly machineType?: string;
|
|
83
|
-
}
|
|
84
77
|
interface TaskOptions<State, Event, ED extends EffectsDef, A, E1, ES, EF> {
|
|
85
78
|
readonly onSuccess: (value: A, ctx: StateHandlerContext<State, Event, ED>) => ES;
|
|
86
79
|
readonly onFailure?: (cause: Cause.Cause<E1>, ctx: StateHandlerContext<State, Event, ED>) => EF;
|
|
87
80
|
readonly name?: string;
|
|
88
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Configuration for `.timeout()` — gen_statem-style state timeouts.
|
|
84
|
+
*
|
|
85
|
+
* Entering the state starts a timer. Leaving cancels it.
|
|
86
|
+
* `.reenter()` restarts the timer with fresh state values.
|
|
87
|
+
*/
|
|
88
|
+
interface TimeoutConfig<State, Event> {
|
|
89
|
+
/** Duration before firing. Static or derived from current state. */
|
|
90
|
+
readonly duration: Duration.Input | ((state: State) => Duration.Input);
|
|
91
|
+
/** Event to send when the timer fires. Static or derived from current state. */
|
|
92
|
+
readonly event: Event | ((state: State) => Event);
|
|
93
|
+
}
|
|
89
94
|
type IsAny<T> = 0 extends 1 & T ? true : false;
|
|
90
95
|
type IsUnknown<T> = unknown extends T ? ([T] extends [unknown] ? true : false) : false;
|
|
91
96
|
type NormalizeR<T> = IsAny<T> extends true ? T : IsUnknown<T> extends true ? never : T;
|
|
@@ -123,11 +128,6 @@ declare class BuiltMachine<State, Event, R = never> {
|
|
|
123
128
|
/** @internal */
|
|
124
129
|
constructor(machine: Machine<State, Event, R, any, any, any, any>);
|
|
125
130
|
get initial(): State;
|
|
126
|
-
persist(config: PersistOptions): PersistentMachine<State & {
|
|
127
|
-
readonly _tag: string;
|
|
128
|
-
}, Event & {
|
|
129
|
-
readonly _tag: string;
|
|
130
|
-
}, R>;
|
|
131
131
|
}
|
|
132
132
|
/**
|
|
133
133
|
* Machine definition with fluent builder API.
|
|
@@ -152,6 +152,11 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
|
|
|
152
152
|
/** @internal */
|
|
153
153
|
readonly _finalStates: Set<string>;
|
|
154
154
|
/** @internal */
|
|
155
|
+
readonly _postponeRules: Array<{
|
|
156
|
+
readonly stateTag: string;
|
|
157
|
+
readonly eventTag: string;
|
|
158
|
+
}>;
|
|
159
|
+
/** @internal */
|
|
155
160
|
readonly _guardsSchema?: GuardsSchema<GD>;
|
|
156
161
|
/** @internal */
|
|
157
162
|
readonly _effectsSchema?: EffectsSchema<EFD>;
|
|
@@ -175,6 +180,10 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
|
|
|
175
180
|
get spawnEffects(): ReadonlyArray<SpawnEffect<State, Event, EFD, R>>;
|
|
176
181
|
get backgroundEffects(): ReadonlyArray<BackgroundEffect<State, Event, EFD, R>>;
|
|
177
182
|
get finalStates(): ReadonlySet<string>;
|
|
183
|
+
get postponeRules(): ReadonlyArray<{
|
|
184
|
+
readonly stateTag: string;
|
|
185
|
+
readonly eventTag: string;
|
|
186
|
+
}>;
|
|
178
187
|
get guardsSchema(): GuardsSchema<GD> | undefined;
|
|
179
188
|
get effectsSchema(): EffectsSchema<EFD> | undefined;
|
|
180
189
|
/** @internal */
|
|
@@ -230,6 +239,28 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
|
|
|
230
239
|
* Interrupts do not emit failure events.
|
|
231
240
|
*/
|
|
232
241
|
task<NS extends VariantsUnion<_SD> & BrandedState, A, E1, ES extends VariantsUnion<_ED> & BrandedEvent, EF extends VariantsUnion<_ED> & BrandedEvent>(state: TaggedOrConstructor<NS>, run: (ctx: StateHandlerContext<NS, VariantsUnion<_ED> & BrandedEvent, EFD>) => Effect.Effect<A, E1, Scope.Scope>, options: TaskOptions<NS, VariantsUnion<_ED> & BrandedEvent, EFD, A, E1, ES, EF>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
|
|
242
|
+
/**
|
|
243
|
+
* State timeout — gen_statem's `state_timeout`.
|
|
244
|
+
*
|
|
245
|
+
* Entering the state starts a timer. Leaving cancels it (via state scope).
|
|
246
|
+
* `.reenter()` restarts the timer with fresh state values.
|
|
247
|
+
* Compiles to `.task()` internally — preserves `@machine.task` inspection events.
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* ```ts
|
|
251
|
+
* machine
|
|
252
|
+
* .timeout(State.Loading, {
|
|
253
|
+
* duration: Duration.seconds(30),
|
|
254
|
+
* event: Event.Timeout,
|
|
255
|
+
* })
|
|
256
|
+
* // Dynamic duration from state
|
|
257
|
+
* .timeout(State.Retrying, {
|
|
258
|
+
* duration: (state) => Duration.seconds(state.backoff),
|
|
259
|
+
* event: Event.GiveUp,
|
|
260
|
+
* })
|
|
261
|
+
* ```
|
|
262
|
+
*/
|
|
263
|
+
timeout<NS extends VariantsUnion<_SD> & BrandedState>(state: TaggedOrConstructor<NS>, config: TimeoutConfig<NS, VariantsUnion<_ED> & BrandedEvent>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
|
|
233
264
|
/**
|
|
234
265
|
* Machine-lifetime effect that is forked on actor spawn and runs until the actor stops.
|
|
235
266
|
* Use effect slots defined via `Slot.Effects` for the actual work.
|
|
@@ -251,6 +282,24 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
|
|
|
251
282
|
* ```
|
|
252
283
|
*/
|
|
253
284
|
background(handler: StateEffectHandler<State, Event, EFD, Scope.Scope>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
|
|
285
|
+
/**
|
|
286
|
+
* Postpone events — gen_statem's event postpone.
|
|
287
|
+
*
|
|
288
|
+
* When a matching event arrives in the given state, it is buffered instead of
|
|
289
|
+
* processed. After the next state transition (tag change), all buffered events
|
|
290
|
+
* are drained through the loop in FIFO order.
|
|
291
|
+
*
|
|
292
|
+
* Reply-bearing events (from `call`/`ask`) in the postpone buffer are settled
|
|
293
|
+
* with `ActorStoppedError` on stop/interrupt/final-state.
|
|
294
|
+
*
|
|
295
|
+
* @example
|
|
296
|
+
* ```ts
|
|
297
|
+
* machine
|
|
298
|
+
* .postpone(State.Connecting, Event.Data) // single event
|
|
299
|
+
* .postpone(State.Connecting, [Event.Data, Event.Cmd]) // multiple events
|
|
300
|
+
* ```
|
|
301
|
+
*/
|
|
302
|
+
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>;
|
|
254
303
|
final<NS extends VariantsUnion<_SD> & BrandedState>(state: TaggedOrConstructor<NS>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
|
|
255
304
|
/**
|
|
256
305
|
* Finalize the machine. Returns a `BuiltMachine` — the only type accepted by `Machine.spawn`.
|
|
@@ -259,12 +308,6 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
|
|
|
259
308
|
* - Machines without slots: call with no arguments.
|
|
260
309
|
*/
|
|
261
310
|
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>>;
|
|
262
|
-
/** @internal Persist from raw Machine — prefer BuiltMachine.persist() */
|
|
263
|
-
persist(config: PersistOptions): PersistentMachine<State & {
|
|
264
|
-
readonly _tag: string;
|
|
265
|
-
}, Event & {
|
|
266
|
-
readonly _tag: string;
|
|
267
|
-
}, R>;
|
|
268
311
|
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>;
|
|
269
312
|
}
|
|
270
313
|
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> {
|
|
@@ -275,17 +318,33 @@ declare class TransitionScope<State, Event, R, _SD extends Record<string, Schema
|
|
|
275
318
|
reenter<NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(event: TaggedOrConstructor<NE>, handler: TransitionHandler<SelectedState, NE, RS, GD, EFD, never>): TransitionScope<State, Event, R, _SD, _ED, GD, EFD, SelectedState>;
|
|
276
319
|
}
|
|
277
320
|
declare const make: typeof Machine.make;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
321
|
+
/**
|
|
322
|
+
* Spawn an actor from a built machine.
|
|
323
|
+
*
|
|
324
|
+
* Options:
|
|
325
|
+
* - `id` — custom actor ID (default: random)
|
|
326
|
+
* - `hydrate` — restore from a previously-saved state snapshot.
|
|
327
|
+
* The actor starts in the hydrated state and re-runs spawn effects
|
|
328
|
+
* for that state (timers, scoped resources, etc.). Transition history
|
|
329
|
+
* is not replayed — only the current state's entry effects run.
|
|
330
|
+
*
|
|
331
|
+
* Persistence is composed in userland by observing `actor.changes`
|
|
332
|
+
* and saving snapshots to your own storage.
|
|
333
|
+
*/
|
|
334
|
+
declare const spawn: <S extends {
|
|
335
|
+
readonly _tag: string;
|
|
336
|
+
}, E extends {
|
|
337
|
+
readonly _tag: string;
|
|
338
|
+
}, R>(machine: BuiltMachine<S, E, R>, idOrOptions?: string | {
|
|
339
|
+
id?: string;
|
|
340
|
+
hydrate?: S;
|
|
341
|
+
}) => Effect.Effect<ActorRef<S, E>, never, R>;
|
|
342
|
+
declare const replay: <S extends {
|
|
343
|
+
readonly _tag: string;
|
|
344
|
+
}, E extends {
|
|
345
|
+
readonly _tag: string;
|
|
346
|
+
}, R>(machine: BuiltMachine<S, E, R>, events: ReadonlyArray<E>, options?: {
|
|
347
|
+
from?: S;
|
|
348
|
+
}) => Effect.Effect<S, never, R>;
|
|
290
349
|
//#endregion
|
|
291
|
-
export { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig,
|
|
350
|
+
export { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, ProvideHandlers, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, TransitionHandler, findTransitions, machine_d_exports, make, replay, spawn };
|
package/dist/machine.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { __exportAll } from "./_virtual/_rolldown/runtime.js";
|
|
2
2
|
import { Inspector } from "./inspection.js";
|
|
3
|
-
import { getTag } from "./internal/utils.js";
|
|
3
|
+
import { getTag, stubSystem } from "./internal/utils.js";
|
|
4
4
|
import { ProvisionValidationError, SlotProvisionError } from "./errors.js";
|
|
5
|
-
import { persist } from "./persistence/persistent-machine.js";
|
|
6
5
|
import { emitWithTimestamp } from "./internal/inspection.js";
|
|
7
6
|
import { MachineContextTag } from "./slot.js";
|
|
8
|
-
import { findTransitions, invalidateIndex } from "./internal/transition.js";
|
|
7
|
+
import { findTransitions, invalidateIndex, resolveTransition, runTransitionHandler, shouldPostpone } from "./internal/transition.js";
|
|
9
8
|
import { createActor } from "./actor.js";
|
|
10
9
|
import { Cause, Effect, Exit, Option, Scope } from "effect";
|
|
11
10
|
//#region src/machine.ts
|
|
@@ -14,6 +13,7 @@ var machine_exports = /* @__PURE__ */ __exportAll({
|
|
|
14
13
|
Machine: () => Machine,
|
|
15
14
|
findTransitions: () => findTransitions,
|
|
16
15
|
make: () => make,
|
|
16
|
+
replay: () => replay,
|
|
17
17
|
spawn: () => spawn
|
|
18
18
|
});
|
|
19
19
|
const emitTaskInspection = (input) => Effect.flatMap(Effect.serviceOption(Inspector), (inspector) => Option.isNone(inspector) ? Effect.void : emitWithTimestamp(inspector.value, (timestamp) => ({
|
|
@@ -42,9 +42,6 @@ var BuiltMachine = class {
|
|
|
42
42
|
get initial() {
|
|
43
43
|
return this._inner.initial;
|
|
44
44
|
}
|
|
45
|
-
persist(config) {
|
|
46
|
-
return this._inner.persist(config);
|
|
47
|
-
}
|
|
48
45
|
};
|
|
49
46
|
/**
|
|
50
47
|
* Machine definition with fluent builder API.
|
|
@@ -64,6 +61,7 @@ var Machine = class Machine {
|
|
|
64
61
|
/** @internal */ _spawnEffects;
|
|
65
62
|
/** @internal */ _backgroundEffects;
|
|
66
63
|
/** @internal */ _finalStates;
|
|
64
|
+
/** @internal */ _postponeRules;
|
|
67
65
|
/** @internal */ _guardsSchema;
|
|
68
66
|
/** @internal */ _effectsSchema;
|
|
69
67
|
/** @internal */ _guardHandlers;
|
|
@@ -88,6 +86,9 @@ var Machine = class Machine {
|
|
|
88
86
|
get finalStates() {
|
|
89
87
|
return this._finalStates;
|
|
90
88
|
}
|
|
89
|
+
get postponeRules() {
|
|
90
|
+
return this._postponeRules;
|
|
91
|
+
}
|
|
91
92
|
get guardsSchema() {
|
|
92
93
|
return this._guardsSchema;
|
|
93
94
|
}
|
|
@@ -101,6 +102,7 @@ var Machine = class Machine {
|
|
|
101
102
|
this._spawnEffects = [];
|
|
102
103
|
this._backgroundEffects = [];
|
|
103
104
|
this._finalStates = /* @__PURE__ */ new Set();
|
|
105
|
+
this._postponeRules = [];
|
|
104
106
|
this._guardsSchema = guardsSchema;
|
|
105
107
|
this._effectsSchema = effectsSchema;
|
|
106
108
|
this._guardHandlers = /* @__PURE__ */ new Map();
|
|
@@ -259,6 +261,36 @@ var Machine = class Machine {
|
|
|
259
261
|
return this.spawn(state, handler);
|
|
260
262
|
}
|
|
261
263
|
/**
|
|
264
|
+
* State timeout — gen_statem's `state_timeout`.
|
|
265
|
+
*
|
|
266
|
+
* Entering the state starts a timer. Leaving cancels it (via state scope).
|
|
267
|
+
* `.reenter()` restarts the timer with fresh state values.
|
|
268
|
+
* Compiles to `.task()` internally — preserves `@machine.task` inspection events.
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* ```ts
|
|
272
|
+
* machine
|
|
273
|
+
* .timeout(State.Loading, {
|
|
274
|
+
* duration: Duration.seconds(30),
|
|
275
|
+
* event: Event.Timeout,
|
|
276
|
+
* })
|
|
277
|
+
* // Dynamic duration from state
|
|
278
|
+
* .timeout(State.Retrying, {
|
|
279
|
+
* duration: (state) => Duration.seconds(state.backoff),
|
|
280
|
+
* event: Event.GiveUp,
|
|
281
|
+
* })
|
|
282
|
+
* ```
|
|
283
|
+
*/
|
|
284
|
+
timeout(state, config) {
|
|
285
|
+
const stateTag = getTag(state);
|
|
286
|
+
const resolveDuration = typeof config.duration === "function" ? config.duration : () => config.duration;
|
|
287
|
+
const resolveEvent = typeof config.event === "function" ? config.event : () => config.event;
|
|
288
|
+
return this.task(state, (ctx) => Effect.sleep(resolveDuration(ctx.state)), {
|
|
289
|
+
onSuccess: (_, ctx) => resolveEvent(ctx.state),
|
|
290
|
+
name: `$timeout:${stateTag}`
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
262
294
|
* Machine-lifetime effect that is forked on actor spawn and runs until the actor stops.
|
|
263
295
|
* Use effect slots defined via `Slot.Effects` for the actual work.
|
|
264
296
|
*
|
|
@@ -282,6 +314,35 @@ var Machine = class Machine {
|
|
|
282
314
|
this._backgroundEffects.push({ handler });
|
|
283
315
|
return this;
|
|
284
316
|
}
|
|
317
|
+
/**
|
|
318
|
+
* Postpone events — gen_statem's event postpone.
|
|
319
|
+
*
|
|
320
|
+
* When a matching event arrives in the given state, it is buffered instead of
|
|
321
|
+
* processed. After the next state transition (tag change), all buffered events
|
|
322
|
+
* are drained through the loop in FIFO order.
|
|
323
|
+
*
|
|
324
|
+
* Reply-bearing events (from `call`/`ask`) in the postpone buffer are settled
|
|
325
|
+
* with `ActorStoppedError` on stop/interrupt/final-state.
|
|
326
|
+
*
|
|
327
|
+
* @example
|
|
328
|
+
* ```ts
|
|
329
|
+
* machine
|
|
330
|
+
* .postpone(State.Connecting, Event.Data) // single event
|
|
331
|
+
* .postpone(State.Connecting, [Event.Data, Event.Cmd]) // multiple events
|
|
332
|
+
* ```
|
|
333
|
+
*/
|
|
334
|
+
postpone(state, events) {
|
|
335
|
+
const stateTag = getTag(state);
|
|
336
|
+
const eventList = Array.isArray(events) ? events : [events];
|
|
337
|
+
for (const ev of eventList) {
|
|
338
|
+
const eventTag = getTag(ev);
|
|
339
|
+
this._postponeRules.push({
|
|
340
|
+
stateTag,
|
|
341
|
+
eventTag
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
return this;
|
|
345
|
+
}
|
|
285
346
|
final(state) {
|
|
286
347
|
const stateTag = getTag(state);
|
|
287
348
|
this._finalStates.add(stateTag);
|
|
@@ -313,6 +374,7 @@ var Machine = class Machine {
|
|
|
313
374
|
result._finalStates = new Set(this._finalStates);
|
|
314
375
|
result._spawnEffects = [...this._spawnEffects];
|
|
315
376
|
result._backgroundEffects = [...this._backgroundEffects];
|
|
377
|
+
result._postponeRules = [...this._postponeRules];
|
|
316
378
|
const anyHandlers = handlers;
|
|
317
379
|
if (this._guardsSchema !== void 0) for (const name of Object.keys(this._guardsSchema.definitions)) result._guardHandlers.set(name, anyHandlers[name]);
|
|
318
380
|
if (this._effectsSchema !== void 0) for (const name of Object.keys(this._effectsSchema.definitions)) result._effectHandlers.set(name, anyHandlers[name]);
|
|
@@ -320,10 +382,6 @@ var Machine = class Machine {
|
|
|
320
382
|
}
|
|
321
383
|
return new BuiltMachine(this);
|
|
322
384
|
}
|
|
323
|
-
/** @internal Persist from raw Machine — prefer BuiltMachine.persist() */
|
|
324
|
-
persist(config) {
|
|
325
|
-
return persist(config)(this);
|
|
326
|
-
}
|
|
327
385
|
static make(config) {
|
|
328
386
|
return new Machine(config.initial, config.state, config.event, config.guards, config.effects);
|
|
329
387
|
}
|
|
@@ -343,11 +401,68 @@ var TransitionScope = class {
|
|
|
343
401
|
}
|
|
344
402
|
};
|
|
345
403
|
const make = Machine.make;
|
|
346
|
-
|
|
347
|
-
|
|
404
|
+
/**
|
|
405
|
+
* Spawn an actor from a built machine.
|
|
406
|
+
*
|
|
407
|
+
* Options:
|
|
408
|
+
* - `id` — custom actor ID (default: random)
|
|
409
|
+
* - `hydrate` — restore from a previously-saved state snapshot.
|
|
410
|
+
* The actor starts in the hydrated state and re-runs spawn effects
|
|
411
|
+
* for that state (timers, scoped resources, etc.). Transition history
|
|
412
|
+
* is not replayed — only the current state's entry effects run.
|
|
413
|
+
*
|
|
414
|
+
* Persistence is composed in userland by observing `actor.changes`
|
|
415
|
+
* and saving snapshots to your own storage.
|
|
416
|
+
*/
|
|
417
|
+
const spawn = Effect.fn("effect-machine.spawn")(function* (built, idOrOptions) {
|
|
418
|
+
const opts = typeof idOrOptions === "string" ? { id: idOrOptions } : idOrOptions;
|
|
419
|
+
const actor = yield* createActor(opts?.id ?? `actor-${Math.random().toString(36).slice(2)}`, built._inner, { initialState: opts?.hydrate });
|
|
348
420
|
const maybeScope = yield* Effect.serviceOption(Scope.Scope);
|
|
349
421
|
if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, actor.stop);
|
|
350
422
|
return actor;
|
|
351
423
|
});
|
|
424
|
+
const replay = Effect.fn("effect-machine.replay")(function* (built, events, options) {
|
|
425
|
+
const machine = built._inner;
|
|
426
|
+
let state = options?.from ?? machine.initial;
|
|
427
|
+
const hasPostponeRules = machine.postponeRules.length > 0;
|
|
428
|
+
const postponed = [];
|
|
429
|
+
const dummySend = Effect.fn("effect-machine.replay.send")((_event) => Effect.void);
|
|
430
|
+
const self = {
|
|
431
|
+
send: dummySend,
|
|
432
|
+
cast: dummySend,
|
|
433
|
+
spawn: () => Effect.die("spawn not supported in replay")
|
|
434
|
+
};
|
|
435
|
+
for (const event of events) {
|
|
436
|
+
if (machine.finalStates.has(state._tag)) break;
|
|
437
|
+
if (hasPostponeRules && shouldPostpone(machine, state._tag, event._tag)) {
|
|
438
|
+
postponed.push(event);
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
const transition = resolveTransition(machine, state, event);
|
|
442
|
+
if (transition !== void 0) {
|
|
443
|
+
const result = yield* runTransitionHandler(machine, transition, state, event, self, stubSystem, "replay");
|
|
444
|
+
const previousTag = state._tag;
|
|
445
|
+
state = result.newState;
|
|
446
|
+
if ((state._tag !== previousTag || transition.reenter === true) && postponed.length > 0) {
|
|
447
|
+
let drainTag = previousTag;
|
|
448
|
+
while (state._tag !== drainTag && postponed.length > 0) {
|
|
449
|
+
if (machine.finalStates.has(state._tag)) break;
|
|
450
|
+
drainTag = state._tag;
|
|
451
|
+
const drained = postponed.splice(0);
|
|
452
|
+
for (const postponedEvent of drained) {
|
|
453
|
+
if (machine.finalStates.has(state._tag)) break;
|
|
454
|
+
if (shouldPostpone(machine, state._tag, postponedEvent._tag)) {
|
|
455
|
+
postponed.push(postponedEvent);
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
const pTransition = resolveTransition(machine, state, postponedEvent);
|
|
459
|
+
if (pTransition !== void 0) state = (yield* runTransitionHandler(machine, pTransition, state, postponedEvent, self, stubSystem, "replay")).newState;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return state;
|
|
466
|
+
});
|
|
352
467
|
//#endregion
|
|
353
|
-
export { BuiltMachine, Machine, findTransitions, machine_exports, make, spawn };
|
|
468
|
+
export { BuiltMachine, Machine, findTransitions, machine_exports, make, replay, spawn };
|
package/dist/testing.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { stubSystem } from "./internal/utils.js";
|
|
2
2
|
import { AssertionError } from "./errors.js";
|
|
3
3
|
import { BuiltMachine } from "./machine.js";
|
|
4
|
-
import { executeTransition } from "./internal/transition.js";
|
|
4
|
+
import { executeTransition, shouldPostpone } from "./internal/transition.js";
|
|
5
5
|
import { Effect, SubscriptionRef } from "effect";
|
|
6
6
|
//#region src/testing.ts
|
|
7
7
|
/**
|
|
@@ -26,18 +26,44 @@ import { Effect, SubscriptionRef } from "effect";
|
|
|
26
26
|
*/
|
|
27
27
|
const simulate = Effect.fn("effect-machine.simulate")(function* (input, events) {
|
|
28
28
|
const machine = input instanceof BuiltMachine ? input._inner : input;
|
|
29
|
+
const dummySend = Effect.fn("effect-machine.testing.simulate.send")((_event) => Effect.void);
|
|
29
30
|
const dummySelf = {
|
|
30
|
-
send:
|
|
31
|
+
send: dummySend,
|
|
32
|
+
cast: dummySend,
|
|
31
33
|
spawn: () => Effect.die("spawn not supported in simulation")
|
|
32
34
|
};
|
|
33
35
|
let currentState = machine.initial;
|
|
34
36
|
const states = [currentState];
|
|
37
|
+
const hasPostponeRules = machine.postponeRules.length > 0;
|
|
38
|
+
const postponed = [];
|
|
35
39
|
for (const event of events) {
|
|
40
|
+
if (hasPostponeRules && shouldPostpone(machine, currentState._tag, event._tag)) {
|
|
41
|
+
postponed.push(event);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
36
44
|
const result = yield* executeTransition(machine, currentState, event, dummySelf, stubSystem, "simulation");
|
|
37
45
|
if (!result.transitioned) continue;
|
|
46
|
+
const prevTag = currentState._tag;
|
|
38
47
|
currentState = result.newState;
|
|
39
48
|
states.push(currentState);
|
|
40
49
|
if (machine.finalStates.has(currentState._tag)) break;
|
|
50
|
+
let drainTag = prevTag;
|
|
51
|
+
while (currentState._tag !== drainTag && postponed.length > 0) {
|
|
52
|
+
drainTag = currentState._tag;
|
|
53
|
+
const drained = postponed.splice(0);
|
|
54
|
+
for (const postponedEvent of drained) {
|
|
55
|
+
if (shouldPostpone(machine, currentState._tag, postponedEvent._tag)) {
|
|
56
|
+
postponed.push(postponedEvent);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const drainResult = yield* executeTransition(machine, currentState, postponedEvent, dummySelf, stubSystem, "simulation");
|
|
60
|
+
if (drainResult.transitioned) {
|
|
61
|
+
currentState = drainResult.newState;
|
|
62
|
+
states.push(currentState);
|
|
63
|
+
if (machine.finalStates.has(currentState._tag)) break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
41
67
|
}
|
|
42
68
|
return {
|
|
43
69
|
states,
|
|
@@ -113,20 +139,48 @@ const assertNeverReaches = Effect.fn("effect-machine.assertNeverReaches")(functi
|
|
|
113
139
|
*/
|
|
114
140
|
const createTestHarness = Effect.fn("effect-machine.createTestHarness")(function* (input, options) {
|
|
115
141
|
const machine = input instanceof BuiltMachine ? input._inner : input;
|
|
142
|
+
const dummySend = Effect.fn("effect-machine.testing.harness.send")((_event) => Effect.void);
|
|
116
143
|
const dummySelf = {
|
|
117
|
-
send:
|
|
144
|
+
send: dummySend,
|
|
145
|
+
cast: dummySend,
|
|
118
146
|
spawn: () => Effect.die("spawn not supported in test harness")
|
|
119
147
|
};
|
|
120
148
|
const stateRef = yield* SubscriptionRef.make(machine.initial);
|
|
149
|
+
const hasPostponeRules = machine.postponeRules.length > 0;
|
|
150
|
+
const postponed = [];
|
|
121
151
|
return {
|
|
122
152
|
state: stateRef,
|
|
123
153
|
send: Effect.fn("effect-machine.testHarness.send")(function* (event) {
|
|
124
154
|
const currentState = yield* SubscriptionRef.get(stateRef);
|
|
155
|
+
if (hasPostponeRules && shouldPostpone(machine, currentState._tag, event._tag)) {
|
|
156
|
+
postponed.push(event);
|
|
157
|
+
return currentState;
|
|
158
|
+
}
|
|
125
159
|
const result = yield* executeTransition(machine, currentState, event, dummySelf, stubSystem, "test-harness");
|
|
126
160
|
if (!result.transitioned) return currentState;
|
|
161
|
+
const prevTag = currentState._tag;
|
|
127
162
|
const newState = result.newState;
|
|
128
163
|
yield* SubscriptionRef.set(stateRef, newState);
|
|
129
164
|
if (options?.onTransition !== void 0) options.onTransition(currentState, event, newState);
|
|
165
|
+
let drainTag = prevTag;
|
|
166
|
+
let currentTag = newState._tag;
|
|
167
|
+
while (currentTag !== drainTag && postponed.length > 0) {
|
|
168
|
+
drainTag = currentTag;
|
|
169
|
+
const drained = postponed.splice(0);
|
|
170
|
+
for (const postponedEvent of drained) {
|
|
171
|
+
const state = yield* SubscriptionRef.get(stateRef);
|
|
172
|
+
if (shouldPostpone(machine, state._tag, postponedEvent._tag)) {
|
|
173
|
+
postponed.push(postponedEvent);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const drainResult = yield* executeTransition(machine, state, postponedEvent, dummySelf, stubSystem, "test-harness");
|
|
177
|
+
if (drainResult.transitioned) {
|
|
178
|
+
yield* SubscriptionRef.set(stateRef, drainResult.newState);
|
|
179
|
+
currentTag = drainResult.newState._tag;
|
|
180
|
+
if (options?.onTransition !== void 0) options.onTransition(state, postponedEvent, drainResult.newState);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
130
184
|
return newState;
|
|
131
185
|
}),
|
|
132
186
|
getState: SubscriptionRef.get(stateRef)
|