effect-machine 0.6.0 → 0.7.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/dist/actor.d.ts +2 -2
- package/dist/actor.js +12 -14
- package/dist/cluster/entity-machine.d.ts +2 -2
- package/dist/cluster/entity-machine.js +1 -1
- package/dist/cluster/to-entity.d.ts +5 -5
- package/dist/cluster/to-entity.js +2 -2
- package/dist/errors.d.ts +25 -40
- package/dist/errors.js +10 -10
- package/dist/inspection.d.ts +3 -3
- package/dist/inspection.js +2 -2
- package/dist/internal/brands.d.ts +3 -6
- package/dist/internal/inspection.js +5 -1
- package/dist/internal/transition.d.ts +2 -2
- package/dist/internal/transition.js +6 -6
- package/dist/internal/utils.js +5 -1
- package/dist/machine.d.ts +5 -5
- package/dist/machine.js +9 -5
- package/dist/persistence/adapter.d.ts +18 -21
- package/dist/persistence/adapter.js +4 -4
- package/dist/persistence/adapters/in-memory.js +4 -4
- package/dist/persistence/persistent-actor.js +9 -9
- package/dist/persistence/persistent-machine.d.ts +3 -3
- package/dist/schema.d.ts +4 -4
- package/dist/schema.js +2 -2
- package/dist/slot.d.ts +3 -3
- package/dist/slot.js +2 -2
- package/dist-v3/_virtual/_rolldown/runtime.js +18 -0
- package/dist-v3/actor.d.ts +291 -0
- package/dist-v3/actor.js +459 -0
- package/dist-v3/cluster/entity-machine.d.ts +90 -0
- package/dist-v3/cluster/entity-machine.js +80 -0
- package/dist-v3/cluster/index.d.ts +3 -0
- package/dist-v3/cluster/index.js +4 -0
- package/dist-v3/cluster/to-entity.d.ts +61 -0
- package/dist-v3/cluster/to-entity.js +53 -0
- package/dist-v3/errors.d.ts +27 -0
- package/dist-v3/errors.js +38 -0
- package/dist-v3/index.d.ts +13 -0
- package/dist-v3/index.js +14 -0
- package/dist-v3/inspection.d.ts +125 -0
- package/dist-v3/inspection.js +50 -0
- package/dist-v3/internal/brands.d.ts +40 -0
- package/dist-v3/internal/brands.js +0 -0
- package/dist-v3/internal/inspection.d.ts +11 -0
- package/dist-v3/internal/inspection.js +15 -0
- package/dist-v3/internal/transition.d.ts +160 -0
- package/dist-v3/internal/transition.js +238 -0
- package/dist-v3/internal/utils.d.ts +60 -0
- package/dist-v3/internal/utils.js +51 -0
- package/dist-v3/machine.d.ts +278 -0
- package/dist-v3/machine.js +317 -0
- package/dist-v3/persistence/adapter.d.ts +125 -0
- package/dist-v3/persistence/adapter.js +27 -0
- package/dist-v3/persistence/adapters/in-memory.d.ts +32 -0
- package/dist-v3/persistence/adapters/in-memory.js +176 -0
- package/dist-v3/persistence/index.d.ts +5 -0
- package/dist-v3/persistence/index.js +6 -0
- package/dist-v3/persistence/persistent-actor.d.ts +49 -0
- package/dist-v3/persistence/persistent-actor.js +367 -0
- package/dist-v3/persistence/persistent-machine.d.ts +105 -0
- package/dist-v3/persistence/persistent-machine.js +24 -0
- package/dist-v3/schema.d.ts +141 -0
- package/dist-v3/schema.js +165 -0
- package/dist-v3/slot.d.ts +130 -0
- package/dist-v3/slot.js +99 -0
- package/dist-v3/testing.d.ts +136 -0
- package/dist-v3/testing.js +138 -0
- package/package.json +29 -21
package/dist-v3/actor.js
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import { Inspector } from "./inspection.js";
|
|
2
|
+
import { INTERNAL_INIT_EVENT } from "./internal/utils.js";
|
|
3
|
+
import { DuplicateActorError } from "./errors.js";
|
|
4
|
+
import { isPersistentMachine } from "./persistence/persistent-machine.js";
|
|
5
|
+
import { processEventCore, resolveTransition, runSpawnEffects } from "./internal/transition.js";
|
|
6
|
+
import { emitWithTimestamp } from "./internal/inspection.js";
|
|
7
|
+
import { PersistenceAdapterTag, PersistenceError } from "./persistence/adapter.js";
|
|
8
|
+
import { createPersistentActor, restorePersistentActor } from "./persistence/persistent-actor.js";
|
|
9
|
+
import { Cause, Context, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, PubSub, Queue, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect";
|
|
10
|
+
|
|
11
|
+
//#region src-v3/actor.ts
|
|
12
|
+
/**
|
|
13
|
+
* Actor system: spawning, lifecycle, and event processing.
|
|
14
|
+
*
|
|
15
|
+
* Combines:
|
|
16
|
+
* - ActorRef interface (running actor handle)
|
|
17
|
+
* - ActorSystem service (spawn/stop/get actors)
|
|
18
|
+
* - Actor creation and event loop
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* ActorSystem service tag
|
|
22
|
+
*/
|
|
23
|
+
const ActorSystem = Context.GenericTag("@effect/machine/ActorSystem");
|
|
24
|
+
/**
|
|
25
|
+
* Notify all listeners of state change.
|
|
26
|
+
*/
|
|
27
|
+
const notifyListeners = (listeners, state) => {
|
|
28
|
+
for (const listener of listeners) try {
|
|
29
|
+
listener(state);
|
|
30
|
+
} catch {}
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Build core ActorRef methods shared between regular and persistent actors.
|
|
34
|
+
*/
|
|
35
|
+
const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listeners, stop, system, childrenMap) => {
|
|
36
|
+
const send = Effect.fn("effect-machine.actor.send")(function* (event) {
|
|
37
|
+
if (yield* Ref.get(stoppedRef)) return;
|
|
38
|
+
yield* Queue.offer(eventQueue, event);
|
|
39
|
+
});
|
|
40
|
+
const snapshot = SubscriptionRef.get(stateRef).pipe(Effect.withSpan("effect-machine.actor.snapshot"));
|
|
41
|
+
const matches = Effect.fn("effect-machine.actor.matches")(function* (tag) {
|
|
42
|
+
return (yield* SubscriptionRef.get(stateRef))._tag === tag;
|
|
43
|
+
});
|
|
44
|
+
const can = Effect.fn("effect-machine.actor.can")(function* (event) {
|
|
45
|
+
return resolveTransition(machine, yield* SubscriptionRef.get(stateRef), event) !== void 0;
|
|
46
|
+
});
|
|
47
|
+
const waitFor = Effect.fn("effect-machine.actor.waitFor")(function* (predicateOrState) {
|
|
48
|
+
const predicate = typeof predicateOrState === "function" && !("_tag" in predicateOrState) ? predicateOrState : (s) => s._tag === predicateOrState._tag;
|
|
49
|
+
const current = yield* SubscriptionRef.get(stateRef);
|
|
50
|
+
if (predicate(current)) return current;
|
|
51
|
+
const done = yield* Deferred.make();
|
|
52
|
+
const rt = yield* Effect.runtime();
|
|
53
|
+
const runFork = Runtime.runFork(rt);
|
|
54
|
+
const listener = (state) => {
|
|
55
|
+
if (predicate(state)) runFork(Deferred.succeed(done, state));
|
|
56
|
+
};
|
|
57
|
+
listeners.add(listener);
|
|
58
|
+
const afterSubscribe = yield* SubscriptionRef.get(stateRef);
|
|
59
|
+
if (predicate(afterSubscribe)) {
|
|
60
|
+
listeners.delete(listener);
|
|
61
|
+
return afterSubscribe;
|
|
62
|
+
}
|
|
63
|
+
const result = yield* Deferred.await(done);
|
|
64
|
+
listeners.delete(listener);
|
|
65
|
+
return result;
|
|
66
|
+
});
|
|
67
|
+
const awaitFinal = waitFor((state) => machine.finalStates.has(state._tag)).pipe(Effect.withSpan("effect-machine.actor.awaitFinal"));
|
|
68
|
+
const sendAndWait = Effect.fn("effect-machine.actor.sendAndWait")(function* (event, predicateOrState) {
|
|
69
|
+
yield* send(event);
|
|
70
|
+
if (predicateOrState !== void 0) return yield* waitFor(predicateOrState);
|
|
71
|
+
return yield* awaitFinal;
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
id,
|
|
75
|
+
send,
|
|
76
|
+
state: stateRef,
|
|
77
|
+
stop,
|
|
78
|
+
stopSync: () => Effect.runFork(stop),
|
|
79
|
+
snapshot,
|
|
80
|
+
snapshotSync: () => Effect.runSync(SubscriptionRef.get(stateRef)),
|
|
81
|
+
matches,
|
|
82
|
+
matchesSync: (tag) => Effect.runSync(SubscriptionRef.get(stateRef))._tag === tag,
|
|
83
|
+
can,
|
|
84
|
+
canSync: (event) => {
|
|
85
|
+
return resolveTransition(machine, Effect.runSync(SubscriptionRef.get(stateRef)), event) !== void 0;
|
|
86
|
+
},
|
|
87
|
+
changes: stateRef.changes,
|
|
88
|
+
waitFor,
|
|
89
|
+
awaitFinal,
|
|
90
|
+
sendAndWait,
|
|
91
|
+
sendSync: (event) => {
|
|
92
|
+
if (!Effect.runSync(Ref.get(stoppedRef))) Effect.runSync(Queue.offer(eventQueue, event));
|
|
93
|
+
},
|
|
94
|
+
subscribe: (fn) => {
|
|
95
|
+
listeners.add(fn);
|
|
96
|
+
return () => {
|
|
97
|
+
listeners.delete(fn);
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
system,
|
|
101
|
+
children: childrenMap
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Create and start an actor for a machine
|
|
106
|
+
*/
|
|
107
|
+
const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machine) {
|
|
108
|
+
yield* Effect.annotateCurrentSpan("effect_machine.actor.id", id);
|
|
109
|
+
const existingSystem = yield* Effect.serviceOption(ActorSystem);
|
|
110
|
+
let system;
|
|
111
|
+
let implicitSystemScope;
|
|
112
|
+
if (Option.isSome(existingSystem)) system = existingSystem.value;
|
|
113
|
+
else {
|
|
114
|
+
const scope = yield* Scope.make();
|
|
115
|
+
system = yield* make().pipe(Effect.provideService(Scope.Scope, scope));
|
|
116
|
+
implicitSystemScope = scope;
|
|
117
|
+
}
|
|
118
|
+
const inspectorValue = Option.getOrUndefined(yield* Effect.serviceOption(Inspector));
|
|
119
|
+
const eventQueue = yield* Queue.unbounded();
|
|
120
|
+
const stoppedRef = yield* Ref.make(false);
|
|
121
|
+
const childrenMap = /* @__PURE__ */ new Map();
|
|
122
|
+
const self = {
|
|
123
|
+
send: Effect.fn("effect-machine.actor.self.send")(function* (event) {
|
|
124
|
+
if (yield* Ref.get(stoppedRef)) return;
|
|
125
|
+
yield* Queue.offer(eventQueue, event);
|
|
126
|
+
}),
|
|
127
|
+
spawn: (childId, childMachine) => Effect.gen(function* () {
|
|
128
|
+
const child = yield* system.spawn(childId, childMachine).pipe(Effect.provideService(ActorSystem, system));
|
|
129
|
+
childrenMap.set(childId, child);
|
|
130
|
+
const maybeScope = yield* Effect.serviceOption(Scope.Scope);
|
|
131
|
+
if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, Effect.sync(() => {
|
|
132
|
+
childrenMap.delete(childId);
|
|
133
|
+
}));
|
|
134
|
+
return child;
|
|
135
|
+
})
|
|
136
|
+
};
|
|
137
|
+
yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", machine.initial._tag);
|
|
138
|
+
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
139
|
+
type: "@machine.spawn",
|
|
140
|
+
actorId: id,
|
|
141
|
+
initialState: machine.initial,
|
|
142
|
+
timestamp
|
|
143
|
+
}));
|
|
144
|
+
const stateRef = yield* SubscriptionRef.make(machine.initial);
|
|
145
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
146
|
+
const backgroundFibers = [];
|
|
147
|
+
const initEvent = { _tag: INTERNAL_INIT_EVENT };
|
|
148
|
+
const ctx = {
|
|
149
|
+
state: machine.initial,
|
|
150
|
+
event: initEvent,
|
|
151
|
+
self,
|
|
152
|
+
system
|
|
153
|
+
};
|
|
154
|
+
const { effects: effectSlots } = machine._slots;
|
|
155
|
+
for (const bg of machine.backgroundEffects) {
|
|
156
|
+
const fiber = yield* Effect.forkDaemon(bg.handler({
|
|
157
|
+
state: machine.initial,
|
|
158
|
+
event: initEvent,
|
|
159
|
+
self,
|
|
160
|
+
effects: effectSlots,
|
|
161
|
+
system
|
|
162
|
+
}).pipe(Effect.provideService(machine.Context, ctx)));
|
|
163
|
+
backgroundFibers.push(fiber);
|
|
164
|
+
}
|
|
165
|
+
const stateScopeRef = { current: yield* Scope.make() };
|
|
166
|
+
yield* runSpawnEffectsWithInspection(machine, machine.initial, initEvent, self, stateScopeRef.current, id, inspectorValue, system);
|
|
167
|
+
if (machine.finalStates.has(machine.initial._tag)) {
|
|
168
|
+
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
169
|
+
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
170
|
+
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
171
|
+
type: "@machine.stop",
|
|
172
|
+
actorId: id,
|
|
173
|
+
finalState: machine.initial,
|
|
174
|
+
timestamp
|
|
175
|
+
}));
|
|
176
|
+
yield* Ref.set(stoppedRef, true);
|
|
177
|
+
if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
|
|
178
|
+
return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, Ref.set(stoppedRef, true).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap);
|
|
179
|
+
}
|
|
180
|
+
const loopFiber = yield* Effect.forkDaemon(eventLoop(machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, id, inspectorValue, system));
|
|
181
|
+
return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, Effect.gen(function* () {
|
|
182
|
+
const finalState = yield* SubscriptionRef.get(stateRef);
|
|
183
|
+
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
184
|
+
type: "@machine.stop",
|
|
185
|
+
actorId: id,
|
|
186
|
+
finalState,
|
|
187
|
+
timestamp
|
|
188
|
+
}));
|
|
189
|
+
yield* Ref.set(stoppedRef, true);
|
|
190
|
+
yield* Fiber.interrupt(loopFiber);
|
|
191
|
+
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
192
|
+
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
193
|
+
if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
|
|
194
|
+
}).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap);
|
|
195
|
+
});
|
|
196
|
+
/**
|
|
197
|
+
* Main event loop for the actor
|
|
198
|
+
*/
|
|
199
|
+
const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, actorId, inspector, system) {
|
|
200
|
+
while (true) {
|
|
201
|
+
const event = yield* Queue.take(eventQueue);
|
|
202
|
+
const currentState = yield* SubscriptionRef.get(stateRef);
|
|
203
|
+
if (yield* Effect.withSpan("effect-machine.event.process", { attributes: {
|
|
204
|
+
"effect_machine.actor.id": actorId,
|
|
205
|
+
"effect_machine.state.current": currentState._tag,
|
|
206
|
+
"effect_machine.event.type": event._tag
|
|
207
|
+
} })(processEvent(machine, currentState, event, stateRef, self, listeners, stateScopeRef, actorId, inspector, system))) {
|
|
208
|
+
yield* Ref.set(stoppedRef, true);
|
|
209
|
+
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
210
|
+
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
/**
|
|
216
|
+
* Process a single event, returning true if the actor should stop.
|
|
217
|
+
* Wraps processEventCore with actor-specific concerns (inspection, listeners, state ref).
|
|
218
|
+
*/
|
|
219
|
+
const processEvent = Effect.fn("effect-machine.actor.processEvent")(function* (machine, currentState, event, stateRef, self, listeners, stateScopeRef, actorId, inspector, system) {
|
|
220
|
+
yield* emitWithTimestamp(inspector, (timestamp) => ({
|
|
221
|
+
type: "@machine.event",
|
|
222
|
+
actorId,
|
|
223
|
+
state: currentState,
|
|
224
|
+
event,
|
|
225
|
+
timestamp
|
|
226
|
+
}));
|
|
227
|
+
const result = yield* processEventCore(machine, currentState, event, self, stateScopeRef, system, inspector === void 0 ? void 0 : {
|
|
228
|
+
onSpawnEffect: (state) => emitWithTimestamp(inspector, (timestamp) => ({
|
|
229
|
+
type: "@machine.effect",
|
|
230
|
+
actorId,
|
|
231
|
+
effectType: "spawn",
|
|
232
|
+
state,
|
|
233
|
+
timestamp
|
|
234
|
+
})),
|
|
235
|
+
onTransition: (from, to, ev) => emitWithTimestamp(inspector, (timestamp) => ({
|
|
236
|
+
type: "@machine.transition",
|
|
237
|
+
actorId,
|
|
238
|
+
fromState: from,
|
|
239
|
+
toState: to,
|
|
240
|
+
event: ev,
|
|
241
|
+
timestamp
|
|
242
|
+
})),
|
|
243
|
+
onError: (info) => emitWithTimestamp(inspector, (timestamp) => ({
|
|
244
|
+
type: "@machine.error",
|
|
245
|
+
actorId,
|
|
246
|
+
phase: info.phase,
|
|
247
|
+
state: info.state,
|
|
248
|
+
event: info.event,
|
|
249
|
+
error: Cause.pretty(info.cause),
|
|
250
|
+
timestamp
|
|
251
|
+
}))
|
|
252
|
+
});
|
|
253
|
+
if (!result.transitioned) {
|
|
254
|
+
yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", false);
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", true);
|
|
258
|
+
yield* SubscriptionRef.set(stateRef, result.newState);
|
|
259
|
+
notifyListeners(listeners, result.newState);
|
|
260
|
+
if (result.lifecycleRan) {
|
|
261
|
+
yield* Effect.annotateCurrentSpan("effect_machine.state.from", result.previousState._tag);
|
|
262
|
+
yield* Effect.annotateCurrentSpan("effect_machine.state.to", result.newState._tag);
|
|
263
|
+
if (result.isFinal) {
|
|
264
|
+
yield* emitWithTimestamp(inspector, (timestamp) => ({
|
|
265
|
+
type: "@machine.stop",
|
|
266
|
+
actorId,
|
|
267
|
+
finalState: result.newState,
|
|
268
|
+
timestamp
|
|
269
|
+
}));
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return false;
|
|
274
|
+
});
|
|
275
|
+
/**
|
|
276
|
+
* Run spawn effects with actor-specific inspection and tracing.
|
|
277
|
+
* Wraps the core runSpawnEffects with inspection events and spans.
|
|
278
|
+
* @internal
|
|
279
|
+
*/
|
|
280
|
+
const runSpawnEffectsWithInspection = Effect.fn("effect-machine.actor.spawnEffects")(function* (machine, state, event, self, stateScope, actorId, inspector, system) {
|
|
281
|
+
yield* emitWithTimestamp(inspector, (timestamp) => ({
|
|
282
|
+
type: "@machine.effect",
|
|
283
|
+
actorId,
|
|
284
|
+
effectType: "spawn",
|
|
285
|
+
state,
|
|
286
|
+
timestamp
|
|
287
|
+
}));
|
|
288
|
+
yield* runSpawnEffects(machine, state, event, self, stateScope, system, inspector === void 0 ? void 0 : (info) => emitWithTimestamp(inspector, (timestamp) => ({
|
|
289
|
+
type: "@machine.error",
|
|
290
|
+
actorId,
|
|
291
|
+
phase: info.phase,
|
|
292
|
+
state: info.state,
|
|
293
|
+
event: info.event,
|
|
294
|
+
error: Cause.pretty(info.cause),
|
|
295
|
+
timestamp
|
|
296
|
+
})));
|
|
297
|
+
});
|
|
298
|
+
/** Notify all system event listeners (sync). */
|
|
299
|
+
const notifySystemListeners = (listeners, event) => {
|
|
300
|
+
for (const listener of listeners) try {
|
|
301
|
+
listener(event);
|
|
302
|
+
} catch {}
|
|
303
|
+
};
|
|
304
|
+
const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
305
|
+
const actorsMap = MutableHashMap.empty();
|
|
306
|
+
const withSpawnGate = (yield* Effect.makeSemaphore(1)).withPermits(1);
|
|
307
|
+
const eventPubSub = yield* PubSub.unbounded();
|
|
308
|
+
const eventListeners = /* @__PURE__ */ new Set();
|
|
309
|
+
const emitSystemEvent = (event) => Effect.sync(() => notifySystemListeners(eventListeners, event)).pipe(Effect.andThen(PubSub.publish(eventPubSub, event)), Effect.catchAllCause(() => Effect.void), Effect.asVoid);
|
|
310
|
+
yield* Effect.addFinalizer(() => {
|
|
311
|
+
const stops = [];
|
|
312
|
+
MutableHashMap.forEach(actorsMap, (actor) => {
|
|
313
|
+
stops.push(actor.stop);
|
|
314
|
+
});
|
|
315
|
+
return Effect.all(stops, { concurrency: "unbounded" }).pipe(Effect.andThen(PubSub.shutdown(eventPubSub)), Effect.asVoid);
|
|
316
|
+
});
|
|
317
|
+
/** Check for duplicate ID, register actor, attach scope cleanup if available */
|
|
318
|
+
const registerActor = Effect.fn("effect-machine.actorSystem.register")(function* (id, actor) {
|
|
319
|
+
if (MutableHashMap.has(actorsMap, id)) {
|
|
320
|
+
yield* actor.stop;
|
|
321
|
+
return yield* new DuplicateActorError({ actorId: id });
|
|
322
|
+
}
|
|
323
|
+
const actorRef = actor;
|
|
324
|
+
MutableHashMap.set(actorsMap, id, actorRef);
|
|
325
|
+
yield* emitSystemEvent({
|
|
326
|
+
_tag: "ActorSpawned",
|
|
327
|
+
id,
|
|
328
|
+
actor: actorRef
|
|
329
|
+
});
|
|
330
|
+
const maybeScope = yield* Effect.serviceOption(Scope.Scope);
|
|
331
|
+
if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, Effect.gen(function* () {
|
|
332
|
+
if (MutableHashMap.has(actorsMap, id)) {
|
|
333
|
+
yield* emitSystemEvent({
|
|
334
|
+
_tag: "ActorStopped",
|
|
335
|
+
id,
|
|
336
|
+
actor: actorRef
|
|
337
|
+
});
|
|
338
|
+
MutableHashMap.remove(actorsMap, id);
|
|
339
|
+
}
|
|
340
|
+
yield* actor.stop;
|
|
341
|
+
}));
|
|
342
|
+
return actor;
|
|
343
|
+
});
|
|
344
|
+
const spawnRegular = Effect.fn("effect-machine.actorSystem.spawnRegular")(function* (id, built) {
|
|
345
|
+
if (MutableHashMap.has(actorsMap, id)) return yield* new DuplicateActorError({ actorId: id });
|
|
346
|
+
return yield* registerActor(id, yield* createActor(id, built._inner));
|
|
347
|
+
});
|
|
348
|
+
const spawnPersistent = Effect.fn("effect-machine.actorSystem.spawnPersistent")(function* (id, persistentMachine) {
|
|
349
|
+
if (MutableHashMap.has(actorsMap, id)) return yield* new DuplicateActorError({ actorId: id });
|
|
350
|
+
const adapter = yield* PersistenceAdapterTag;
|
|
351
|
+
const maybeSnapshot = yield* adapter.loadSnapshot(id, persistentMachine.persistence.stateSchema);
|
|
352
|
+
return yield* registerActor(id, yield* createPersistentActor(id, persistentMachine, maybeSnapshot, yield* adapter.loadEvents(id, persistentMachine.persistence.eventSchema, Option.isSome(maybeSnapshot) ? maybeSnapshot.value.version : void 0)));
|
|
353
|
+
});
|
|
354
|
+
const spawnImpl = Effect.fn("effect-machine.actorSystem.spawn")(function* (id, machine) {
|
|
355
|
+
if (isPersistentMachine(machine)) return yield* spawnPersistent(id, machine);
|
|
356
|
+
return yield* spawnRegular(id, machine);
|
|
357
|
+
});
|
|
358
|
+
function spawn(id, machine) {
|
|
359
|
+
return withSpawnGate(spawnImpl(id, machine));
|
|
360
|
+
}
|
|
361
|
+
const restoreImpl = Effect.fn("effect-machine.actorSystem.restore")(function* (id, persistentMachine) {
|
|
362
|
+
const maybeActor = yield* restorePersistentActor(id, persistentMachine);
|
|
363
|
+
if (Option.isSome(maybeActor)) yield* registerActor(id, maybeActor.value);
|
|
364
|
+
return maybeActor;
|
|
365
|
+
});
|
|
366
|
+
const restore = (id, persistentMachine) => withSpawnGate(restoreImpl(id, persistentMachine));
|
|
367
|
+
const get = Effect.fn("effect-machine.actorSystem.get")(function* (id) {
|
|
368
|
+
return yield* Effect.sync(() => MutableHashMap.get(actorsMap, id));
|
|
369
|
+
});
|
|
370
|
+
const stop = Effect.fn("effect-machine.actorSystem.stop")(function* (id) {
|
|
371
|
+
const maybeActor = MutableHashMap.get(actorsMap, id);
|
|
372
|
+
if (Option.isNone(maybeActor)) return false;
|
|
373
|
+
const actor = maybeActor.value;
|
|
374
|
+
MutableHashMap.remove(actorsMap, id);
|
|
375
|
+
yield* emitSystemEvent({
|
|
376
|
+
_tag: "ActorStopped",
|
|
377
|
+
id,
|
|
378
|
+
actor
|
|
379
|
+
});
|
|
380
|
+
yield* actor.stop;
|
|
381
|
+
return true;
|
|
382
|
+
});
|
|
383
|
+
const listPersisted = Effect.fn("effect-machine.actorSystem.listPersisted")(function* () {
|
|
384
|
+
const adapter = yield* PersistenceAdapterTag;
|
|
385
|
+
if (adapter.listActors === void 0) return [];
|
|
386
|
+
return yield* adapter.listActors();
|
|
387
|
+
});
|
|
388
|
+
const restoreMany = Effect.fn("effect-machine.actorSystem.restoreMany")(function* (ids, persistentMachine) {
|
|
389
|
+
const restored = [];
|
|
390
|
+
const failed = [];
|
|
391
|
+
for (const id of ids) {
|
|
392
|
+
if (MutableHashMap.has(actorsMap, id)) continue;
|
|
393
|
+
const result = yield* Effect.either(restore(id, persistentMachine));
|
|
394
|
+
if (result._tag === "Left") failed.push({
|
|
395
|
+
id,
|
|
396
|
+
error: result.left
|
|
397
|
+
});
|
|
398
|
+
else if (Option.isSome(result.right)) restored.push(result.right.value);
|
|
399
|
+
else failed.push({
|
|
400
|
+
id,
|
|
401
|
+
error: new PersistenceError({
|
|
402
|
+
operation: "restore",
|
|
403
|
+
actorId: id,
|
|
404
|
+
message: "No persisted state found"
|
|
405
|
+
})
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
restored,
|
|
410
|
+
failed
|
|
411
|
+
};
|
|
412
|
+
});
|
|
413
|
+
const restoreAll = Effect.fn("effect-machine.actorSystem.restoreAll")(function* (persistentMachine, options) {
|
|
414
|
+
const adapter = yield* PersistenceAdapterTag;
|
|
415
|
+
if (adapter.listActors === void 0) return {
|
|
416
|
+
restored: [],
|
|
417
|
+
failed: []
|
|
418
|
+
};
|
|
419
|
+
const machineType = persistentMachine.persistence.machineType;
|
|
420
|
+
if (machineType === void 0) return yield* new PersistenceError({
|
|
421
|
+
operation: "restoreAll",
|
|
422
|
+
actorId: "*",
|
|
423
|
+
message: "restoreAll requires explicit machineType in persistence config"
|
|
424
|
+
});
|
|
425
|
+
let filtered = (yield* adapter.listActors()).filter((meta) => meta.machineType === machineType);
|
|
426
|
+
if (options?.filter !== void 0) filtered = filtered.filter(options.filter);
|
|
427
|
+
return yield* restoreMany(filtered.map((meta) => meta.id), persistentMachine);
|
|
428
|
+
});
|
|
429
|
+
return ActorSystem.of({
|
|
430
|
+
spawn,
|
|
431
|
+
restore,
|
|
432
|
+
get,
|
|
433
|
+
stop,
|
|
434
|
+
events: Stream.fromPubSub(eventPubSub),
|
|
435
|
+
get actors() {
|
|
436
|
+
const snapshot = /* @__PURE__ */ new Map();
|
|
437
|
+
MutableHashMap.forEach(actorsMap, (actor, id) => {
|
|
438
|
+
snapshot.set(id, actor);
|
|
439
|
+
});
|
|
440
|
+
return snapshot;
|
|
441
|
+
},
|
|
442
|
+
subscribe: (fn) => {
|
|
443
|
+
eventListeners.add(fn);
|
|
444
|
+
return () => {
|
|
445
|
+
eventListeners.delete(fn);
|
|
446
|
+
};
|
|
447
|
+
},
|
|
448
|
+
listPersisted,
|
|
449
|
+
restoreMany,
|
|
450
|
+
restoreAll
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
/**
|
|
454
|
+
* Default ActorSystem layer
|
|
455
|
+
*/
|
|
456
|
+
const Default = Layer.scoped(ActorSystem, make());
|
|
457
|
+
|
|
458
|
+
//#endregion
|
|
459
|
+
export { ActorSystem, Default, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { EffectsDef, GuardsDef } from "../slot.js";
|
|
2
|
+
import { ProcessEventHooks } from "../internal/transition.js";
|
|
3
|
+
import { Machine } from "../machine.js";
|
|
4
|
+
import "../actor.js";
|
|
5
|
+
import { Layer } from "effect";
|
|
6
|
+
import { Entity } from "@effect/cluster";
|
|
7
|
+
import { Rpc } from "@effect/rpc";
|
|
8
|
+
|
|
9
|
+
//#region src-v3/cluster/entity-machine.d.ts
|
|
10
|
+
/**
|
|
11
|
+
* Options for EntityMachine.layer
|
|
12
|
+
*/
|
|
13
|
+
interface EntityMachineOptions<S, E> {
|
|
14
|
+
/**
|
|
15
|
+
* Initialize state from entity ID.
|
|
16
|
+
* Called once when entity is first activated.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* EntityMachine.layer(OrderEntity, orderMachine, {
|
|
21
|
+
* initializeState: (entityId) => OrderState.Pending({ orderId: entityId }),
|
|
22
|
+
* })
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
readonly initializeState?: (entityId: string) => S;
|
|
26
|
+
/**
|
|
27
|
+
* Optional hooks for inspection/tracing.
|
|
28
|
+
* Called at specific points during event processing.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* EntityMachine.layer(OrderEntity, orderMachine, {
|
|
33
|
+
* hooks: {
|
|
34
|
+
* onTransition: (from, to, event) =>
|
|
35
|
+
* Effect.log(`Transition: ${from._tag} -> ${to._tag}`),
|
|
36
|
+
* onSpawnEffect: (state) =>
|
|
37
|
+
* Effect.log(`Running spawn effects for ${state._tag}`),
|
|
38
|
+
* onError: ({ phase, state }) =>
|
|
39
|
+
* Effect.log(`Defect in ${phase} at ${state._tag}`),
|
|
40
|
+
* },
|
|
41
|
+
* })
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
readonly hooks?: ProcessEventHooks<S, E>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Create an Entity layer that wires a machine to handle RPC calls.
|
|
48
|
+
*
|
|
49
|
+
* The layer:
|
|
50
|
+
* - Maintains state via Ref per entity instance
|
|
51
|
+
* - Resolves transitions using the indexed lookup
|
|
52
|
+
* - Evaluates guards in registration order
|
|
53
|
+
* - Runs lifecycle effects (onEnter/spawn)
|
|
54
|
+
* - Processes internal events from spawn effects
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* const OrderEntity = toEntity(orderMachine, {
|
|
59
|
+
* type: "Order",
|
|
60
|
+
* stateSchema: OrderState,
|
|
61
|
+
* eventSchema: OrderEvent,
|
|
62
|
+
* })
|
|
63
|
+
*
|
|
64
|
+
* const OrderEntityLayer = EntityMachine.layer(OrderEntity, orderMachine, {
|
|
65
|
+
* initializeState: (entityId) => OrderState.Pending({ orderId: entityId }),
|
|
66
|
+
* })
|
|
67
|
+
*
|
|
68
|
+
* // Use in cluster
|
|
69
|
+
* const program = Effect.gen(function* () {
|
|
70
|
+
* const client = yield* ShardingClient.client(OrderEntity)
|
|
71
|
+
* yield* client.Send("order-123", { event: OrderEvent.Ship({ trackingId: "abc" }) })
|
|
72
|
+
* })
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
declare const EntityMachine: {
|
|
76
|
+
/**
|
|
77
|
+
* Create a layer that wires a machine to an Entity.
|
|
78
|
+
*
|
|
79
|
+
* @param entity - Entity created via toEntity()
|
|
80
|
+
* @param machine - Machine with all effects provided
|
|
81
|
+
* @param options - Optional configuration (state initializer, inspection hooks)
|
|
82
|
+
*/
|
|
83
|
+
layer: <S extends {
|
|
84
|
+
readonly _tag: string;
|
|
85
|
+
}, E extends {
|
|
86
|
+
readonly _tag: string;
|
|
87
|
+
}, R, GD extends GuardsDef, EFD extends EffectsDef, EntityType extends string, Rpcs extends Rpc.Any>(entity: Entity.Entity<EntityType, Rpcs>, machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>, options?: EntityMachineOptions<S, E>) => Layer.Layer<never, never, R>;
|
|
88
|
+
};
|
|
89
|
+
//#endregion
|
|
90
|
+
export { EntityMachine, EntityMachineOptions };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { processEventCore, runSpawnEffects } from "../internal/transition.js";
|
|
2
|
+
import { ActorSystem } from "../actor.js";
|
|
3
|
+
import { Effect, Option, Queue, Ref, Scope } from "effect";
|
|
4
|
+
import { Entity } from "@effect/cluster";
|
|
5
|
+
|
|
6
|
+
//#region src-v3/cluster/entity-machine.ts
|
|
7
|
+
/**
|
|
8
|
+
* EntityMachine adapter - wires a machine to a cluster Entity layer.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Process a single event through the machine using shared core.
|
|
14
|
+
* Returns the new state after processing.
|
|
15
|
+
*/
|
|
16
|
+
const processEvent = Effect.fn("effect-machine.cluster.processEvent")(function* (machine, stateRef, event, self, stateScopeRef, system, hooks) {
|
|
17
|
+
const result = yield* processEventCore(machine, yield* Ref.get(stateRef), event, self, stateScopeRef, system, hooks);
|
|
18
|
+
if (result.transitioned) yield* Ref.set(stateRef, result.newState);
|
|
19
|
+
return result.newState;
|
|
20
|
+
});
|
|
21
|
+
/**
|
|
22
|
+
* Create an Entity layer that wires a machine to handle RPC calls.
|
|
23
|
+
*
|
|
24
|
+
* The layer:
|
|
25
|
+
* - Maintains state via Ref per entity instance
|
|
26
|
+
* - Resolves transitions using the indexed lookup
|
|
27
|
+
* - Evaluates guards in registration order
|
|
28
|
+
* - Runs lifecycle effects (onEnter/spawn)
|
|
29
|
+
* - Processes internal events from spawn effects
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* const OrderEntity = toEntity(orderMachine, {
|
|
34
|
+
* type: "Order",
|
|
35
|
+
* stateSchema: OrderState,
|
|
36
|
+
* eventSchema: OrderEvent,
|
|
37
|
+
* })
|
|
38
|
+
*
|
|
39
|
+
* const OrderEntityLayer = EntityMachine.layer(OrderEntity, orderMachine, {
|
|
40
|
+
* initializeState: (entityId) => OrderState.Pending({ orderId: entityId }),
|
|
41
|
+
* })
|
|
42
|
+
*
|
|
43
|
+
* // Use in cluster
|
|
44
|
+
* const program = Effect.gen(function* () {
|
|
45
|
+
* const client = yield* ShardingClient.client(OrderEntity)
|
|
46
|
+
* yield* client.Send("order-123", { event: OrderEvent.Ship({ trackingId: "abc" }) })
|
|
47
|
+
* })
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
const EntityMachine = { layer: (entity, machine, options) => {
|
|
51
|
+
const layer = Effect.fn("effect-machine.cluster.layer")(function* () {
|
|
52
|
+
const entityId = yield* Effect.serviceOption(Entity.CurrentAddress).pipe(Effect.map((opt) => opt._tag === "Some" ? opt.value.entityId : ""));
|
|
53
|
+
const initialState = options?.initializeState !== void 0 ? options.initializeState(entityId) : machine.initial;
|
|
54
|
+
const existingSystem = yield* Effect.serviceOption(ActorSystem);
|
|
55
|
+
if (Option.isNone(existingSystem)) return yield* Effect.die("EntityMachine requires ActorSystem in context");
|
|
56
|
+
const system = existingSystem.value;
|
|
57
|
+
const internalQueue = yield* Queue.unbounded();
|
|
58
|
+
const self = {
|
|
59
|
+
send: Effect.fn("effect-machine.cluster.self.send")(function* (event) {
|
|
60
|
+
yield* Queue.offer(internalQueue, event);
|
|
61
|
+
}),
|
|
62
|
+
spawn: (childId, childMachine) => system.spawn(childId, childMachine).pipe(Effect.provideService(ActorSystem, system))
|
|
63
|
+
};
|
|
64
|
+
const stateRef = yield* Ref.make(initialState);
|
|
65
|
+
const stateScopeRef = { current: yield* Scope.make() };
|
|
66
|
+
yield* runSpawnEffects(machine, initialState, { _tag: "$init" }, self, stateScopeRef.current, system, options?.hooks?.onError);
|
|
67
|
+
const runInternalEvent = Effect.fn("effect-machine.cluster.internalEvent")(function* () {
|
|
68
|
+
yield* processEvent(machine, stateRef, yield* Queue.take(internalQueue), self, stateScopeRef, system, options?.hooks);
|
|
69
|
+
});
|
|
70
|
+
yield* Effect.forkScoped(Effect.forever(runInternalEvent()));
|
|
71
|
+
return entity.of({
|
|
72
|
+
Send: (envelope) => processEvent(machine, stateRef, envelope.payload.event, self, stateScopeRef, system, options?.hooks),
|
|
73
|
+
GetState: () => Ref.get(stateRef)
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
return entity.toLayer(layer());
|
|
77
|
+
} };
|
|
78
|
+
|
|
79
|
+
//#endregion
|
|
80
|
+
export { EntityMachine };
|