effect-machine 0.9.0 → 0.10.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 +76 -16
- package/dist/actor.d.ts +55 -89
- package/dist/actor.js +135 -30
- 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 +3 -3
- package/dist/index.js +2 -2
- package/dist/internal/transition.d.ts +26 -2
- package/dist/internal/transition.js +37 -8
- package/dist/internal/utils.d.ts +7 -2
- package/dist/machine.d.ts +66 -3
- package/dist/machine.js +65 -0
- package/dist/persistence/persistent-actor.js +52 -16
- package/dist/testing.js +57 -3
- package/package.json +9 -8
- package/{dist-v3 → v3/dist}/actor.d.ts +65 -78
- package/{dist-v3 → v3/dist}/actor.js +173 -35
- 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 +13 -0
- package/v3/dist/index.js +13 -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 +1 -1
- package/{dist-v3 → v3/dist}/machine.d.ts +86 -9
- package/{dist-v3 → v3/dist}/machine.js +128 -2
- package/{dist-v3 → v3/dist}/persistence/adapter.d.ts +18 -5
- package/{dist-v3 → v3/dist}/persistence/adapter.js +1 -1
- package/{dist-v3 → v3/dist}/persistence/adapters/in-memory.d.ts +1 -1
- package/{dist-v3 → v3/dist}/persistence/adapters/in-memory.js +1 -1
- package/{dist-v3 → v3/dist}/persistence/persistent-actor.d.ts +7 -6
- package/{dist-v3 → v3/dist}/persistence/persistent-actor.js +58 -19
- package/{dist-v3 → v3/dist}/persistence/persistent-machine.d.ts +1 -1
- package/{dist-v3 → v3/dist}/persistence/persistent-machine.js +1 -1
- 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-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 → 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
- /package/{dist-v3 → v3/dist}/persistence/index.d.ts +0 -0
- /package/{dist-v3 → v3/dist}/persistence/index.js +0 -0
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { __exportAll } from "./_virtual/_rolldown/runtime.js";
|
|
2
|
+
import { Inspector } from "./inspection.js";
|
|
2
3
|
import { getTag } from "./internal/utils.js";
|
|
3
4
|
import { ProvisionValidationError, SlotProvisionError } from "./errors.js";
|
|
4
5
|
import { persist } from "./persistence/persistent-machine.js";
|
|
6
|
+
import { emitWithTimestamp } from "./internal/inspection.js";
|
|
5
7
|
import { MachineContextTag } from "./slot.js";
|
|
6
8
|
import { findTransitions, invalidateIndex } from "./internal/transition.js";
|
|
7
9
|
import { createActor } from "./actor.js";
|
|
8
10
|
import { Cause, Effect, Exit, Option, Scope } from "effect";
|
|
9
|
-
//#region src
|
|
11
|
+
//#region src/machine.ts
|
|
10
12
|
var machine_exports = /* @__PURE__ */ __exportAll({
|
|
11
13
|
BuiltMachine: () => BuiltMachine,
|
|
12
14
|
Machine: () => Machine,
|
|
@@ -14,6 +16,15 @@ var machine_exports = /* @__PURE__ */ __exportAll({
|
|
|
14
16
|
make: () => make,
|
|
15
17
|
spawn: () => spawn
|
|
16
18
|
});
|
|
19
|
+
const emitTaskInspection = (input) => Effect.flatMap(Effect.serviceOptional(Inspector).pipe(Effect.option), (inspector) => Option.isNone(inspector) ? Effect.void : emitWithTimestamp(inspector.value, (timestamp) => ({
|
|
20
|
+
type: "@machine.task",
|
|
21
|
+
actorId: input.actorId,
|
|
22
|
+
state: input.state,
|
|
23
|
+
taskName: input.taskName,
|
|
24
|
+
phase: input.phase,
|
|
25
|
+
error: input.error,
|
|
26
|
+
timestamp
|
|
27
|
+
})));
|
|
17
28
|
/**
|
|
18
29
|
* A finalized machine ready for spawning.
|
|
19
30
|
*
|
|
@@ -53,6 +64,7 @@ var Machine = class Machine {
|
|
|
53
64
|
/** @internal */ _spawnEffects;
|
|
54
65
|
/** @internal */ _backgroundEffects;
|
|
55
66
|
/** @internal */ _finalStates;
|
|
67
|
+
/** @internal */ _postponeRules;
|
|
56
68
|
/** @internal */ _guardsSchema;
|
|
57
69
|
/** @internal */ _effectsSchema;
|
|
58
70
|
/** @internal */ _guardHandlers;
|
|
@@ -77,6 +89,9 @@ var Machine = class Machine {
|
|
|
77
89
|
get finalStates() {
|
|
78
90
|
return this._finalStates;
|
|
79
91
|
}
|
|
92
|
+
get postponeRules() {
|
|
93
|
+
return this._postponeRules;
|
|
94
|
+
}
|
|
80
95
|
get guardsSchema() {
|
|
81
96
|
return this._guardsSchema;
|
|
82
97
|
}
|
|
@@ -90,6 +105,7 @@ var Machine = class Machine {
|
|
|
90
105
|
this._spawnEffects = [];
|
|
91
106
|
this._backgroundEffects = [];
|
|
92
107
|
this._finalStates = /* @__PURE__ */ new Set();
|
|
108
|
+
this._postponeRules = [];
|
|
93
109
|
this._guardsSchema = guardsSchema;
|
|
94
110
|
this._effectsSchema = effectsSchema;
|
|
95
111
|
this._guardHandlers = /* @__PURE__ */ new Map();
|
|
@@ -116,6 +132,15 @@ var Machine = class Machine {
|
|
|
116
132
|
})) : {}
|
|
117
133
|
};
|
|
118
134
|
}
|
|
135
|
+
from(stateOrStates, build) {
|
|
136
|
+
build(new TransitionScope(this, Array.isArray(stateOrStates) ? stateOrStates : [stateOrStates]));
|
|
137
|
+
return this;
|
|
138
|
+
}
|
|
139
|
+
/** @internal */
|
|
140
|
+
scopeTransition(states, event, handler, reenter) {
|
|
141
|
+
for (const state of states) this.addTransition(state, event, handler, reenter);
|
|
142
|
+
return this;
|
|
143
|
+
}
|
|
119
144
|
on(stateOrStates, event, handler) {
|
|
120
145
|
const states = Array.isArray(stateOrStates) ? stateOrStates : [stateOrStates];
|
|
121
146
|
for (const s of states) this.addTransition(s, event, handler, false);
|
|
@@ -190,14 +215,41 @@ var Machine = class Machine {
|
|
|
190
215
|
*/
|
|
191
216
|
task(state, run, options) {
|
|
192
217
|
const handler = Effect.fn("effect-machine.task")(function* (ctx) {
|
|
218
|
+
yield* emitTaskInspection({
|
|
219
|
+
actorId: ctx.actorId,
|
|
220
|
+
state: ctx.state,
|
|
221
|
+
taskName: options.name,
|
|
222
|
+
phase: "start"
|
|
223
|
+
});
|
|
193
224
|
const exit = yield* Effect.exit(run(ctx));
|
|
194
225
|
if (Exit.isSuccess(exit)) {
|
|
226
|
+
yield* emitTaskInspection({
|
|
227
|
+
actorId: ctx.actorId,
|
|
228
|
+
state: ctx.state,
|
|
229
|
+
taskName: options.name,
|
|
230
|
+
phase: "success"
|
|
231
|
+
});
|
|
195
232
|
yield* ctx.self.send(options.onSuccess(exit.value, ctx));
|
|
196
233
|
yield* Effect.yieldNow();
|
|
197
234
|
return;
|
|
198
235
|
}
|
|
199
236
|
const cause = exit.cause;
|
|
200
|
-
if (Cause.isInterruptedOnly(cause))
|
|
237
|
+
if (Cause.isInterruptedOnly(cause)) {
|
|
238
|
+
yield* emitTaskInspection({
|
|
239
|
+
actorId: ctx.actorId,
|
|
240
|
+
state: ctx.state,
|
|
241
|
+
taskName: options.name,
|
|
242
|
+
phase: "interrupt"
|
|
243
|
+
});
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
yield* emitTaskInspection({
|
|
247
|
+
actorId: ctx.actorId,
|
|
248
|
+
state: ctx.state,
|
|
249
|
+
taskName: options.name,
|
|
250
|
+
phase: "failure",
|
|
251
|
+
error: Cause.pretty(cause)
|
|
252
|
+
});
|
|
201
253
|
if (options.onFailure !== void 0) {
|
|
202
254
|
yield* ctx.self.send(options.onFailure(cause, ctx));
|
|
203
255
|
yield* Effect.yieldNow();
|
|
@@ -208,6 +260,36 @@ var Machine = class Machine {
|
|
|
208
260
|
return this.spawn(state, handler);
|
|
209
261
|
}
|
|
210
262
|
/**
|
|
263
|
+
* State timeout — gen_statem's `state_timeout`.
|
|
264
|
+
*
|
|
265
|
+
* Entering the state starts a timer. Leaving cancels it (via state scope).
|
|
266
|
+
* `.reenter()` restarts the timer with fresh state values.
|
|
267
|
+
* Compiles to `.task()` internally — preserves `@machine.task` inspection events.
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* ```ts
|
|
271
|
+
* machine
|
|
272
|
+
* .timeout(State.Loading, {
|
|
273
|
+
* duration: Duration.seconds(30),
|
|
274
|
+
* event: Event.Timeout,
|
|
275
|
+
* })
|
|
276
|
+
* // Dynamic duration from state
|
|
277
|
+
* .timeout(State.Retrying, {
|
|
278
|
+
* duration: (state) => Duration.seconds(state.backoff),
|
|
279
|
+
* event: Event.GiveUp,
|
|
280
|
+
* })
|
|
281
|
+
* ```
|
|
282
|
+
*/
|
|
283
|
+
timeout(state, config) {
|
|
284
|
+
const stateTag = getTag(state);
|
|
285
|
+
const resolveDuration = typeof config.duration === "function" ? config.duration : () => config.duration;
|
|
286
|
+
const resolveEvent = typeof config.event === "function" ? config.event : () => config.event;
|
|
287
|
+
return this.task(state, (ctx) => Effect.sleep(resolveDuration(ctx.state)), {
|
|
288
|
+
onSuccess: (_, ctx) => resolveEvent(ctx.state),
|
|
289
|
+
name: `$timeout:${stateTag}`
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
211
293
|
* Machine-lifetime effect that is forked on actor spawn and runs until the actor stops.
|
|
212
294
|
* Use effect slots defined via `Slot.Effects` for the actual work.
|
|
213
295
|
*
|
|
@@ -231,6 +313,35 @@ var Machine = class Machine {
|
|
|
231
313
|
this._backgroundEffects.push({ handler });
|
|
232
314
|
return this;
|
|
233
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* Postpone events — gen_statem's event postpone.
|
|
318
|
+
*
|
|
319
|
+
* When a matching event arrives in the given state, it is buffered instead of
|
|
320
|
+
* processed. After the next state transition (tag change), all buffered events
|
|
321
|
+
* are drained through the loop in FIFO order.
|
|
322
|
+
*
|
|
323
|
+
* Reply-bearing events (from `call`/`ask`) in the postpone buffer are settled
|
|
324
|
+
* with `ActorStoppedError` on stop/interrupt/final-state.
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* ```ts
|
|
328
|
+
* machine
|
|
329
|
+
* .postpone(State.Connecting, Event.Data) // single event
|
|
330
|
+
* .postpone(State.Connecting, [Event.Data, Event.Cmd]) // multiple events
|
|
331
|
+
* ```
|
|
332
|
+
*/
|
|
333
|
+
postpone(state, events) {
|
|
334
|
+
const stateTag = getTag(state);
|
|
335
|
+
const eventList = Array.isArray(events) ? events : [events];
|
|
336
|
+
for (const ev of eventList) {
|
|
337
|
+
const eventTag = getTag(ev);
|
|
338
|
+
this._postponeRules.push({
|
|
339
|
+
stateTag,
|
|
340
|
+
eventTag
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
return this;
|
|
344
|
+
}
|
|
234
345
|
final(state) {
|
|
235
346
|
const stateTag = getTag(state);
|
|
236
347
|
this._finalStates.add(stateTag);
|
|
@@ -262,6 +373,7 @@ var Machine = class Machine {
|
|
|
262
373
|
result._finalStates = new Set(this._finalStates);
|
|
263
374
|
result._spawnEffects = [...this._spawnEffects];
|
|
264
375
|
result._backgroundEffects = [...this._backgroundEffects];
|
|
376
|
+
result._postponeRules = [...this._postponeRules];
|
|
265
377
|
const anyHandlers = handlers;
|
|
266
378
|
if (this._guardsSchema !== void 0) for (const name of Object.keys(this._guardsSchema.definitions)) result._guardHandlers.set(name, anyHandlers[name]);
|
|
267
379
|
if (this._effectsSchema !== void 0) for (const name of Object.keys(this._effectsSchema.definitions)) result._effectHandlers.set(name, anyHandlers[name]);
|
|
@@ -277,6 +389,20 @@ var Machine = class Machine {
|
|
|
277
389
|
return new Machine(config.initial, config.state, config.event, config.guards, config.effects);
|
|
278
390
|
}
|
|
279
391
|
};
|
|
392
|
+
var TransitionScope = class {
|
|
393
|
+
constructor(machine, states) {
|
|
394
|
+
this.machine = machine;
|
|
395
|
+
this.states = states;
|
|
396
|
+
}
|
|
397
|
+
on(event, handler) {
|
|
398
|
+
this.machine.scopeTransition(this.states, event, handler, false);
|
|
399
|
+
return this;
|
|
400
|
+
}
|
|
401
|
+
reenter(event, handler) {
|
|
402
|
+
this.machine.scopeTransition(this.states, event, handler, true);
|
|
403
|
+
return this;
|
|
404
|
+
}
|
|
405
|
+
};
|
|
280
406
|
const make = Machine.make;
|
|
281
407
|
const spawn = Effect.fn("effect-machine.spawn")(function* (built, id) {
|
|
282
408
|
const actor = yield* createActor(id ?? `actor-${Math.random().toString(36).slice(2)}`, built._inner);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { DuplicateActorError } from "../errors.js";
|
|
2
2
|
import { PersistentActorRef } from "./persistent-actor.js";
|
|
3
|
-
import { Effect, Option, Schema } from "effect";
|
|
3
|
+
import { Context, Effect, Option, Schema } from "effect";
|
|
4
4
|
|
|
5
|
-
//#region src
|
|
5
|
+
//#region src/persistence/adapter.d.ts
|
|
6
6
|
/**
|
|
7
7
|
* Metadata for a persisted actor.
|
|
8
8
|
* Used for discovery and filtering during bulk restore.
|
|
@@ -106,17 +106,30 @@ interface PersistenceAdapter {
|
|
|
106
106
|
*/
|
|
107
107
|
readonly loadMetadata?: (id: string) => Effect.Effect<Option.Option<ActorMetadata>, PersistenceError>;
|
|
108
108
|
}
|
|
109
|
-
declare const PersistenceError_base:
|
|
109
|
+
declare const PersistenceError_base: Schema.TaggedErrorClass<PersistenceError, "PersistenceError", {
|
|
110
|
+
readonly _tag: Schema.tag<"PersistenceError">;
|
|
111
|
+
} & {
|
|
112
|
+
operation: typeof Schema.String;
|
|
113
|
+
actorId: typeof Schema.String;
|
|
114
|
+
cause: Schema.optional<typeof Schema.Unknown>;
|
|
115
|
+
message: Schema.optional<typeof Schema.String>;
|
|
116
|
+
}>;
|
|
110
117
|
/**
|
|
111
118
|
* Error type for persistence operations
|
|
112
119
|
*/
|
|
113
120
|
declare class PersistenceError extends PersistenceError_base {}
|
|
114
|
-
declare const VersionConflictError_base:
|
|
121
|
+
declare const VersionConflictError_base: Schema.TaggedErrorClass<VersionConflictError, "VersionConflictError", {
|
|
122
|
+
readonly _tag: Schema.tag<"VersionConflictError">;
|
|
123
|
+
} & {
|
|
124
|
+
actorId: typeof Schema.String;
|
|
125
|
+
expectedVersion: typeof Schema.Number;
|
|
126
|
+
actualVersion: typeof Schema.Number;
|
|
127
|
+
}>;
|
|
115
128
|
/**
|
|
116
129
|
* Version conflict error — snapshot version doesn't match expected
|
|
117
130
|
*/
|
|
118
131
|
declare class VersionConflictError extends VersionConflictError_base {}
|
|
119
|
-
declare const PersistenceAdapterTag_base:
|
|
132
|
+
declare const PersistenceAdapterTag_base: Context.TagClass<PersistenceAdapterTag, "effect-machine/src/persistence/adapter/PersistenceAdapterTag", PersistenceAdapter>;
|
|
120
133
|
/**
|
|
121
134
|
* PersistenceAdapter service tag
|
|
122
135
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { PersistenceAdapter, PersistenceAdapterTag } from "../adapter.js";
|
|
2
2
|
import { Effect, Layer } from "effect";
|
|
3
3
|
|
|
4
|
-
//#region src
|
|
4
|
+
//#region src/persistence/adapters/in-memory.d.ts
|
|
5
5
|
/**
|
|
6
6
|
* Create an in-memory persistence adapter effect.
|
|
7
7
|
* Returns the adapter directly for custom layer composition.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PersistenceAdapterTag, PersistenceError, VersionConflictError } from "../adapter.js";
|
|
2
2
|
import { Effect, Layer, Option, Ref, Schema } from "effect";
|
|
3
|
-
//#region src
|
|
3
|
+
//#region src/persistence/adapters/in-memory.ts
|
|
4
4
|
/**
|
|
5
5
|
* Create an in-memory persistence adapter.
|
|
6
6
|
* Useful for testing and development.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { EffectsDef, GuardsDef, MachineContext } from "../slot.js";
|
|
1
2
|
import { PersistentMachine } from "./persistent-machine.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
3
|
+
import { PersistedEvent, PersistenceAdapterTag, PersistenceError, Snapshot, VersionConflictError } from "./adapter.js";
|
|
4
|
+
import { MachineRef } from "../machine.js";
|
|
4
5
|
import { ActorRef } from "../actor.js";
|
|
5
|
-
import { Effect, Option } from "effect";
|
|
6
|
+
import { Effect, Option, Scope } from "effect";
|
|
6
7
|
|
|
7
|
-
//#region src
|
|
8
|
+
//#region src/persistence/persistent-actor.d.ts
|
|
8
9
|
/**
|
|
9
10
|
* Extended ActorRef with persistence capabilities
|
|
10
11
|
*/
|
|
@@ -35,7 +36,7 @@ declare const createPersistentActor: <S extends {
|
|
|
35
36
|
readonly _tag: string;
|
|
36
37
|
}, E extends {
|
|
37
38
|
readonly _tag: string;
|
|
38
|
-
}, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(id: string, persistentMachine: PersistentMachine<S, E, R>, initialSnapshot: Option.Option<Snapshot<S>>, initialEvents: readonly PersistedEvent<E>[]) => Effect.Effect<PersistentActorRef<S, E, R>,
|
|
39
|
+
}, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(id: string, persistentMachine: PersistentMachine<S, E, R>, initialSnapshot: Option.Option<Snapshot<S>>, initialEvents: readonly PersistedEvent<E>[]) => Effect.Effect<PersistentActorRef<S, E, R>, PersistenceError, PersistenceAdapterTag | Exclude<R, MachineContext<S, E, MachineRef<E>>> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>>;
|
|
39
40
|
/**
|
|
40
41
|
* Restore an actor from persistence.
|
|
41
42
|
* Returns None if no persisted state exists.
|
|
@@ -44,6 +45,6 @@ declare const restorePersistentActor: <S extends {
|
|
|
44
45
|
readonly _tag: string;
|
|
45
46
|
}, E extends {
|
|
46
47
|
readonly _tag: string;
|
|
47
|
-
}, R>(id: string, persistentMachine: PersistentMachine<S, E, R>) => Effect.Effect<Option.None<PersistentActorRef<S, E, R>> | Option.Some<PersistentActorRef<S, E, R>>,
|
|
48
|
+
}, R>(id: string, persistentMachine: PersistentMachine<S, E, R>) => Effect.Effect<Option.None<PersistentActorRef<S, E, R>> | Option.Some<PersistentActorRef<S, E, R>>, PersistenceError, PersistenceAdapterTag | Exclude<R, MachineContext<S, E, MachineRef<E>>> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>>;
|
|
48
49
|
//#endregion
|
|
49
50
|
export { PersistentActorRef, createPersistentActor, restorePersistentActor };
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Inspector } from "../inspection.js";
|
|
2
2
|
import { INTERNAL_INIT_EVENT, stubSystem } from "../internal/utils.js";
|
|
3
|
-
import {
|
|
3
|
+
import { NoReplyError } from "../errors.js";
|
|
4
4
|
import { emitWithTimestamp } from "../internal/inspection.js";
|
|
5
|
+
import { processEventCore, resolveTransition, runSpawnEffects, runTransitionHandler, shouldPostpone } from "../internal/transition.js";
|
|
5
6
|
import { PersistenceAdapterTag } from "./adapter.js";
|
|
6
|
-
import { ActorSystem, buildActorRefCore, notifyListeners } from "../actor.js";
|
|
7
|
-
import { Cause, Clock, Effect, Exit, Fiber, Option, Queue, Ref, Schedule, Scope, SubscriptionRef } from "effect";
|
|
8
|
-
//#region src
|
|
7
|
+
import { ActorSystem, buildActorRefCore, notifyListeners, settlePendingReplies } from "../actor.js";
|
|
8
|
+
import { Cause, Clock, Deferred, Effect, Exit, Fiber, Option, Queue, Ref, Schedule, Scope, SubscriptionRef } from "effect";
|
|
9
|
+
//#region src/persistence/persistent-actor.ts
|
|
9
10
|
/** Get current time in milliseconds using Effect Clock */
|
|
10
11
|
const now = Clock.currentTimeMillis;
|
|
11
12
|
/**
|
|
@@ -19,7 +20,7 @@ const replayEvents = Effect.fn("effect-machine.persistentActor.replayEvents")(fu
|
|
|
19
20
|
for (const persistedEvent of events) {
|
|
20
21
|
if (stopVersion !== void 0 && persistedEvent.version > stopVersion) break;
|
|
21
22
|
const transition = resolveTransition(machine, state, persistedEvent.event);
|
|
22
|
-
if (transition !== void 0) state = yield* runTransitionHandler(machine, transition, state, persistedEvent.event, self, stubSystem);
|
|
23
|
+
if (transition !== void 0) state = (yield* runTransitionHandler(machine, transition, state, persistedEvent.event, self, stubSystem, "restore")).newState;
|
|
23
24
|
version = persistedEvent.version;
|
|
24
25
|
}
|
|
25
26
|
return {
|
|
@@ -30,7 +31,7 @@ const replayEvents = Effect.fn("effect-machine.persistentActor.replayEvents")(fu
|
|
|
30
31
|
/**
|
|
31
32
|
* Build PersistentActorRef with all methods
|
|
32
33
|
*/
|
|
33
|
-
const buildPersistentActorRef = (id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, listeners, stop, adapter, system, childrenMap) => {
|
|
34
|
+
const buildPersistentActorRef = (id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, listeners, stop, adapter, system, childrenMap, pendingReplies) => {
|
|
34
35
|
const { machine, persistence } = persistentMachine;
|
|
35
36
|
const typedMachine = machine;
|
|
36
37
|
const persist = Effect.gen(function* () {
|
|
@@ -44,8 +45,10 @@ const buildPersistentActorRef = (id, persistentMachine, stateRef, versionRef, ev
|
|
|
44
45
|
const version = Ref.get(versionRef).pipe(Effect.withSpan("effect-machine.persistentActor.version"));
|
|
45
46
|
const replayTo = Effect.fn("effect-machine.persistentActor.replayTo")(function* (targetVersion) {
|
|
46
47
|
if (targetVersion <= (yield* Ref.get(versionRef))) {
|
|
48
|
+
const dummySend = Effect.fn("effect-machine.persistentActor.replay.send")((_event) => Effect.void);
|
|
47
49
|
const dummySelf = {
|
|
48
|
-
send:
|
|
50
|
+
send: dummySend,
|
|
51
|
+
cast: dummySend,
|
|
49
52
|
spawn: () => Effect.die("spawn not supported in replay")
|
|
50
53
|
};
|
|
51
54
|
const maybeSnapshot = yield* adapter.loadSnapshot(id, persistence.stateSchema);
|
|
@@ -70,7 +73,7 @@ const buildPersistentActorRef = (id, persistentMachine, stateRef, versionRef, ev
|
|
|
70
73
|
}
|
|
71
74
|
});
|
|
72
75
|
return {
|
|
73
|
-
...buildActorRefCore(id, typedMachine, stateRef, eventQueue, stoppedRef, listeners, stop, system, childrenMap),
|
|
76
|
+
...buildActorRefCore(id, typedMachine, stateRef, eventQueue, stoppedRef, listeners, stop, system, childrenMap, pendingReplies),
|
|
74
77
|
persist,
|
|
75
78
|
version,
|
|
76
79
|
replayTo
|
|
@@ -92,11 +95,16 @@ const createPersistentActor = Effect.fn("effect-machine.persistentActor.spawn")(
|
|
|
92
95
|
const eventQueue = yield* Queue.unbounded();
|
|
93
96
|
const stoppedRef = yield* Ref.make(false);
|
|
94
97
|
const childrenMap = /* @__PURE__ */ new Map();
|
|
98
|
+
const selfSend = Effect.fn("effect-machine.persistentActor.self.send")(function* (event) {
|
|
99
|
+
if (yield* Ref.get(stoppedRef)) return;
|
|
100
|
+
yield* Queue.offer(eventQueue, {
|
|
101
|
+
_tag: "send",
|
|
102
|
+
event
|
|
103
|
+
});
|
|
104
|
+
});
|
|
95
105
|
const self = {
|
|
96
|
-
send:
|
|
97
|
-
|
|
98
|
-
yield* Queue.offer(eventQueue, event);
|
|
99
|
-
}),
|
|
106
|
+
send: selfSend,
|
|
107
|
+
cast: selfSend,
|
|
100
108
|
spawn: (childId, childMachine) => Effect.gen(function* () {
|
|
101
109
|
const child = yield* system.spawn(childId, childMachine).pipe(Effect.provideService(ActorSystem, system));
|
|
102
110
|
childrenMap.set(childId, child);
|
|
@@ -145,6 +153,7 @@ const createPersistentActor = Effect.fn("effect-machine.persistentActor.spawn")(
|
|
|
145
153
|
const backgroundFibers = [];
|
|
146
154
|
const initEvent = { _tag: INTERNAL_INIT_EVENT };
|
|
147
155
|
const initCtx = {
|
|
156
|
+
actorId: id,
|
|
148
157
|
state: resolvedInitial,
|
|
149
158
|
event: initEvent,
|
|
150
159
|
self,
|
|
@@ -153,6 +162,7 @@ const createPersistentActor = Effect.fn("effect-machine.persistentActor.spawn")(
|
|
|
153
162
|
const { effects: effectSlots } = typedMachine._slots;
|
|
154
163
|
for (const bg of typedMachine.backgroundEffects) {
|
|
155
164
|
const fiber = yield* Effect.forkDaemon(bg.handler({
|
|
165
|
+
actorId: id,
|
|
156
166
|
state: resolvedInitial,
|
|
157
167
|
event: initEvent,
|
|
158
168
|
self,
|
|
@@ -175,9 +185,10 @@ const createPersistentActor = Effect.fn("effect-machine.persistentActor.spawn")(
|
|
|
175
185
|
finalState: resolvedInitial,
|
|
176
186
|
timestamp
|
|
177
187
|
}));
|
|
178
|
-
return buildPersistentActorRef(id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, listeners, Ref.set(stoppedRef, true).pipe(Effect.withSpan("effect-machine.persistentActor.stop"), Effect.asVoid), adapter, system, childrenMap);
|
|
188
|
+
return buildPersistentActorRef(id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, listeners, Ref.set(stoppedRef, true).pipe(Effect.withSpan("effect-machine.persistentActor.stop"), Effect.asVoid), adapter, system, childrenMap, /* @__PURE__ */ new Set());
|
|
179
189
|
}
|
|
180
|
-
const
|
|
190
|
+
const pendingReplies = /* @__PURE__ */ new Set();
|
|
191
|
+
const loopFiber = yield* Effect.forkDaemon(persistentEventLoop(id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, self, listeners, adapter, createdAt, stateScopeRef, backgroundFibers, snapshotQueue, snapshotEnabledRef, persistenceQueue, snapshotFiber, persistenceFiber, inspector, system, pendingReplies));
|
|
181
192
|
return buildPersistentActorRef(id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, listeners, Effect.gen(function* () {
|
|
182
193
|
const finalState = yield* SubscriptionRef.get(stateRef);
|
|
183
194
|
yield* emitWithTimestamp(inspector, (timestamp) => ({
|
|
@@ -188,16 +199,17 @@ const createPersistentActor = Effect.fn("effect-machine.persistentActor.spawn")(
|
|
|
188
199
|
}));
|
|
189
200
|
yield* Ref.set(stoppedRef, true);
|
|
190
201
|
yield* Fiber.interrupt(loopFiber);
|
|
202
|
+
yield* settlePendingReplies(pendingReplies, id);
|
|
191
203
|
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
192
204
|
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
193
205
|
yield* Fiber.interrupt(snapshotFiber);
|
|
194
206
|
yield* Fiber.interrupt(persistenceFiber);
|
|
195
|
-
}).pipe(Effect.withSpan("effect-machine.persistentActor.stop"), Effect.asVoid), adapter, system, childrenMap);
|
|
207
|
+
}).pipe(Effect.withSpan("effect-machine.persistentActor.stop"), Effect.asVoid), adapter, system, childrenMap, pendingReplies);
|
|
196
208
|
});
|
|
197
209
|
/**
|
|
198
210
|
* Main event loop for persistent actor
|
|
199
211
|
*/
|
|
200
|
-
const persistentEventLoop = Effect.fn("effect-machine.persistentActor.eventLoop")(function* (id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, self, listeners, adapter, createdAt, stateScopeRef, backgroundFibers, snapshotQueue, snapshotEnabledRef, persistenceQueue, snapshotFiber, persistenceFiber, inspector, system) {
|
|
212
|
+
const persistentEventLoop = Effect.fn("effect-machine.persistentActor.eventLoop")(function* (id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, self, listeners, adapter, createdAt, stateScopeRef, backgroundFibers, snapshotQueue, snapshotEnabledRef, persistenceQueue, snapshotFiber, persistenceFiber, inspector, system, pendingReplies) {
|
|
201
213
|
const { machine, persistence } = persistentMachine;
|
|
202
214
|
const typedMachine = machine;
|
|
203
215
|
const hooks = inspector === void 0 ? void 0 : {
|
|
@@ -226,10 +238,28 @@ const persistentEventLoop = Effect.fn("effect-machine.persistentActor.eventLoop"
|
|
|
226
238
|
timestamp
|
|
227
239
|
}))
|
|
228
240
|
};
|
|
241
|
+
const postponed = [];
|
|
242
|
+
const pendingDrain = [];
|
|
243
|
+
const hasPostponeRules = machine.postponeRules.length > 0;
|
|
229
244
|
while (true) {
|
|
230
|
-
const
|
|
245
|
+
const queued = pendingDrain.length > 0 ? pendingDrain.shift() : yield* Queue.take(eventQueue);
|
|
246
|
+
const event = queued.event;
|
|
231
247
|
const currentState = yield* SubscriptionRef.get(stateRef);
|
|
232
248
|
const currentVersion = yield* Ref.get(versionRef);
|
|
249
|
+
if (hasPostponeRules && shouldPostpone(typedMachine, currentState._tag, event._tag)) {
|
|
250
|
+
postponed.push(queued);
|
|
251
|
+
if (queued._tag === "call") yield* Deferred.succeed(queued.reply, {
|
|
252
|
+
newState: currentState,
|
|
253
|
+
previousState: currentState,
|
|
254
|
+
transitioned: false,
|
|
255
|
+
lifecycleRan: false,
|
|
256
|
+
isFinal: false,
|
|
257
|
+
hasReply: false,
|
|
258
|
+
reply: void 0,
|
|
259
|
+
postponed: true
|
|
260
|
+
});
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
233
263
|
yield* emitWithTimestamp(inspector, (timestamp) => ({
|
|
234
264
|
type: "@machine.event",
|
|
235
265
|
actorId: id,
|
|
@@ -237,7 +267,13 @@ const persistentEventLoop = Effect.fn("effect-machine.persistentActor.eventLoop"
|
|
|
237
267
|
event,
|
|
238
268
|
timestamp
|
|
239
269
|
}));
|
|
240
|
-
const result = yield* processEventCore(typedMachine, currentState, event, self, stateScopeRef, system, hooks);
|
|
270
|
+
const result = yield* processEventCore(typedMachine, currentState, event, self, stateScopeRef, system, id, hooks);
|
|
271
|
+
if (queued._tag === "call") yield* Deferred.succeed(queued.reply, result);
|
|
272
|
+
else if (queued._tag === "ask") if (result.hasReply) yield* Deferred.succeed(queued.reply, result.reply);
|
|
273
|
+
else yield* Deferred.fail(queued.reply, new NoReplyError({
|
|
274
|
+
actorId: id,
|
|
275
|
+
eventTag: event._tag
|
|
276
|
+
}));
|
|
241
277
|
if (!result.transitioned) continue;
|
|
242
278
|
const newVersion = currentVersion + 1;
|
|
243
279
|
yield* Ref.set(versionRef, newVersion);
|
|
@@ -265,12 +301,15 @@ const persistentEventLoop = Effect.fn("effect-machine.persistentActor.eventLoop"
|
|
|
265
301
|
timestamp
|
|
266
302
|
}));
|
|
267
303
|
yield* Ref.set(stoppedRef, true);
|
|
304
|
+
postponed.length = 0;
|
|
305
|
+
yield* settlePendingReplies(pendingReplies, id);
|
|
268
306
|
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
269
307
|
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
270
308
|
yield* Fiber.interrupt(snapshotFiber);
|
|
271
309
|
yield* Fiber.interrupt(persistenceFiber);
|
|
272
310
|
return;
|
|
273
311
|
}
|
|
312
|
+
if (result.lifecycleRan && postponed.length > 0) pendingDrain.push(...postponed.splice(0));
|
|
274
313
|
}
|
|
275
314
|
});
|
|
276
315
|
/**
|
|
@@ -285,7 +324,7 @@ const runSpawnEffectsWithInspection = Effect.fn("effect-machine.persistentActor.
|
|
|
285
324
|
state,
|
|
286
325
|
timestamp
|
|
287
326
|
}));
|
|
288
|
-
yield* runSpawnEffects(machine, state, event, self, stateScope, system, inspector === void 0 ? void 0 : (info) => emitWithTimestamp(inspector, (timestamp) => ({
|
|
327
|
+
yield* runSpawnEffects(machine, state, event, self, stateScope, system, actorId, inspector === void 0 ? void 0 : (info) => emitWithTimestamp(inspector, (timestamp) => ({
|
|
289
328
|
type: "@machine.error",
|
|
290
329
|
actorId,
|
|
291
330
|
phase: info.phase,
|
|
@@ -2,7 +2,7 @@ import { EventBrand, StateBrand } from "../internal/brands.js";
|
|
|
2
2
|
import { Machine } from "../machine.js";
|
|
3
3
|
import { Schedule, Schema } from "effect";
|
|
4
4
|
|
|
5
|
-
//#region src
|
|
5
|
+
//#region src/persistence/persistent-machine.d.ts
|
|
6
6
|
type BrandedState = {
|
|
7
7
|
readonly _tag: string;
|
|
8
8
|
} & StateBrand;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { InvalidSchemaError, MissingMatchHandlerError } from "./errors.js";
|
|
2
2
|
import { Schema } from "effect";
|
|
3
|
-
//#region src
|
|
3
|
+
//#region src/schema.ts
|
|
4
4
|
/**
|
|
5
5
|
* Schema-first State/Event definitions for effect-machine.
|
|
6
6
|
*
|
|
@@ -58,7 +58,10 @@ const buildMachineSchema = (definition) => {
|
|
|
58
58
|
constructor.derive = (source, partial) => {
|
|
59
59
|
const result = { _tag: tag };
|
|
60
60
|
for (const key of fieldNames) if (key in source) result[key] = source[key];
|
|
61
|
-
if (partial !== void 0) for (const [key, value] of Object.entries(partial))
|
|
61
|
+
if (partial !== void 0) for (const [key, value] of Object.entries(partial)) {
|
|
62
|
+
if (key === "_tag") continue;
|
|
63
|
+
result[key] = value;
|
|
64
|
+
}
|
|
62
65
|
return result;
|
|
63
66
|
};
|
|
64
67
|
constructors[tag] = constructor;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ActorSystem } from "./actor.js";
|
|
2
|
-
import { Effect, Schema } from "effect";
|
|
2
|
+
import { Context, Effect, Schema } from "effect";
|
|
3
3
|
|
|
4
|
-
//#region src
|
|
4
|
+
//#region src/slot.d.ts
|
|
5
5
|
/** Schema fields definition (like Schema.Struct.Fields) */
|
|
6
6
|
type Fields = Record<string, Schema.Schema.All>;
|
|
7
7
|
/** Extract the encoded type from schema fields (used for parameters) */
|
|
@@ -43,6 +43,7 @@ type EffectSlots<D extends EffectsDef> = { readonly [K in keyof D & string]: Eff
|
|
|
43
43
|
* Shared across all machines via MachineContextTag.
|
|
44
44
|
*/
|
|
45
45
|
interface MachineContext<State, Event, Self> {
|
|
46
|
+
readonly actorId: string;
|
|
46
47
|
readonly state: State;
|
|
47
48
|
readonly event: Event;
|
|
48
49
|
readonly self: Self;
|
|
@@ -53,7 +54,7 @@ interface MachineContext<State, Event, Self> {
|
|
|
53
54
|
* Single module-level tag instead of per-machine allocation.
|
|
54
55
|
* @internal
|
|
55
56
|
*/
|
|
56
|
-
declare const MachineContextTag: any
|
|
57
|
+
declare const MachineContextTag: Context.Tag<MachineContext<any, any, any>, MachineContext<any, any, any>>;
|
|
57
58
|
/**
|
|
58
59
|
* Guard handler implementation.
|
|
59
60
|
* Receives params and context, returns Effect<boolean>.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { EffectsDef, GuardsDef, MachineContext } from "./slot.js";
|
|
1
2
|
import { AssertionError } from "./errors.js";
|
|
2
|
-
import {
|
|
3
|
-
import { BuiltMachine, Machine } from "./machine.js";
|
|
3
|
+
import { BuiltMachine, Machine, MachineRef } from "./machine.js";
|
|
4
4
|
import { Effect, SubscriptionRef } from "effect";
|
|
5
5
|
|
|
6
|
-
//#region src
|
|
6
|
+
//#region src/testing.d.ts
|
|
7
7
|
/** Accept either Machine or BuiltMachine for testing utilities. */
|
|
8
8
|
type MachineInput<S, E, R, GD extends GuardsDef, EFD extends EffectsDef> = Machine<S, E, R, any, any, GD, EFD> | BuiltMachine<S, E, R>;
|
|
9
9
|
/**
|
|
@@ -40,7 +40,7 @@ declare const simulate: <S extends {
|
|
|
40
40
|
}, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[]) => Effect.Effect<{
|
|
41
41
|
states: S[];
|
|
42
42
|
finalState: S;
|
|
43
|
-
}, never, Exclude<R,
|
|
43
|
+
}, never, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
|
|
44
44
|
/**
|
|
45
45
|
* Assert that a machine can reach a specific state given a sequence of events
|
|
46
46
|
*/
|
|
@@ -48,7 +48,7 @@ declare const assertReaches: <S extends {
|
|
|
48
48
|
readonly _tag: string;
|
|
49
49
|
}, E extends {
|
|
50
50
|
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) => Effect.Effect<
|
|
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) => Effect.Effect<S, AssertionError, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
|
|
52
52
|
/**
|
|
53
53
|
* Assert that a machine follows a specific path of state tags
|
|
54
54
|
*
|
|
@@ -65,7 +65,10 @@ declare const assertPath: <S extends {
|
|
|
65
65
|
readonly _tag: string;
|
|
66
66
|
}, E extends {
|
|
67
67
|
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[]) => Effect.Effect<
|
|
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[]) => Effect.Effect<{
|
|
69
|
+
states: S[];
|
|
70
|
+
finalState: S;
|
|
71
|
+
}, AssertionError, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
|
|
69
72
|
/**
|
|
70
73
|
* Assert that a machine never reaches a specific state given a sequence of events
|
|
71
74
|
*
|
|
@@ -83,7 +86,10 @@ declare const assertNeverReaches: <S extends {
|
|
|
83
86
|
readonly _tag: string;
|
|
84
87
|
}, E extends {
|
|
85
88
|
readonly _tag: string;
|
|
86
|
-
}, 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) => Effect.Effect<
|
|
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) => Effect.Effect<{
|
|
90
|
+
states: S[];
|
|
91
|
+
finalState: S;
|
|
92
|
+
}, AssertionError, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
|
|
87
93
|
/**
|
|
88
94
|
* Create a controllable test harness for a machine
|
|
89
95
|
*/
|
|
@@ -129,7 +135,7 @@ declare const createTestHarness: <S extends {
|
|
|
129
135
|
readonly _tag: string;
|
|
130
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> | undefined) => Effect.Effect<{
|
|
131
137
|
state: SubscriptionRef.SubscriptionRef<S>;
|
|
132
|
-
send: (event: E) => Effect.Effect<S, never,
|
|
138
|
+
send: (event: E) => Effect.Effect<S, never, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
|
|
133
139
|
getState: Effect.Effect<S, never, never>;
|
|
134
140
|
}, never, never>;
|
|
135
141
|
//#endregion
|