effect-machine 0.12.0 → 0.14.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 +133 -324
- package/dist/actor.d.ts +46 -28
- package/dist/actor.js +276 -315
- package/dist/cluster/entity-machine.d.ts +1 -1
- package/dist/cluster/entity-machine.js +20 -9
- package/dist/cluster/to-entity.d.ts +3 -3
- package/dist/errors.d.ts +24 -20
- package/dist/errors.js +10 -6
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -2
- package/dist/internal/runtime.d.ts +82 -7
- package/dist/internal/runtime.js +162 -58
- package/dist/internal/transition.d.ts +12 -11
- package/dist/internal/transition.js +12 -14
- package/dist/machine.d.ts +148 -140
- package/dist/machine.js +141 -155
- package/dist/schema.d.ts +14 -0
- package/dist/schema.js +10 -1
- package/dist/slot.d.ts +112 -86
- package/dist/slot.js +92 -59
- package/dist/supervision.d.ts +97 -0
- package/dist/supervision.js +42 -0
- package/dist/testing.d.ts +21 -12
- package/dist/testing.js +23 -26
- package/package.json +7 -7
- package/v3/dist/actor.d.ts +53 -30
- package/v3/dist/actor.js +286 -311
- package/v3/dist/cluster/entity-machine.d.ts +1 -1
- package/v3/dist/cluster/entity-machine.js +5 -5
- package/v3/dist/cluster/to-entity.d.ts +1 -1
- package/v3/dist/errors.d.ts +14 -10
- package/v3/dist/errors.js +11 -7
- package/v3/dist/index.d.ts +6 -5
- package/v3/dist/index.js +3 -2
- package/v3/dist/inspection.d.ts +3 -22
- package/v3/dist/inspection.js +1 -15
- package/v3/dist/internal/brands.d.ts +4 -8
- package/v3/dist/internal/inspection.js +1 -1
- package/v3/dist/internal/runtime.d.ts +87 -10
- package/v3/dist/internal/runtime.js +177 -61
- package/v3/dist/internal/transition.d.ts +13 -12
- package/v3/dist/internal/transition.js +14 -16
- package/v3/dist/internal/utils.js +5 -1
- package/v3/dist/machine.d.ts +158 -143
- package/v3/dist/machine.js +148 -155
- package/v3/dist/schema.d.ts +25 -11
- package/v3/dist/schema.js +18 -5
- package/v3/dist/slot.d.ts +112 -86
- package/v3/dist/slot.js +92 -59
- package/v3/dist/supervision.d.ts +97 -0
- package/v3/dist/supervision.js +42 -0
- package/v3/dist/testing.d.ts +21 -12
- package/v3/dist/testing.js +23 -24
package/dist/testing.js
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
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
|
+
const makeDummySelf = (label) => {
|
|
8
|
+
const dummySend = Effect.fn(label)((_event) => Effect.void);
|
|
9
|
+
return {
|
|
10
|
+
send: dummySend,
|
|
11
|
+
cast: dummySend,
|
|
12
|
+
spawn: () => Effect.die(`spawn not supported in ${label}`),
|
|
13
|
+
reply: () => Effect.succeed(false)
|
|
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.
|
|
10
|
-
* Does not run onEnter/spawn/background effects, but does run
|
|
19
|
+
* Does not run onEnter/spawn/background effects, but does run slots
|
|
11
20
|
* within transition handlers.
|
|
12
21
|
*
|
|
13
22
|
* @example
|
|
@@ -24,15 +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
|
-
reply: () => Effect.succeed(false)
|
|
35
|
-
};
|
|
36
|
+
const simulate = Effect.fn("effect-machine.simulate")(function* (input, events, options) {
|
|
37
|
+
const machine = materializeMachine(input, options?.slots);
|
|
38
|
+
const dummySelf = makeDummySelf("effect-machine.testing.simulate");
|
|
36
39
|
let currentState = machine.initial;
|
|
37
40
|
const states = [currentState];
|
|
38
41
|
const hasPostponeRules = machine.postponeRules.length > 0;
|
|
@@ -74,8 +77,8 @@ const simulate = Effect.fn("effect-machine.simulate")(function* (input, events)
|
|
|
74
77
|
/**
|
|
75
78
|
* Assert that a machine can reach a specific state given a sequence of events
|
|
76
79
|
*/
|
|
77
|
-
const assertReaches = Effect.fn("effect-machine.assertReaches")(function* (input, events, expectedTag) {
|
|
78
|
-
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);
|
|
79
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(" -> ")}` });
|
|
80
83
|
return result.finalState;
|
|
81
84
|
});
|
|
@@ -91,8 +94,8 @@ const assertReaches = Effect.fn("effect-machine.assertReaches")(function* (input
|
|
|
91
94
|
* )
|
|
92
95
|
* ```
|
|
93
96
|
*/
|
|
94
|
-
const assertPath = Effect.fn("effect-machine.assertPath")(function* (input, events, expectedPath) {
|
|
95
|
-
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);
|
|
96
99
|
const actualPath = result.states.map((s) => s._tag);
|
|
97
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(" -> ")}` });
|
|
98
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(" -> ")}` });
|
|
@@ -111,15 +114,15 @@ const assertPath = Effect.fn("effect-machine.assertPath")(function* (input, even
|
|
|
111
114
|
* )
|
|
112
115
|
* ```
|
|
113
116
|
*/
|
|
114
|
-
const assertNeverReaches = Effect.fn("effect-machine.assertNeverReaches")(function* (input, events, forbiddenTag) {
|
|
115
|
-
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);
|
|
116
119
|
const visitedIndex = result.states.findIndex((s) => s._tag === forbiddenTag);
|
|
117
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(" -> ")}` });
|
|
118
121
|
return result;
|
|
119
122
|
});
|
|
120
123
|
/**
|
|
121
124
|
* Create a test harness for step-by-step testing.
|
|
122
|
-
* Does not run onEnter/spawn/background effects, but does run
|
|
125
|
+
* Does not run onEnter/spawn/background effects, but does run slots
|
|
123
126
|
* within transition handlers.
|
|
124
127
|
*
|
|
125
128
|
* @example Basic usage
|
|
@@ -139,14 +142,8 @@ const assertNeverReaches = Effect.fn("effect-machine.assertNeverReaches")(functi
|
|
|
139
142
|
* ```
|
|
140
143
|
*/
|
|
141
144
|
const createTestHarness = Effect.fn("effect-machine.createTestHarness")(function* (input, options) {
|
|
142
|
-
const machine = input
|
|
143
|
-
const
|
|
144
|
-
const dummySelf = {
|
|
145
|
-
send: dummySend,
|
|
146
|
-
cast: dummySend,
|
|
147
|
-
spawn: () => Effect.die("spawn not supported in test harness"),
|
|
148
|
-
reply: () => Effect.succeed(false)
|
|
149
|
-
};
|
|
145
|
+
const machine = materializeMachine(input, options?.slots);
|
|
146
|
+
const dummySelf = makeDummySelf("effect-machine.testing.harness");
|
|
150
147
|
const stateRef = yield* SubscriptionRef.make(machine.initial);
|
|
151
148
|
const hasPostponeRules = machine.postponeRules.length > 0;
|
|
152
149
|
const postponed = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "effect-machine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/cevr/effect-machine.git"
|
|
@@ -56,20 +56,20 @@
|
|
|
56
56
|
"release": "bun run build && changeset publish"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"effect": "4.0.0-beta.
|
|
59
|
+
"effect": "4.0.0-beta.43"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@changesets/changelog-github": "^0.6.0",
|
|
63
63
|
"@changesets/cli": "^2.30.0",
|
|
64
|
-
"@effect/language-service": "^0.
|
|
64
|
+
"@effect/language-service": "^0.84.2",
|
|
65
65
|
"@types/bun": "1.3.11",
|
|
66
66
|
"concurrently": "^9.2.1",
|
|
67
67
|
"effect-bun-test": "0.3.0",
|
|
68
68
|
"effect-v3": "npm:effect@^3.21.0",
|
|
69
69
|
"lefthook": "^2.1.4",
|
|
70
|
-
"oxfmt": "^0.
|
|
71
|
-
"oxlint": "^1.
|
|
72
|
-
"tsdown": "^0.21.
|
|
70
|
+
"oxfmt": "^0.42.0",
|
|
71
|
+
"oxlint": "^1.57.0",
|
|
72
|
+
"tsdown": "^0.21.7",
|
|
73
73
|
"typescript": "^5.9.3"
|
|
74
74
|
},
|
|
75
75
|
"peerDependencies": {
|
|
@@ -81,6 +81,6 @@
|
|
|
81
81
|
}
|
|
82
82
|
},
|
|
83
83
|
"overrides": {
|
|
84
|
-
"effect": "4.0.0-beta.
|
|
84
|
+
"effect": "4.0.0-beta.43"
|
|
85
85
|
}
|
|
86
86
|
}
|
package/v3/dist/actor.d.ts
CHANGED
|
@@ -1,30 +1,15 @@
|
|
|
1
|
-
import { EffectsDef, GuardsDef, MachineContext } from "./slot.js";
|
|
2
1
|
import { ExtractReply, ReplyTypeBrand } from "./internal/brands.js";
|
|
3
2
|
import { ActorStoppedError, DuplicateActorError, NoReplyError } from "./errors.js";
|
|
3
|
+
import { ProvideSlots, SlotsDef } from "./slot.js";
|
|
4
|
+
import { ActorExit, Supervision } from "./supervision.js";
|
|
4
5
|
import { ProcessEventError, ProcessEventHooks, ProcessEventResult, processEventCore, resolveTransition, runSpawnEffects } from "./internal/transition.js";
|
|
5
|
-
import {
|
|
6
|
+
import { Machine, PersistConfig } from "./machine.js";
|
|
7
|
+
import { RuntimeQueuedEvent } from "./internal/runtime.js";
|
|
6
8
|
import { Context, Deferred, Effect, Layer, Option, PubSub, Queue, Ref, Scope, Stream, SubscriptionRef } from "effect";
|
|
7
|
-
import * as effect_dist_dts_Tracer_js0 from "effect/dist/dts/Tracer.js";
|
|
8
9
|
|
|
9
10
|
//#region src/actor.d.ts
|
|
10
|
-
/** Discriminated mailbox request */
|
|
11
|
-
type QueuedEvent<E> =
|
|
12
|
-
readonly _tag: "send";
|
|
13
|
-
readonly event: E;
|
|
14
|
-
} | {
|
|
15
|
-
readonly _tag: "call";
|
|
16
|
-
readonly event: E;
|
|
17
|
-
readonly reply: Deferred.Deferred<ProcessEventResult<{
|
|
18
|
-
readonly _tag: string;
|
|
19
|
-
}>, ActorStoppedError>;
|
|
20
|
-
} | {
|
|
21
|
-
readonly _tag: "ask";
|
|
22
|
-
readonly event: E;
|
|
23
|
-
readonly reply: Deferred.Deferred<unknown, NoReplyError | ActorStoppedError>;
|
|
24
|
-
};
|
|
25
|
-
/**
|
|
26
|
-
* Reference to a running actor.
|
|
27
|
-
*/
|
|
11
|
+
/** Discriminated mailbox request — alias for RuntimeQueuedEvent */
|
|
12
|
+
type QueuedEvent<E> = RuntimeQueuedEvent<E>;
|
|
28
13
|
/**
|
|
29
14
|
* Sync projection of ActorRef for non-Effect boundaries (React hooks, framework callbacks).
|
|
30
15
|
*/
|
|
@@ -104,6 +89,25 @@ interface ActorRef<State extends {
|
|
|
104
89
|
};
|
|
105
90
|
/** Subscribe to state changes (sync callback). Returns unsubscribe function. */
|
|
106
91
|
readonly subscribe: (fn: (state: State) => void) => () => void;
|
|
92
|
+
/**
|
|
93
|
+
* Wait for this actor's terminal exit. Resolves with the exit reason.
|
|
94
|
+
* Set exactly once when the actor terminates (final, stop, drain, or defect).
|
|
95
|
+
*/
|
|
96
|
+
readonly awaitExit: Effect.Effect<ActorExit<State>>;
|
|
97
|
+
/**
|
|
98
|
+
* Watch another actor. Returns an Effect that resolves with the exit reason
|
|
99
|
+
* when the watched actor terminally stops. Ignores restarts (Step 3).
|
|
100
|
+
* Built on the other actor's exitDeferred — authoritative, not system events.
|
|
101
|
+
*/
|
|
102
|
+
readonly watch: (other: {
|
|
103
|
+
readonly id: string;
|
|
104
|
+
readonly awaitExit: Effect.Effect<ActorExit<unknown>>;
|
|
105
|
+
}) => Effect.Effect<ActorExit<unknown>>;
|
|
106
|
+
/**
|
|
107
|
+
* Drain: process all remaining events in the queue, then stop.
|
|
108
|
+
* Unlike `stop` (which interrupts immediately), `drain` lets the actor finish its work.
|
|
109
|
+
*/
|
|
110
|
+
readonly drain: Effect.Effect<void>;
|
|
107
111
|
/** Sync helpers for non-Effect boundaries. */
|
|
108
112
|
readonly sync: ActorRefSync<State, Event>;
|
|
109
113
|
/** The actor system this actor belongs to. */
|
|
@@ -122,10 +126,17 @@ type SystemEvent = {
|
|
|
122
126
|
readonly _tag: "ActorSpawned";
|
|
123
127
|
readonly id: string;
|
|
124
128
|
readonly actor: ActorRef<AnyState, unknown>;
|
|
129
|
+
} | {
|
|
130
|
+
readonly _tag: "ActorRestarted";
|
|
131
|
+
readonly id: string;
|
|
132
|
+
readonly actor: ActorRef<AnyState, unknown>;
|
|
133
|
+
readonly generation: number;
|
|
134
|
+
readonly exit: ActorExit<unknown>;
|
|
125
135
|
} | {
|
|
126
136
|
readonly _tag: "ActorStopped";
|
|
127
137
|
readonly id: string;
|
|
128
138
|
readonly actor: ActorRef<AnyState, unknown>;
|
|
139
|
+
readonly exit: ActorExit<unknown>;
|
|
129
140
|
};
|
|
130
141
|
/**
|
|
131
142
|
* Listener callback for system events.
|
|
@@ -140,15 +151,18 @@ interface ActorSystem {
|
|
|
140
151
|
*
|
|
141
152
|
* @example
|
|
142
153
|
* ```ts
|
|
143
|
-
* const
|
|
144
|
-
* const actor = yield* system.spawn("my-actor", built);
|
|
154
|
+
* const actor = yield* system.spawn("my-actor", machine);
|
|
145
155
|
* ```
|
|
146
156
|
*/
|
|
147
157
|
readonly spawn: <S extends {
|
|
148
158
|
readonly _tag: string;
|
|
149
159
|
}, E extends {
|
|
150
160
|
readonly _tag: string;
|
|
151
|
-
}, R
|
|
161
|
+
}, R, SD extends SlotsDef = Record<string, never>>(id: string, machine: Machine<S, E, R, any, any, SD>, options?: {
|
|
162
|
+
readonly supervision?: Supervision.Policy;
|
|
163
|
+
readonly slots?: ProvideSlots<SD, any>;
|
|
164
|
+
readonly persist?: PersistConfig<S>;
|
|
165
|
+
}) => Effect.Effect<ActorRef<S, E>, DuplicateActorError, R>;
|
|
152
166
|
/**
|
|
153
167
|
* Get an existing actor by ID
|
|
154
168
|
*/
|
|
@@ -184,28 +198,37 @@ type Listeners<S> = Set<(state: S) => void>;
|
|
|
184
198
|
*/
|
|
185
199
|
declare const notifyListeners: <S>(listeners: Listeners<S>, state: S) => void;
|
|
186
200
|
/**
|
|
187
|
-
* Build core ActorRef methods
|
|
201
|
+
* Build core ActorRef methods.
|
|
188
202
|
*/
|
|
189
203
|
declare const buildActorRefCore: <S extends {
|
|
190
204
|
readonly _tag: string;
|
|
191
205
|
}, E extends {
|
|
192
206
|
readonly _tag: string;
|
|
193
|
-
}, R,
|
|
207
|
+
}, R, SD extends SlotsDef>(id: string, machine: Machine<S, E, R, any, any, SD>, stateRef: SubscriptionRef.SubscriptionRef<S>, eventQueueRef: Ref.Ref<Queue.Queue<QueuedEvent<E>>>, stoppedRef: Ref.Ref<boolean>, listeners: Listeners<S>, stop: Effect.Effect<void>, system: ActorSystem, childrenMap: ReadonlyMap<string, ActorRef<AnyState, unknown>>, pendingReplies: Set<Deferred.Deferred<unknown, unknown>>, transitionsPubSub: PubSub.PubSub<TransitionInfo<S, E>> | undefined, exitDeferred: Deferred.Deferred<ActorExit<S>, never>) => ActorRef<S, E>;
|
|
194
208
|
/**
|
|
195
|
-
* Create and start an actor for a machine
|
|
209
|
+
* Create and start an actor for a machine.
|
|
210
|
+
* Delegates to the shared runtime kernel with actor-specific lifecycle hooks.
|
|
196
211
|
*/
|
|
197
212
|
declare const createActor: <S extends {
|
|
198
213
|
readonly _tag: string;
|
|
199
214
|
}, E extends {
|
|
200
215
|
readonly _tag: string;
|
|
201
|
-
}, R,
|
|
216
|
+
}, R, SD extends SlotsDef>(id: string, machine: Machine<S, E, R, any, any, SD>, options?: {
|
|
202
217
|
initialState?: S;
|
|
203
|
-
|
|
218
|
+
supervision?: Supervision.Policy;
|
|
219
|
+
persist?: PersistConfig<S>; /** @internal Called by system after each restart — emits ActorRestarted system event */
|
|
220
|
+
onRestart?: (generation: number, exit: ActorExit<unknown>) => Effect.Effect<void>;
|
|
221
|
+
} | undefined) => Effect.Effect<ActorRef<S, E>, never, never>;
|
|
204
222
|
/** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
|
|
205
223
|
declare const settlePendingReplies: (pendingReplies: Set<Deferred.Deferred<unknown, unknown>>, actorId: string) => Effect.Effect<void, never, never>;
|
|
224
|
+
/**
|
|
225
|
+
* Create an ActorSystem instance. Must be run in a Scope.
|
|
226
|
+
* @internal — use Default layer for normal usage
|
|
227
|
+
*/
|
|
228
|
+
declare const makeSystem: () => Effect.Effect<ActorSystem, never, Scope.Scope>;
|
|
206
229
|
/**
|
|
207
230
|
* Default ActorSystem layer
|
|
208
231
|
*/
|
|
209
232
|
declare const Default: Layer.Layer<ActorSystem, never, never>;
|
|
210
233
|
//#endregion
|
|
211
|
-
export { ActorRef, ActorRefSync, ActorSystem, Default, Listeners, type ProcessEventError, type ProcessEventHooks, type ProcessEventResult, QueuedEvent, SystemEvent, SystemEventListener, TransitionInfo, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };
|
|
234
|
+
export { ActorRef, ActorRefSync, ActorSystem, Default, Listeners, type ProcessEventError, type ProcessEventHooks, type ProcessEventResult, QueuedEvent, SystemEvent, SystemEventListener, TransitionInfo, buildActorRefCore, createActor, makeSystem, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };
|