effect-machine 0.18.0 → 0.20.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 +183 -138
- package/dist/actor.d.ts +65 -22
- package/dist/actor.js +172 -78
- package/dist/atom.d.ts +56 -0
- package/dist/atom.js +47 -0
- package/dist/cluster/entity-machine.d.ts +9 -3
- package/dist/cluster/entity-machine.js +8 -8
- package/dist/cluster/index.d.ts +2 -2
- package/dist/cluster/to-entity.d.ts +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/inspection.d.ts +31 -3
- package/dist/inspection.js +21 -0
- package/dist/internal/inspection.d.ts +1 -1
- package/dist/internal/inspection.js +23 -1
- package/dist/internal/machine-definition.d.ts +1 -0
- package/dist/internal/machine-initialization.d.ts +21 -0
- package/dist/internal/machine-initialization.js +27 -0
- package/dist/internal/runtime.d.ts +15 -1
- package/dist/internal/runtime.js +67 -26
- package/dist/internal/transition.d.ts +51 -3
- package/dist/internal/transition.js +216 -38
- package/dist/machine.d.ts +151 -40
- package/dist/machine.js +139 -40
- package/dist/supervision.d.ts +3 -2
- package/dist/supervision.js +3 -2
- package/dist/testing.d.ts +39 -67
- package/dist/testing.js +13 -52
- package/package.json +29 -3
package/dist/actor.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { ActorStoppedError, DuplicateActorError } from "./errors.js";
|
|
2
|
-
import { resolveTransition } from "./internal/transition.js";
|
|
2
|
+
import { resolveTransition, resolveTransitionEffect } from "./internal/transition.js";
|
|
3
3
|
import { emitWithTimestamp, makeInspectionHooks } from "./internal/inspection.js";
|
|
4
4
|
import { Inspector } from "./inspection.js";
|
|
5
|
+
import { ActorExit } from "./supervision.js";
|
|
5
6
|
import { createRuntime } from "./internal/runtime.js";
|
|
6
|
-
import { Context, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, PubSub, Queue, Ref, Schedule, Scope, Semaphore, Stream, SubscriptionRef } from "effect";
|
|
7
|
+
import { Cause, Context, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, PubSub, Queue, Ref, Schedule, Scope, Semaphore, Stream, SubscriptionRef } from "effect";
|
|
7
8
|
//#region src/actor.ts
|
|
8
9
|
/**
|
|
9
10
|
* Actor system: spawning, lifecycle, and event processing.
|
|
@@ -35,11 +36,20 @@ const notifyListeners = (listeners, state) => {
|
|
|
35
36
|
listener(state);
|
|
36
37
|
} catch {}
|
|
37
38
|
};
|
|
39
|
+
const toActorExit = (machine, exit) => {
|
|
40
|
+
if (exit._tag === "Final") return ActorExit.Final(exit.state, machine._output(exit.state));
|
|
41
|
+
if (exit._tag === "Defect") return {
|
|
42
|
+
_tag: "Defect",
|
|
43
|
+
cause: exit.cause,
|
|
44
|
+
phase: exit.phase
|
|
45
|
+
};
|
|
46
|
+
return { _tag: "Stopped" };
|
|
47
|
+
};
|
|
38
48
|
/**
|
|
39
49
|
* Build core ActorRef methods.
|
|
40
50
|
*/
|
|
41
|
-
const buildActorRefCore = (cell, stop, start) => {
|
|
42
|
-
const { id, machine, stateRef, runtimeRef, listeners, system } = cell;
|
|
51
|
+
const buildActorRefCore = (cell, stop, start, serviceContext) => {
|
|
52
|
+
const { id, machine, stateRef, runtimeRef, listeners, system, lifecycleRef, latestTransitionRef } = cell;
|
|
43
53
|
const send = (event) => Effect.gen(function* () {
|
|
44
54
|
const runtime = runtimeRef.current;
|
|
45
55
|
if (runtime !== void 0) yield* runtime.send(event);
|
|
@@ -59,7 +69,8 @@ const buildActorRefCore = (cell, stop, start) => {
|
|
|
59
69
|
reply: void 0,
|
|
60
70
|
postponed: false,
|
|
61
71
|
lifecycleRan: false,
|
|
62
|
-
isFinal: machine._isFinal(currentState._tag)
|
|
72
|
+
isFinal: machine._isFinal(currentState._tag),
|
|
73
|
+
transitions: []
|
|
63
74
|
};
|
|
64
75
|
});
|
|
65
76
|
const call = (event) => Effect.suspend(() => {
|
|
@@ -76,10 +87,11 @@ const buildActorRefCore = (cell, stop, start) => {
|
|
|
76
87
|
const matches = Effect.fn("effect-machine.actor.matches")(function* (tag) {
|
|
77
88
|
return (yield* SubscriptionRef.get(stateRef))._tag === tag;
|
|
78
89
|
});
|
|
79
|
-
const
|
|
90
|
+
const canEffect = Effect.fn("effect-machine.actor.can")(function* (event) {
|
|
80
91
|
const state = yield* SubscriptionRef.get(stateRef);
|
|
81
|
-
return
|
|
92
|
+
return (yield* resolveTransitionEffect(machine, state, event)) !== void 0;
|
|
82
93
|
});
|
|
94
|
+
const can = (event) => canEffect(event).pipe(Effect.provide(serviceContext));
|
|
83
95
|
const waitFor = Effect.fn("effect-machine.actor.waitFor")(function* (predicateOrState) {
|
|
84
96
|
let predicate;
|
|
85
97
|
if (typeof predicateOrState === "function" && !("_tag" in predicateOrState)) predicate = predicateOrState;
|
|
@@ -101,18 +113,53 @@ const buildActorRefCore = (cell, stop, start) => {
|
|
|
101
113
|
return result;
|
|
102
114
|
});
|
|
103
115
|
const awaitFinal = waitFor((state) => machine._isFinal(state._tag)).pipe(Effect.withSpan("effect-machine.actor.awaitFinal"));
|
|
116
|
+
const awaitOutput = Deferred.await(cell.terminalExitDeferred).pipe(Effect.flatMap((exit) => {
|
|
117
|
+
if (exit._tag === "Final") return Effect.succeed(exit.output);
|
|
118
|
+
if (exit._tag === "Defect") return Effect.die(Cause.squash(exit.cause));
|
|
119
|
+
return ActorStoppedError.make({ actorId: id });
|
|
120
|
+
}), Effect.withSpan("effect-machine.actor.awaitOutput"));
|
|
104
121
|
const sendAndWait = Effect.fn("effect-machine.actor.sendAndWait")(function* (event, predicateOrState) {
|
|
105
122
|
yield* send(event);
|
|
106
123
|
if (predicateOrState !== void 0) return yield* waitFor(predicateOrState);
|
|
107
124
|
return yield* awaitFinal;
|
|
108
125
|
});
|
|
109
126
|
const transitions = Stream.fromPubSub(cell.transitions);
|
|
127
|
+
const subscribe = (listener) => {
|
|
128
|
+
listeners.add(listener);
|
|
129
|
+
return () => {
|
|
130
|
+
listeners.delete(listener);
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
const sendClient = (event) => {
|
|
134
|
+
runtimeRef.current?.sendSync(event);
|
|
135
|
+
};
|
|
136
|
+
const stopClient = () => {
|
|
137
|
+
Effect.runFork(stop);
|
|
138
|
+
};
|
|
139
|
+
const getSnapshot = () => Effect.runSync(SubscriptionRef.get(stateRef));
|
|
140
|
+
const matchesClient = (tag) => getSnapshot()._tag === tag;
|
|
141
|
+
const canSync = (event) => resolveTransition(machine, getSnapshot(), event) !== void 0;
|
|
142
|
+
const getLifecycle = () => Effect.runSync(SubscriptionRef.get(lifecycleRef));
|
|
143
|
+
const getLatestTransition = () => Effect.runSync(SubscriptionRef.get(latestTransitionRef));
|
|
144
|
+
const client = {
|
|
145
|
+
send: sendClient,
|
|
146
|
+
stop: stopClient,
|
|
147
|
+
getSnapshot,
|
|
148
|
+
matches: matchesClient,
|
|
149
|
+
canSync,
|
|
150
|
+
can: (event) => Effect.runPromise(can(event)),
|
|
151
|
+
getLifecycle,
|
|
152
|
+
getLatestTransition,
|
|
153
|
+
subscribe
|
|
154
|
+
};
|
|
110
155
|
return {
|
|
111
156
|
id,
|
|
112
157
|
send,
|
|
113
158
|
call,
|
|
114
159
|
ask,
|
|
115
160
|
state: stateRef,
|
|
161
|
+
lifecycle: lifecycleRef,
|
|
162
|
+
latestTransition: latestTransitionRef,
|
|
116
163
|
stop,
|
|
117
164
|
start,
|
|
118
165
|
snapshot,
|
|
@@ -122,26 +169,20 @@ const buildActorRefCore = (cell, stop, start) => {
|
|
|
122
169
|
transitions,
|
|
123
170
|
waitFor,
|
|
124
171
|
awaitFinal,
|
|
172
|
+
awaitOutput,
|
|
125
173
|
sendAndWait,
|
|
126
|
-
subscribe
|
|
127
|
-
|
|
128
|
-
return () => {
|
|
129
|
-
listeners.delete(fn);
|
|
130
|
-
};
|
|
131
|
-
},
|
|
174
|
+
subscribe,
|
|
175
|
+
client,
|
|
132
176
|
awaitExit: Deferred.await(cell.terminalExitDeferred),
|
|
133
177
|
drain: Effect.suspend(() => runtimeRef.current?.drain ?? Effect.void),
|
|
134
178
|
sync: {
|
|
135
|
-
send:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const state = Effect.runSync(SubscriptionRef.get(stateRef));
|
|
143
|
-
return resolveTransition(machine, state, event) !== void 0;
|
|
144
|
-
}
|
|
179
|
+
send: sendClient,
|
|
180
|
+
stop: stopClient,
|
|
181
|
+
snapshot: getSnapshot,
|
|
182
|
+
matches: matchesClient,
|
|
183
|
+
can: canSync,
|
|
184
|
+
lifecycle: getLifecycle,
|
|
185
|
+
latestTransition: getLatestTransition
|
|
145
186
|
},
|
|
146
187
|
system,
|
|
147
188
|
children: cell.children
|
|
@@ -175,7 +216,7 @@ const runSupervisionLoop = (cell, options) => Effect.gen(function* () {
|
|
|
175
216
|
if (currentRuntime === void 0) return;
|
|
176
217
|
const generationExit = yield* Deferred.await(currentRuntime.exitDeferred);
|
|
177
218
|
if (generationExit._tag !== "Defect") {
|
|
178
|
-
yield* Deferred.succeed(cell.terminalExitDeferred, generationExit);
|
|
219
|
+
yield* Deferred.succeed(cell.terminalExitDeferred, toActorExit(cell.machine, generationExit));
|
|
179
220
|
return;
|
|
180
221
|
}
|
|
181
222
|
if (options.supervision.shouldRestart !== void 0 && !options.supervision.shouldRestart(generationExit)) {
|
|
@@ -188,26 +229,33 @@ const runSupervisionLoop = (cell, options) => Effect.gen(function* () {
|
|
|
188
229
|
}
|
|
189
230
|
const nextGeneration = cell.generation.current + 1;
|
|
190
231
|
cell.generation.current = nextGeneration;
|
|
191
|
-
let restartState = cell.
|
|
232
|
+
let restartState = cell.machineInitial;
|
|
192
233
|
if (options.lifecycle?.recovery !== void 0) {
|
|
193
234
|
const resolved = yield* options.lifecycle.recovery.resolve({
|
|
194
235
|
actorId: cell.id,
|
|
195
236
|
generation: nextGeneration,
|
|
196
|
-
machineInitial: cell.
|
|
237
|
+
machineInitial: cell.machineInitial
|
|
197
238
|
});
|
|
198
239
|
if (Option.isSome(resolved)) restartState = resolved.value;
|
|
199
240
|
}
|
|
200
241
|
yield* currentRuntime.settlePendingRequests;
|
|
242
|
+
yield* SubscriptionRef.set(cell.lifecycleRef, {
|
|
243
|
+
_tag: "Starting",
|
|
244
|
+
generation: nextGeneration
|
|
245
|
+
});
|
|
201
246
|
const freshQueue = yield* Queue.unbounded();
|
|
202
247
|
yield* Ref.set(cell.eventQueueRef, freshQueue);
|
|
203
248
|
yield* SubscriptionRef.set(cell.stateRef, restartState);
|
|
204
249
|
yield* Ref.set(cell.stoppedRef, false);
|
|
205
250
|
cell.children.clear();
|
|
206
|
-
|
|
207
|
-
if (restartState !== cell.machine.initial) machineForRestart = cell.machine._withInitial(restartState);
|
|
208
|
-
const newRuntime = yield* options.spawnGeneration(machineForRestart);
|
|
251
|
+
const newRuntime = yield* options.spawnGeneration(cell.machine);
|
|
209
252
|
cell.runtimeRef.current = newRuntime;
|
|
210
253
|
yield* newRuntime.start;
|
|
254
|
+
const restartExit = yield* Deferred.poll(newRuntime.exitDeferred);
|
|
255
|
+
if (Option.isNone(restartExit)) yield* SubscriptionRef.set(cell.lifecycleRef, {
|
|
256
|
+
_tag: "Active",
|
|
257
|
+
generation: nextGeneration
|
|
258
|
+
});
|
|
211
259
|
if (options.onRestart !== void 0) yield* options.onRestart(nextGeneration, generationExit);
|
|
212
260
|
notifyListeners(cell.listeners, restartState);
|
|
213
261
|
}
|
|
@@ -217,9 +265,9 @@ const runSupervisionLoop = (cell, options) => Effect.gen(function* () {
|
|
|
217
265
|
* Delegates to the shared runtime kernel with actor-specific lifecycle hooks.
|
|
218
266
|
*/
|
|
219
267
|
const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machine, options) {
|
|
220
|
-
const lifecycle = options
|
|
268
|
+
const lifecycle = options.lifecycle;
|
|
221
269
|
const serviceContext = yield* Effect.context();
|
|
222
|
-
const initial = options
|
|
270
|
+
const initial = options.initialState;
|
|
223
271
|
yield* Effect.annotateCurrentSpan("effect_machine.actor.id", id);
|
|
224
272
|
yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", initial._tag);
|
|
225
273
|
const { system, implicitSystemScope } = yield* resolveActorSystem();
|
|
@@ -227,22 +275,24 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
227
275
|
const childrenMap = /* @__PURE__ */ new Map();
|
|
228
276
|
const listeners = /* @__PURE__ */ new Set();
|
|
229
277
|
const transitionsPubSub = yield* PubSub.unbounded();
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
278
|
+
const generation = { current: 0 };
|
|
279
|
+
const inspectionHooks = (runtimeGeneration) => {
|
|
280
|
+
if (inspectorValue === void 0) return void 0;
|
|
281
|
+
return makeInspectionHooks(id, inspectorValue, () => runtimeGeneration);
|
|
282
|
+
};
|
|
234
283
|
const stateRef = yield* SubscriptionRef.make(initial);
|
|
284
|
+
const lifecycleRef = yield* SubscriptionRef.make({ _tag: "Created" });
|
|
285
|
+
const latestTransitionRef = yield* SubscriptionRef.make(void 0);
|
|
235
286
|
const stoppedRef = yield* Ref.make(false);
|
|
236
287
|
const initialQueue = yield* Queue.unbounded();
|
|
237
288
|
const eventQueueRef = yield* Ref.make(initialQueue);
|
|
238
289
|
const terminalExitDeferred = yield* Deferred.make();
|
|
239
|
-
let stopEmitted = false;
|
|
240
|
-
const generation = { current: 0 };
|
|
241
290
|
const runtimeRef = { current: void 0 };
|
|
242
291
|
const supervisorFiberRef = { current: void 0 };
|
|
243
292
|
const cell = {
|
|
244
293
|
id,
|
|
245
294
|
machine,
|
|
295
|
+
machineInitial: options.machineInitial,
|
|
246
296
|
stateRef,
|
|
247
297
|
stoppedRef,
|
|
248
298
|
eventQueueRef,
|
|
@@ -251,16 +301,19 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
251
301
|
listeners,
|
|
252
302
|
children: childrenMap,
|
|
253
303
|
transitions: transitionsPubSub,
|
|
304
|
+
lifecycleRef,
|
|
305
|
+
latestTransitionRef,
|
|
254
306
|
system,
|
|
255
307
|
generation
|
|
256
308
|
};
|
|
257
309
|
/** Build lifecycle hooks for a generation */
|
|
258
|
-
const buildRuntimeLifecycle = () => {
|
|
259
|
-
stopEmitted = false;
|
|
310
|
+
const buildRuntimeLifecycle = (runtimeGeneration) => {
|
|
311
|
+
let stopEmitted = false;
|
|
260
312
|
let onEvent = void 0;
|
|
261
313
|
if (inspectorValue !== void 0) onEvent = (state, event) => emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
262
314
|
type: "@machine.event",
|
|
263
315
|
actorId: id,
|
|
316
|
+
generation: runtimeGeneration,
|
|
264
317
|
state,
|
|
265
318
|
event,
|
|
266
319
|
timestamp
|
|
@@ -271,6 +324,7 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
271
324
|
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
272
325
|
type: "@machine.stop",
|
|
273
326
|
actorId: id,
|
|
327
|
+
generation: runtimeGeneration,
|
|
274
328
|
finalState: state,
|
|
275
329
|
timestamp
|
|
276
330
|
}));
|
|
@@ -279,32 +333,39 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
279
333
|
if (inspectorValue !== void 0) onInitialSpawnEffects = (state) => emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
280
334
|
type: "@machine.effect",
|
|
281
335
|
actorId: id,
|
|
336
|
+
generation: runtimeGeneration,
|
|
282
337
|
effectType: "spawn",
|
|
283
338
|
state,
|
|
284
339
|
timestamp
|
|
285
340
|
}));
|
|
286
341
|
return {
|
|
287
342
|
onEvent,
|
|
288
|
-
onStateChange: (result, event) => {
|
|
343
|
+
onStateChange: (result, event) => Effect.gen(function* () {
|
|
344
|
+
const latest = result.transitions.at(-1);
|
|
345
|
+
if (latest !== void 0) yield* SubscriptionRef.set(latestTransitionRef, {
|
|
346
|
+
fromState: latest.previousState,
|
|
347
|
+
toState: latest.newState,
|
|
348
|
+
event: latest.event
|
|
349
|
+
});
|
|
289
350
|
notifyListeners(listeners, result.newState);
|
|
290
351
|
const durability = lifecycle?.durability;
|
|
291
352
|
if (durability === void 0 || !result.transitioned) return;
|
|
292
353
|
if (!(durability.shouldSave === void 0 || durability.shouldSave(result.newState, result.previousState))) return;
|
|
293
|
-
|
|
354
|
+
yield* durability.save({
|
|
294
355
|
actorId: id,
|
|
295
|
-
generation:
|
|
356
|
+
generation: runtimeGeneration,
|
|
296
357
|
previousState: result.previousState,
|
|
297
358
|
nextState: result.newState,
|
|
298
359
|
event
|
|
299
360
|
});
|
|
300
|
-
},
|
|
301
|
-
onProcessed: (result,
|
|
361
|
+
}),
|
|
362
|
+
onProcessed: (result, _event) => {
|
|
302
363
|
if (!result.transitioned || transitionsPubSub.subscribers.size === 0) return;
|
|
303
|
-
return PubSub.publish(transitionsPubSub, {
|
|
304
|
-
fromState:
|
|
305
|
-
toState:
|
|
306
|
-
event
|
|
307
|
-
})
|
|
364
|
+
return Effect.forEach(result.transitions, (transition) => PubSub.publish(transitionsPubSub, {
|
|
365
|
+
fromState: transition.previousState,
|
|
366
|
+
toState: transition.newState,
|
|
367
|
+
event: transition.event
|
|
368
|
+
}), { discard: true });
|
|
308
369
|
},
|
|
309
370
|
onFinal,
|
|
310
371
|
onShutdown: () => Effect.gen(function* () {
|
|
@@ -313,6 +374,7 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
313
374
|
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
314
375
|
type: "@machine.stop",
|
|
315
376
|
actorId: id,
|
|
377
|
+
generation: runtimeGeneration,
|
|
316
378
|
finalState,
|
|
317
379
|
timestamp
|
|
318
380
|
}));
|
|
@@ -322,45 +384,52 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
322
384
|
};
|
|
323
385
|
};
|
|
324
386
|
/** Create a single runtime generation. machineForGen is machineWithState for initial, machine for restarts. */
|
|
325
|
-
const spawnGeneration = (machineForGen) =>
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
childrenMap.
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
387
|
+
const spawnGeneration = (machineForGen) => {
|
|
388
|
+
const runtimeGeneration = generation.current;
|
|
389
|
+
return Ref.get(eventQueueRef).pipe(Effect.flatMap((currentQueue) => createRuntime(machineForGen, system, {
|
|
390
|
+
actorId: id,
|
|
391
|
+
generation: runtimeGeneration,
|
|
392
|
+
hooks: inspectionHooks(runtimeGeneration),
|
|
393
|
+
skipFinalizer: true,
|
|
394
|
+
cellResources: {
|
|
395
|
+
stateRef,
|
|
396
|
+
stoppedRef,
|
|
397
|
+
eventQueue: currentQueue
|
|
398
|
+
},
|
|
399
|
+
lifecycle: buildRuntimeLifecycle(runtimeGeneration),
|
|
400
|
+
onChildSpawned: (childId, child) => Effect.gen(function* () {
|
|
401
|
+
childrenMap.set(childId, child);
|
|
402
|
+
const maybeScope = yield* Effect.serviceOption(Scope.Scope);
|
|
403
|
+
if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, Effect.sync(() => {
|
|
404
|
+
childrenMap.delete(childId);
|
|
405
|
+
}));
|
|
406
|
+
})
|
|
407
|
+
})));
|
|
408
|
+
};
|
|
409
|
+
runtimeRef.current = yield* spawnGeneration(machine);
|
|
410
|
+
const supervision = options.supervision;
|
|
345
411
|
const stop = Effect.fn("effect-machine.actor.stop")(function* () {
|
|
346
412
|
if (supervisorFiberRef.current !== void 0) yield* Fiber.interrupt(supervisorFiberRef.current);
|
|
347
413
|
const currentRuntime = runtimeRef.current;
|
|
348
414
|
if (currentRuntime !== void 0) yield* currentRuntime.stop;
|
|
349
|
-
yield* Deferred.succeed(terminalExitDeferred,
|
|
415
|
+
yield* Deferred.succeed(terminalExitDeferred, ActorExit.Stopped);
|
|
350
416
|
if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
|
|
351
417
|
})().pipe(Effect.provide(serviceContext), Effect.asVoid);
|
|
352
|
-
const isHydrated = options
|
|
418
|
+
const isHydrated = options.hydrated === true;
|
|
353
419
|
const start = Effect.fn("effect-machine.actor.start")(function* () {
|
|
420
|
+
yield* SubscriptionRef.set(lifecycleRef, {
|
|
421
|
+
_tag: "Starting",
|
|
422
|
+
generation: generation.current
|
|
423
|
+
});
|
|
354
424
|
if (lifecycle?.recovery !== void 0 && !isHydrated) {
|
|
355
425
|
const resolved = yield* lifecycle.recovery.resolve({
|
|
356
426
|
actorId: id,
|
|
357
427
|
generation: generation.current,
|
|
358
|
-
machineInitial:
|
|
428
|
+
machineInitial: options.machineInitial
|
|
359
429
|
});
|
|
360
430
|
if (Option.isSome(resolved)) {
|
|
361
431
|
yield* SubscriptionRef.set(stateRef, resolved.value);
|
|
362
|
-
const
|
|
363
|
-
const newRuntime = yield* spawnGeneration(recoveredMachine);
|
|
432
|
+
const newRuntime = yield* spawnGeneration(machine);
|
|
364
433
|
runtimeRef.current = newRuntime;
|
|
365
434
|
}
|
|
366
435
|
}
|
|
@@ -368,6 +437,7 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
368
437
|
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
369
438
|
type: "@machine.spawn",
|
|
370
439
|
actorId: id,
|
|
440
|
+
generation: generation.current,
|
|
371
441
|
initialState: currentState,
|
|
372
442
|
timestamp
|
|
373
443
|
}));
|
|
@@ -375,16 +445,24 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
375
445
|
supervision,
|
|
376
446
|
spawnGeneration,
|
|
377
447
|
lifecycle,
|
|
378
|
-
onRestart: options
|
|
448
|
+
onRestart: options.onRestart
|
|
379
449
|
}));
|
|
380
450
|
else {
|
|
381
451
|
const currentRuntime = runtimeRef.current;
|
|
382
|
-
if (currentRuntime !== void 0) yield* Effect.forkDetach(Deferred.await(currentRuntime.exitDeferred).pipe(Effect.tap((exit) => Deferred.succeed(terminalExitDeferred, exit))));
|
|
452
|
+
if (currentRuntime !== void 0) yield* Effect.forkDetach(Deferred.await(currentRuntime.exitDeferred).pipe(Effect.tap((exit) => Deferred.succeed(terminalExitDeferred, toActorExit(machine, exit)))));
|
|
383
453
|
}
|
|
384
454
|
const currentRuntime = runtimeRef.current;
|
|
385
|
-
if (currentRuntime !== void 0)
|
|
455
|
+
if (currentRuntime !== void 0) {
|
|
456
|
+
yield* currentRuntime.start;
|
|
457
|
+
const currentExit = yield* Deferred.poll(currentRuntime.exitDeferred);
|
|
458
|
+
if (Option.isNone(currentExit)) yield* SubscriptionRef.set(lifecycleRef, {
|
|
459
|
+
_tag: "Active",
|
|
460
|
+
generation: generation.current
|
|
461
|
+
});
|
|
462
|
+
}
|
|
386
463
|
})().pipe(Effect.provide(serviceContext), Effect.asVoid);
|
|
387
|
-
|
|
464
|
+
yield* Effect.forkDetach(Deferred.await(terminalExitDeferred).pipe(Effect.flatMap((exit) => SubscriptionRef.set(lifecycleRef, exit)), Effect.provide(serviceContext)));
|
|
465
|
+
return buildActorRefCore(cell, stop, start, serviceContext);
|
|
388
466
|
});
|
|
389
467
|
/** Notify all system event listeners (sync). */
|
|
390
468
|
const notifySystemListeners = (listeners, event) => {
|
|
@@ -418,6 +496,17 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
418
496
|
id,
|
|
419
497
|
actor: actorRef
|
|
420
498
|
});
|
|
499
|
+
yield* Effect.forkDetach(actorRef.awaitExit.pipe(Effect.flatMap((exit) => {
|
|
500
|
+
const registered = MutableHashMap.get(actorsMap, id);
|
|
501
|
+
if (Option.isNone(registered) || registered.value !== actorRef) return Effect.void;
|
|
502
|
+
MutableHashMap.remove(actorsMap, id);
|
|
503
|
+
return emitSystemEvent({
|
|
504
|
+
_tag: "ActorStopped",
|
|
505
|
+
id,
|
|
506
|
+
actor: actorRef,
|
|
507
|
+
exit
|
|
508
|
+
});
|
|
509
|
+
})));
|
|
421
510
|
const maybeScope = yield* Effect.serviceOption(ActorScope);
|
|
422
511
|
if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, Effect.gen(function* () {
|
|
423
512
|
if (MutableHashMap.has(actorsMap, id)) {
|
|
@@ -448,7 +537,12 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
448
537
|
exit
|
|
449
538
|
});
|
|
450
539
|
};
|
|
540
|
+
const machineInitial = machine._initial(spawnOptions?.input);
|
|
541
|
+
const initialState = spawnOptions?.hydrate ?? machineInitial;
|
|
451
542
|
const actor = yield* createActor(id, machine, {
|
|
543
|
+
initialState,
|
|
544
|
+
machineInitial,
|
|
545
|
+
hydrated: spawnOptions?.hydrate !== void 0,
|
|
452
546
|
supervision: spawnOptions?.supervision,
|
|
453
547
|
lifecycle: spawnOptions?.lifecycle,
|
|
454
548
|
onRestart
|
package/dist/atom.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ActorLifecycle, ActorRef, TransitionInfo } from "./actor.js";
|
|
2
|
+
import * as Atom from "effect/unstable/reactivity/Atom";
|
|
3
|
+
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
|
|
4
|
+
//#region src/atom.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* A writable Atom projection of an actor.
|
|
7
|
+
*
|
|
8
|
+
* The Atom value is the current actor state. Atom writes send actor events.
|
|
9
|
+
*/
|
|
10
|
+
type ActorAtom<State, Event> = Atom.Writable<State, Event>;
|
|
11
|
+
/**
|
|
12
|
+
* Make a writable Atom from an actor.
|
|
13
|
+
*
|
|
14
|
+
* The actor stays the single state owner. The Atom follows recovery,
|
|
15
|
+
* supervision restarts, and normal transitions through the actor's
|
|
16
|
+
* SubscriptionRef.
|
|
17
|
+
*/
|
|
18
|
+
declare const make: <State extends {
|
|
19
|
+
readonly _tag: string;
|
|
20
|
+
}, Event, Output>(actor: ActorRef<State, Event, Output>) => ActorAtom<State, Event>;
|
|
21
|
+
/**
|
|
22
|
+
* Select part of an actor state.
|
|
23
|
+
*
|
|
24
|
+
* The selected Atom stays writable. Writes still send events to the actor.
|
|
25
|
+
* The equality function controls when Atom subscribers receive a new value.
|
|
26
|
+
*/
|
|
27
|
+
declare const select: {
|
|
28
|
+
<State, Selection>(selector: (state: State) => Selection, equals?: (value: Selection, next: Selection) => boolean): <Event>(self: ActorAtom<State, Event>) => ActorAtom<Selection, Event>;
|
|
29
|
+
<State, Event, Selection>(self: ActorAtom<State, Event>, selector: (state: State) => Selection, equals?: (value: Selection, next: Selection) => boolean): ActorAtom<Selection, Event>;
|
|
30
|
+
};
|
|
31
|
+
/** Observe actor lifecycle without coupling it to domain state. */
|
|
32
|
+
declare const lifecycle: <State extends {
|
|
33
|
+
readonly _tag: string;
|
|
34
|
+
}, Event, Output>(actor: ActorRef<State, Event, Output>) => Atom.Atom<ActorLifecycle<State, Output>>;
|
|
35
|
+
/** Observe the latest accepted edge. The value remains after actor exit. */
|
|
36
|
+
declare const latestTransition: <State extends {
|
|
37
|
+
readonly _tag: string;
|
|
38
|
+
}, Event, Output>(actor: ActorRef<State, Event, Output>) => Atom.Atom<TransitionInfo<State, Event> | undefined>;
|
|
39
|
+
/** A reactive result for whether an actor can accept one event. */
|
|
40
|
+
type CanAtom = Atom.Atom<AsyncResult.AsyncResult<boolean>>;
|
|
41
|
+
/**
|
|
42
|
+
* Observe whether an event has an enabled transition.
|
|
43
|
+
*
|
|
44
|
+
* The Atom reevaluates after each actor state change. It supports pure and
|
|
45
|
+
* Effect predicates. Effect predicates use the context captured by the actor.
|
|
46
|
+
*/
|
|
47
|
+
declare const can: {
|
|
48
|
+
<Event>(event: Event): <State extends {
|
|
49
|
+
readonly _tag: string;
|
|
50
|
+
}, Output>(actor: ActorRef<State, Event, Output>) => CanAtom;
|
|
51
|
+
<State extends {
|
|
52
|
+
readonly _tag: string;
|
|
53
|
+
}, Event, Output>(actor: ActorRef<State, Event, Output>, event: Event): CanAtom;
|
|
54
|
+
};
|
|
55
|
+
//#endregion
|
|
56
|
+
export { ActorAtom, CanAtom, can, latestTransition, lifecycle, make, select };
|
package/dist/atom.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { dual } from "effect/Function";
|
|
2
|
+
import * as Atom from "effect/unstable/reactivity/Atom";
|
|
3
|
+
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
|
|
4
|
+
//#region src/atom.ts
|
|
5
|
+
/**
|
|
6
|
+
* Effect Atom integration for actors.
|
|
7
|
+
*
|
|
8
|
+
* The adapter keeps the actor as the state owner. Atom registries observe the
|
|
9
|
+
* actor's SubscriptionRef and write events through its synchronous boundary.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Make a writable Atom from an actor.
|
|
13
|
+
*
|
|
14
|
+
* The actor stays the single state owner. The Atom follows recovery,
|
|
15
|
+
* supervision restarts, and normal transitions through the actor's
|
|
16
|
+
* SubscriptionRef.
|
|
17
|
+
*/
|
|
18
|
+
const make = (actor) => {
|
|
19
|
+
const state = Atom.subscriptionRef(actor.state);
|
|
20
|
+
return Atom.writable((get) => get(state), (_ctx, event) => actor.client.send(event));
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Select part of an actor state.
|
|
24
|
+
*
|
|
25
|
+
* The selected Atom stays writable. Writes still send events to the actor.
|
|
26
|
+
* The equality function controls when Atom subscribers receive a new value.
|
|
27
|
+
*/
|
|
28
|
+
const select = dual((args) => Atom.isAtom(args[0]), (self, selector, equals = Object.is) => Atom.withEquality(Atom.map(self, selector), equals));
|
|
29
|
+
/** Observe actor lifecycle without coupling it to domain state. */
|
|
30
|
+
const lifecycle = (actor) => Atom.subscriptionRef(actor.lifecycle);
|
|
31
|
+
/** Observe the latest accepted edge. The value remains after actor exit. */
|
|
32
|
+
const latestTransition = (actor) => Atom.subscriptionRef(actor.latestTransition);
|
|
33
|
+
/**
|
|
34
|
+
* Observe whether an event has an enabled transition.
|
|
35
|
+
*
|
|
36
|
+
* The Atom reevaluates after each actor state change. It supports pure and
|
|
37
|
+
* Effect predicates. Effect predicates use the context captured by the actor.
|
|
38
|
+
*/
|
|
39
|
+
const can = dual(2, (actor, event) => {
|
|
40
|
+
const state = Atom.subscriptionRef(actor.state);
|
|
41
|
+
return Atom.make((get) => {
|
|
42
|
+
get(state);
|
|
43
|
+
return actor.can(event);
|
|
44
|
+
}).pipe(Atom.withEquality((value, next) => AsyncResult.isSuccess(value) && AsyncResult.isSuccess(next) && Object.is(value.value, next.value)));
|
|
45
|
+
});
|
|
46
|
+
//#endregion
|
|
47
|
+
export { can, latestTransition, lifecycle, make, select };
|
|
@@ -7,7 +7,7 @@ import { Rpc } from "effect/unstable/rpc";
|
|
|
7
7
|
/**
|
|
8
8
|
* Options for EntityMachine.layer
|
|
9
9
|
*/
|
|
10
|
-
interface
|
|
10
|
+
interface EntityMachineBaseOptions<S> {
|
|
11
11
|
/**
|
|
12
12
|
* Initialize state from entity ID.
|
|
13
13
|
* Called once when entity is first activated.
|
|
@@ -38,6 +38,12 @@ interface EntityMachineOptions<S> {
|
|
|
38
38
|
*/
|
|
39
39
|
readonly persistence?: EntityPersistenceConfig;
|
|
40
40
|
}
|
|
41
|
+
type EntityMachineOptions<S, Input = void> = EntityMachineBaseOptions<S> & ([Input] extends [void] ? {
|
|
42
|
+
readonly input?: never;
|
|
43
|
+
} : {
|
|
44
|
+
/** Map the entity ID to the machine input. */
|
|
45
|
+
readonly input: (entityId: string) => Input;
|
|
46
|
+
});
|
|
41
47
|
/**
|
|
42
48
|
* Create an Entity layer that wires a machine to handle RPC calls.
|
|
43
49
|
*
|
|
@@ -59,7 +65,7 @@ declare const EntityMachine: {
|
|
|
59
65
|
readonly _tag: string;
|
|
60
66
|
}, E extends {
|
|
61
67
|
readonly _tag: string;
|
|
62
|
-
}, R, EntityType extends string, Rpcs extends Rpc.Any>(entity: Entity.Entity<EntityType, Rpcs>, machine: Machine<S, E, R, any, any>, options?: EntityMachineOptions<S>) => Layer.Layer<never, never, R>;
|
|
68
|
+
}, R, Input, Output, EntityType extends string, Rpcs extends Rpc.Any>(entity: Entity.Entity<EntityType, Rpcs>, machine: Machine<S, E, R, any, any, Input, Output>, ...optionsArgument: [Input] extends [void] ? [options?: EntityMachineOptions<S, Input>] : [options: EntityMachineOptions<S, Input>]) => Layer.Layer<never, never, R>;
|
|
63
69
|
};
|
|
64
70
|
//#endregion
|
|
65
|
-
export { EntityMachine, EntityMachineOptions };
|
|
71
|
+
export { EntityMachine, EntityMachineBaseOptions, EntityMachineOptions };
|
|
@@ -35,7 +35,8 @@ import { Entity } from "effect/unstable/cluster";
|
|
|
35
35
|
* })
|
|
36
36
|
* ```
|
|
37
37
|
*/
|
|
38
|
-
const EntityMachine = { layer: (entity, machine,
|
|
38
|
+
const EntityMachine = { layer: (entity, machine, ...optionsArgument) => {
|
|
39
|
+
const options = optionsArgument[0];
|
|
39
40
|
const persistence = options?.persistence;
|
|
40
41
|
const build = Effect.gen(function* () {
|
|
41
42
|
const entityId = yield* Effect.serviceOption(Entity.CurrentAddress).pipe(Effect.map((opt) => {
|
|
@@ -43,23 +44,22 @@ const EntityMachine = { layer: (entity, machine, options) => {
|
|
|
43
44
|
return "";
|
|
44
45
|
}));
|
|
45
46
|
const inspector = Option.getOrUndefined(yield* Effect.serviceOption(Inspector));
|
|
47
|
+
const machineInitial = machine._initial(options?.input?.(entityId));
|
|
46
48
|
const existingSystem = yield* Effect.serviceOption(ActorSystem);
|
|
47
49
|
let system;
|
|
48
50
|
if (Option.isSome(existingSystem)) system = existingSystem.value;
|
|
49
51
|
else system = yield* makeSystem();
|
|
50
|
-
const persistCtx = yield* hydratePersistence(persistence, entity, entityId, machine, options?.initializeState);
|
|
52
|
+
const persistCtx = yield* hydratePersistence(persistence, entity, entityId, machine, machineInitial, options?.initializeState);
|
|
51
53
|
let initialState = persistCtx.hydratedState;
|
|
52
54
|
if (initialState === void 0 && options?.initializeState !== void 0) initialState = options.initializeState(entityId);
|
|
53
|
-
let machineWithState = machine;
|
|
54
|
-
if (initialState !== void 0) machineWithState = machine._withInitial(initialState);
|
|
55
55
|
const versionRef = yield* Ref.make(persistCtx.initialVersion);
|
|
56
|
-
const computedInitial = initialState ??
|
|
56
|
+
const computedInitial = initialState ?? machineInitial;
|
|
57
57
|
const stateRef = yield* SubscriptionRef.make(computedInitial);
|
|
58
58
|
const stoppedRef = yield* Ref.make(false);
|
|
59
59
|
const eventQueue = yield* Queue.unbounded();
|
|
60
60
|
let hooks = void 0;
|
|
61
61
|
if (inspector !== void 0) hooks = makeInspectionHooks(entityId, inspector);
|
|
62
|
-
const runtime = yield* createRuntime(
|
|
62
|
+
const runtime = yield* createRuntime(machine, system, {
|
|
63
63
|
actorId: entityId,
|
|
64
64
|
hooks,
|
|
65
65
|
childIdPrefix: `${entityId}/`,
|
|
@@ -152,7 +152,7 @@ const noPersistence = {
|
|
|
152
152
|
initialVersion: 0
|
|
153
153
|
};
|
|
154
154
|
/** Load snapshot/journal and compute hydrated state. */
|
|
155
|
-
const hydratePersistence = (persistence, entityDef, entityId, machine, initializeState) => Effect.gen(function* () {
|
|
155
|
+
const hydratePersistence = (persistence, entityDef, entityId, machine, machineInitial, initializeState) => Effect.gen(function* () {
|
|
156
156
|
if (persistence === void 0) return noPersistence;
|
|
157
157
|
const adapter = yield* PersistenceAdapter;
|
|
158
158
|
const key = {
|
|
@@ -164,7 +164,7 @@ const hydratePersistence = (persistence, entityDef, entityId, machine, initializ
|
|
|
164
164
|
let baseState;
|
|
165
165
|
if (Option.isSome(maybeSnapshot)) baseState = maybeSnapshot.value.state;
|
|
166
166
|
else if (initializeState !== void 0) baseState = initializeState(entityId);
|
|
167
|
-
else baseState =
|
|
167
|
+
else baseState = machineInitial;
|
|
168
168
|
let snapshotVersion = 0;
|
|
169
169
|
if (Option.isSome(maybeSnapshot)) snapshotVersion = maybeSnapshot.value.version;
|
|
170
170
|
const events = yield* adapter.loadEvents(key, snapshotVersion);
|
package/dist/cluster/index.d.ts
CHANGED
|
@@ -2,5 +2,5 @@ import { EntityPersistenceConfig, PersistedEvent, PersistenceAdapter, Persistenc
|
|
|
2
2
|
import { makeInMemoryPersistenceAdapter } from "./adapters/in-memory.js";
|
|
3
3
|
import { EntityRpcs, ToEntityOptions, toEntity } from "./to-entity.js";
|
|
4
4
|
import { EntityActorRef, makeEntityActorRef } from "./entity-actor-ref.js";
|
|
5
|
-
import { EntityMachine, EntityMachineOptions } from "./entity-machine.js";
|
|
6
|
-
export { type EntityActorRef, EntityMachine, type EntityMachineOptions, type EntityPersistenceConfig, type EntityRpcs, type PersistedEvent, PersistenceAdapter, type PersistenceAdapterService as PersistenceAdapterInterface, type PersistenceKey, type Snapshot, type ToEntityOptions, makeEntityActorRef, makeInMemoryPersistenceAdapter, toEntity };
|
|
5
|
+
import { EntityMachine, EntityMachineBaseOptions, EntityMachineOptions } from "./entity-machine.js";
|
|
6
|
+
export { type EntityActorRef, EntityMachine, type EntityMachineBaseOptions, type EntityMachineOptions, type EntityPersistenceConfig, type EntityRpcs, type PersistedEvent, PersistenceAdapter, type PersistenceAdapterService as PersistenceAdapterInterface, type PersistenceKey, type Snapshot, type ToEntityOptions, makeEntityActorRef, makeInMemoryPersistenceAdapter, toEntity };
|