effect-machine 0.4.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.
Files changed (70) hide show
  1. package/README.md +36 -0
  2. package/dist/actor.d.ts +40 -4
  3. package/dist/actor.js +89 -34
  4. package/dist/cluster/entity-machine.d.ts +2 -2
  5. package/dist/cluster/entity-machine.js +1 -1
  6. package/dist/cluster/to-entity.d.ts +5 -5
  7. package/dist/cluster/to-entity.js +2 -2
  8. package/dist/errors.d.ts +25 -40
  9. package/dist/errors.js +10 -10
  10. package/dist/index.d.ts +2 -2
  11. package/dist/inspection.d.ts +3 -3
  12. package/dist/inspection.js +2 -2
  13. package/dist/internal/brands.d.ts +3 -6
  14. package/dist/internal/inspection.js +5 -1
  15. package/dist/internal/transition.d.ts +2 -2
  16. package/dist/internal/transition.js +6 -6
  17. package/dist/internal/utils.js +11 -2
  18. package/dist/machine.d.ts +5 -5
  19. package/dist/machine.js +9 -5
  20. package/dist/persistence/adapter.d.ts +18 -21
  21. package/dist/persistence/adapter.js +4 -4
  22. package/dist/persistence/adapters/in-memory.js +4 -4
  23. package/dist/persistence/persistent-actor.js +23 -14
  24. package/dist/persistence/persistent-machine.d.ts +3 -3
  25. package/dist/schema.d.ts +4 -4
  26. package/dist/schema.js +2 -2
  27. package/dist/slot.d.ts +3 -3
  28. package/dist/slot.js +2 -2
  29. package/dist-v3/_virtual/_rolldown/runtime.js +18 -0
  30. package/dist-v3/actor.d.ts +291 -0
  31. package/dist-v3/actor.js +459 -0
  32. package/dist-v3/cluster/entity-machine.d.ts +90 -0
  33. package/dist-v3/cluster/entity-machine.js +80 -0
  34. package/dist-v3/cluster/index.d.ts +3 -0
  35. package/dist-v3/cluster/index.js +4 -0
  36. package/dist-v3/cluster/to-entity.d.ts +61 -0
  37. package/dist-v3/cluster/to-entity.js +53 -0
  38. package/dist-v3/errors.d.ts +27 -0
  39. package/dist-v3/errors.js +38 -0
  40. package/dist-v3/index.d.ts +13 -0
  41. package/dist-v3/index.js +14 -0
  42. package/dist-v3/inspection.d.ts +125 -0
  43. package/dist-v3/inspection.js +50 -0
  44. package/dist-v3/internal/brands.d.ts +40 -0
  45. package/dist-v3/internal/brands.js +0 -0
  46. package/dist-v3/internal/inspection.d.ts +11 -0
  47. package/dist-v3/internal/inspection.js +15 -0
  48. package/dist-v3/internal/transition.d.ts +160 -0
  49. package/dist-v3/internal/transition.js +238 -0
  50. package/dist-v3/internal/utils.d.ts +60 -0
  51. package/dist-v3/internal/utils.js +51 -0
  52. package/dist-v3/machine.d.ts +278 -0
  53. package/dist-v3/machine.js +317 -0
  54. package/dist-v3/persistence/adapter.d.ts +125 -0
  55. package/dist-v3/persistence/adapter.js +27 -0
  56. package/dist-v3/persistence/adapters/in-memory.d.ts +32 -0
  57. package/dist-v3/persistence/adapters/in-memory.js +176 -0
  58. package/dist-v3/persistence/index.d.ts +5 -0
  59. package/dist-v3/persistence/index.js +6 -0
  60. package/dist-v3/persistence/persistent-actor.d.ts +49 -0
  61. package/dist-v3/persistence/persistent-actor.js +367 -0
  62. package/dist-v3/persistence/persistent-machine.d.ts +105 -0
  63. package/dist-v3/persistence/persistent-machine.js +24 -0
  64. package/dist-v3/schema.d.ts +141 -0
  65. package/dist-v3/schema.js +165 -0
  66. package/dist-v3/slot.d.ts +130 -0
  67. package/dist-v3/slot.js +99 -0
  68. package/dist-v3/testing.d.ts +136 -0
  69. package/dist-v3/testing.js +138 -0
  70. package/package.json +29 -21
@@ -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 };
@@ -0,0 +1,3 @@
1
+ import { EntityMachine, EntityMachineOptions } from "./entity-machine.js";
2
+ import { ToEntityOptions, toEntity } from "./to-entity.js";
3
+ export { EntityMachine, type EntityMachineOptions, type ToEntityOptions, toEntity };
@@ -0,0 +1,4 @@
1
+ import { EntityMachine } from "./entity-machine.js";
2
+ import { toEntity } from "./to-entity.js";
3
+
4
+ export { EntityMachine, toEntity };