effect-machine 0.17.0 → 0.18.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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -54
  3. package/dist/actor.d.ts +8 -45
  4. package/dist/actor.js +157 -231
  5. package/dist/cluster/adapters/in-memory.d.ts +0 -1
  6. package/dist/cluster/adapters/in-memory.js +10 -5
  7. package/dist/cluster/entity-actor-ref.d.ts +2 -3
  8. package/dist/cluster/entity-actor-ref.js +14 -17
  9. package/dist/cluster/entity-machine.d.ts +2 -8
  10. package/dist/cluster/entity-machine.js +57 -34
  11. package/dist/cluster/index.js +2 -2
  12. package/dist/cluster/persistence.d.ts +0 -1
  13. package/dist/cluster/to-entity.d.ts +19 -21
  14. package/dist/cluster/to-entity.js +18 -19
  15. package/dist/errors.d.ts +11 -37
  16. package/dist/errors.js +12 -30
  17. package/dist/index.d.ts +4 -5
  18. package/dist/index.js +4 -5
  19. package/dist/inspection.d.ts +0 -1
  20. package/dist/inspection.js +24 -13
  21. package/dist/internal/brands.d.ts +0 -1
  22. package/dist/internal/event-advancement.d.ts +50 -0
  23. package/dist/internal/event-advancement.js +79 -0
  24. package/dist/internal/inspection.d.ts +5 -9
  25. package/dist/internal/inspection.js +31 -10
  26. package/dist/internal/machine-definition.d.ts +16 -0
  27. package/dist/internal/runtime.d.ts +1 -161
  28. package/dist/internal/runtime.js +191 -150
  29. package/dist/internal/transition.d.ts +4 -135
  30. package/dist/internal/transition.js +82 -157
  31. package/dist/internal/utils.d.ts +3 -40
  32. package/dist/internal/utils.js +2 -2
  33. package/dist/machine.d.ts +47 -137
  34. package/dist/machine.js +147 -215
  35. package/dist/schema.d.ts +42 -21
  36. package/dist/schema.js +28 -18
  37. package/dist/supervision.d.ts +1 -24
  38. package/dist/supervision.js +2 -3
  39. package/dist/testing.d.ts +15 -29
  40. package/dist/testing.js +76 -95
  41. package/package.json +19 -38
  42. package/dist/slot.d.ts +0 -159
  43. package/dist/slot.js +0 -165
  44. package/v3/dist/_virtual/_rolldown/runtime.js +0 -13
  45. package/v3/dist/actor.d.ts +0 -250
  46. package/v3/dist/actor.js +0 -577
  47. package/v3/dist/cluster/adapters/in-memory.d.ts +0 -15
  48. package/v3/dist/cluster/adapters/in-memory.js +0 -62
  49. package/v3/dist/cluster/entity-actor-ref.d.ts +0 -49
  50. package/v3/dist/cluster/entity-actor-ref.js +0 -19
  51. package/v3/dist/cluster/entity-machine.d.ts +0 -74
  52. package/v3/dist/cluster/entity-machine.js +0 -166
  53. package/v3/dist/cluster/index.d.ts +0 -6
  54. package/v3/dist/cluster/index.js +0 -6
  55. package/v3/dist/cluster/persistence.d.ts +0 -48
  56. package/v3/dist/cluster/persistence.js +0 -14
  57. package/v3/dist/cluster/to-entity.d.ts +0 -69
  58. package/v3/dist/cluster/to-entity.js +0 -59
  59. package/v3/dist/errors.d.ts +0 -95
  60. package/v3/dist/errors.js +0 -54
  61. package/v3/dist/index.d.ts +0 -11
  62. package/v3/dist/index.js +0 -9
  63. package/v3/dist/inspection.d.ts +0 -151
  64. package/v3/dist/inspection.js +0 -128
  65. package/v3/dist/internal/brands.d.ts +0 -50
  66. package/v3/dist/internal/inspection.d.ts +0 -11
  67. package/v3/dist/internal/inspection.js +0 -20
  68. package/v3/dist/internal/runtime.d.ts +0 -161
  69. package/v3/dist/internal/runtime.js +0 -360
  70. package/v3/dist/internal/transition.d.ts +0 -190
  71. package/v3/dist/internal/transition.js +0 -278
  72. package/v3/dist/internal/utils.d.ts +0 -101
  73. package/v3/dist/internal/utils.js +0 -75
  74. package/v3/dist/machine.d.ts +0 -398
  75. package/v3/dist/machine.js +0 -487
  76. package/v3/dist/schema.d.ts +0 -174
  77. package/v3/dist/schema.js +0 -206
  78. package/v3/dist/slot.d.ts +0 -158
  79. package/v3/dist/slot.js +0 -165
  80. package/v3/dist/supervision.d.ts +0 -97
  81. package/v3/dist/supervision.js +0 -42
  82. package/v3/dist/testing.d.ts +0 -151
  83. package/v3/dist/testing.js +0 -189
  84. /package/{v3/dist/internal/brands.js → dist/internal/machine-definition.js} +0 -0
package/v3/dist/actor.js DELETED
@@ -1,577 +0,0 @@
1
- import { ActorStoppedError, DuplicateActorError } from "./errors.js";
2
- import { processEventCore, resolveTransition, runSpawnEffects } from "./internal/transition.js";
3
- import { emitWithTimestamp } from "./internal/inspection.js";
4
- import { Inspector } from "./inspection.js";
5
- import { materializeMachine } from "./machine.js";
6
- import { createRuntime } from "./internal/runtime.js";
7
- import { Cause, Context, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, PubSub, Queue, Ref, Schedule, Scope, Stream, SubscriptionRef } from "effect";
8
- //#region src/actor.ts
9
- /**
10
- * Actor system: spawning, lifecycle, and event processing.
11
- *
12
- * Combines:
13
- * - ActorRef interface (running actor handle)
14
- * - ActorSystem service (spawn/stop/get actors)
15
- * - Actor creation and event loop
16
- */
17
- /**
18
- * ActorSystem service tag
19
- */
20
- const ActorSystem = Context.GenericTag("@effect/machine/ActorSystem");
21
- /**
22
- * Explicit scope for actor lifecycle management.
23
- *
24
- * When present in context, actors attach cleanup finalizers to this scope.
25
- * This replaces ambient `Scope.Scope` detection which caused bugs where
26
- * unrelated scopes would tear down actors unexpectedly.
27
- *
28
- * Provide via `Machine.scoped` or `Effect.provideService(ActorScope, scope)`.
29
- */
30
- const ActorScope = Context.GenericTag("@effect/machine/ActorScope");
31
- /**
32
- * Notify all listeners of state change.
33
- */
34
- const notifyListeners = (listeners, state) => {
35
- for (const listener of listeners) try {
36
- listener(state);
37
- } catch {}
38
- };
39
- /**
40
- * Build core ActorRef methods.
41
- */
42
- const buildActorRefCore = (id, machine, stateRef, eventQueueRef, stoppedRef, listeners, stop, start, system, childrenMap, pendingReplies, transitionsPubSub, exitDeferred) => {
43
- const send = Effect.fn("effect-machine.actor.send")(function* (event) {
44
- if (yield* Ref.get(stoppedRef)) return;
45
- const q = yield* Ref.get(eventQueueRef);
46
- yield* Queue.offer(q, {
47
- _tag: "send",
48
- event
49
- });
50
- });
51
- const call = Effect.fn("effect-machine.actor.call")(function* (event) {
52
- if (yield* Ref.get(stoppedRef)) {
53
- yield* Effect.logWarning("effect-machine.actor.call.stopped").pipe(Effect.annotateLogs({
54
- actorId: id,
55
- eventTag: event._tag
56
- }));
57
- const currentState = yield* SubscriptionRef.get(stateRef);
58
- return {
59
- newState: currentState,
60
- previousState: currentState,
61
- transitioned: false,
62
- hasReply: false,
63
- deferReply: false,
64
- reply: void 0,
65
- postponed: false,
66
- lifecycleRan: false,
67
- isFinal: machine.finalStates.has(currentState._tag)
68
- };
69
- }
70
- const reply = yield* Deferred.make();
71
- pendingReplies.add(reply);
72
- const q = yield* Ref.get(eventQueueRef);
73
- yield* Queue.offer(q, {
74
- _tag: "call",
75
- event,
76
- reply
77
- });
78
- return yield* Deferred.await(reply).pipe(Effect.ensuring(Effect.sync(() => pendingReplies.delete(reply))), Effect.catchTag("ActorStoppedError", () => SubscriptionRef.get(stateRef).pipe(Effect.map((currentState) => ({
79
- newState: currentState,
80
- previousState: currentState,
81
- transitioned: false,
82
- hasReply: false,
83
- deferReply: false,
84
- reply: void 0,
85
- postponed: false,
86
- lifecycleRan: false,
87
- isFinal: machine.finalStates.has(currentState._tag)
88
- })))));
89
- });
90
- const ask = Effect.fn("effect-machine.actor.ask")(function* (event) {
91
- if (yield* Ref.get(stoppedRef)) return yield* new ActorStoppedError({ actorId: id });
92
- const reply = yield* Deferred.make();
93
- pendingReplies.add(reply);
94
- const q = yield* Ref.get(eventQueueRef);
95
- yield* Queue.offer(q, {
96
- _tag: "ask",
97
- event,
98
- reply
99
- });
100
- return yield* Deferred.await(reply).pipe(Effect.ensuring(Effect.sync(() => pendingReplies.delete(reply))));
101
- });
102
- const snapshot = SubscriptionRef.get(stateRef).pipe(Effect.withSpan("effect-machine.actor.snapshot"));
103
- const matches = Effect.fn("effect-machine.actor.matches")(function* (tag) {
104
- return (yield* SubscriptionRef.get(stateRef))._tag === tag;
105
- });
106
- const can = Effect.fn("effect-machine.actor.can")(function* (event) {
107
- return resolveTransition(machine, yield* SubscriptionRef.get(stateRef), event) !== void 0;
108
- });
109
- const waitFor = Effect.fn("effect-machine.actor.waitFor")(function* (predicateOrState) {
110
- const predicate = typeof predicateOrState === "function" && !("_tag" in predicateOrState) ? predicateOrState : (s) => s._tag === predicateOrState._tag;
111
- const current = yield* SubscriptionRef.get(stateRef);
112
- if (predicate(current)) return current;
113
- const done = yield* Deferred.make();
114
- const listener = (state) => {
115
- if (predicate(state)) Effect.runFork(Deferred.succeed(done, state));
116
- };
117
- listeners.add(listener);
118
- const afterSubscribe = yield* SubscriptionRef.get(stateRef);
119
- if (predicate(afterSubscribe)) {
120
- listeners.delete(listener);
121
- return afterSubscribe;
122
- }
123
- const result = yield* Deferred.await(done);
124
- listeners.delete(listener);
125
- return result;
126
- });
127
- const awaitFinal = waitFor((state) => machine.finalStates.has(state._tag)).pipe(Effect.withSpan("effect-machine.actor.awaitFinal"));
128
- const sendAndWait = Effect.fn("effect-machine.actor.sendAndWait")(function* (event, predicateOrState) {
129
- yield* send(event);
130
- if (predicateOrState !== void 0) return yield* waitFor(predicateOrState);
131
- return yield* awaitFinal;
132
- });
133
- return {
134
- id,
135
- send,
136
- cast: send,
137
- call,
138
- ask,
139
- state: stateRef,
140
- stop,
141
- start,
142
- snapshot,
143
- matches,
144
- can,
145
- changes: stateRef.changes,
146
- transitions: transitionsPubSub !== void 0 ? Stream.fromPubSub(transitionsPubSub) : Stream.empty,
147
- waitFor,
148
- awaitFinal,
149
- sendAndWait,
150
- subscribe: (fn) => {
151
- listeners.add(fn);
152
- return () => {
153
- listeners.delete(fn);
154
- };
155
- },
156
- awaitExit: Deferred.await(exitDeferred),
157
- watch: (other) => other.awaitExit,
158
- drain: Effect.gen(function* () {
159
- if (yield* Ref.get(stoppedRef)) return;
160
- const q = yield* Ref.get(eventQueueRef);
161
- const done = yield* Deferred.make();
162
- yield* Queue.offer(q, {
163
- _tag: "drain",
164
- done
165
- });
166
- yield* Deferred.await(done);
167
- }).pipe(Effect.asVoid),
168
- sync: {
169
- send: (event) => {
170
- if (!Effect.runSync(Ref.get(stoppedRef))) {
171
- const q = Effect.runSync(Ref.get(eventQueueRef));
172
- Effect.runSync(Queue.offer(q, {
173
- _tag: "send",
174
- event
175
- }));
176
- }
177
- },
178
- stop: () => Effect.runFork(stop),
179
- snapshot: () => Effect.runSync(SubscriptionRef.get(stateRef)),
180
- matches: (tag) => Effect.runSync(SubscriptionRef.get(stateRef))._tag === tag,
181
- can: (event) => {
182
- return resolveTransition(machine, Effect.runSync(SubscriptionRef.get(stateRef)), event) !== void 0;
183
- }
184
- },
185
- system,
186
- children: childrenMap
187
- };
188
- };
189
- /** Build ProcessEventHooks from an inspector */
190
- const buildInspectionHooks = (actorId, inspector) => ({
191
- onSpawnEffect: (state) => emitWithTimestamp(inspector, (timestamp) => ({
192
- type: "@machine.effect",
193
- actorId,
194
- effectType: "spawn",
195
- state,
196
- timestamp
197
- })),
198
- onTransition: (from, to, ev) => emitWithTimestamp(inspector, (timestamp) => ({
199
- type: "@machine.transition",
200
- actorId,
201
- fromState: from,
202
- toState: to,
203
- event: ev,
204
- timestamp
205
- })),
206
- onError: (info) => emitWithTimestamp(inspector, (timestamp) => ({
207
- type: "@machine.error",
208
- actorId,
209
- phase: info.phase,
210
- state: info.state,
211
- event: info.event,
212
- error: Cause.pretty(info.cause),
213
- timestamp
214
- }))
215
- });
216
- /**
217
- * Resolve actor system from context, creating an implicit one if none exists.
218
- * @internal
219
- */
220
- const resolveActorSystem = Effect.fn("effect-machine.resolveActorSystem")(function* () {
221
- const existingSystem = yield* Effect.serviceOption(ActorSystem);
222
- if (Option.isSome(existingSystem)) return {
223
- system: existingSystem.value,
224
- implicitSystemScope: void 0
225
- };
226
- const scope = yield* Scope.make();
227
- return {
228
- system: yield* make().pipe(Effect.provideService(Scope.Scope, scope)),
229
- implicitSystemScope: scope
230
- };
231
- });
232
- /**
233
- * Run the supervision loop for a supervised actor.
234
- * Observes exit deferred, applies restart policy, resets cell resources on restart.
235
- * @internal
236
- */
237
- const runSupervisionLoop = (params) => Effect.gen(function* () {
238
- const driver = yield* Schedule.driver(params.supervision.schedule);
239
- let generation = 0;
240
- while (true) {
241
- const currentRuntime = params.runtimeRef.current;
242
- if (currentRuntime === void 0) return;
243
- const generationExit = yield* Deferred.await(currentRuntime.exitDeferred);
244
- if (generationExit._tag !== "Defect") {
245
- yield* Deferred.succeed(params.terminalExitDeferred, generationExit);
246
- return;
247
- }
248
- if (params.supervision.shouldRestart !== void 0 && !params.supervision.shouldRestart(generationExit)) {
249
- yield* Deferred.succeed(params.terminalExitDeferred, generationExit);
250
- return;
251
- }
252
- if ((yield* driver.next(generationExit).pipe(Effect.exit))._tag === "Failure") {
253
- yield* Deferred.succeed(params.terminalExitDeferred, generationExit);
254
- return;
255
- }
256
- let restartState = params.machine.initial;
257
- if (params.lifecycle?.recovery !== void 0) {
258
- const resolved = yield* params.lifecycle.recovery.resolve({
259
- actorId: params.id,
260
- generation: generation + 1,
261
- machineInitial: params.machine.initial
262
- });
263
- if (Option.isSome(resolved)) restartState = resolved.value;
264
- }
265
- yield* settlePendingReplies(params.pendingReplies, params.id);
266
- const freshQueue = yield* Queue.unbounded();
267
- yield* Ref.set(params.eventQueueRef, freshQueue);
268
- yield* SubscriptionRef.set(params.stateRef, restartState);
269
- yield* Ref.set(params.stoppedRef, false);
270
- params.childrenMap.clear();
271
- const machineForRestart = restartState !== params.machine.initial ? Object.create(params.machine, { initial: {
272
- value: restartState,
273
- enumerable: true
274
- } }) : params.machine;
275
- const newRuntime = yield* params.spawnGeneration(machineForRestart);
276
- params.runtimeRef.current = newRuntime;
277
- yield* newRuntime.start;
278
- generation++;
279
- if (params.onRestart !== void 0) yield* params.onRestart(generation, generationExit);
280
- notifyListeners(params.listeners, restartState);
281
- }
282
- });
283
- /**
284
- * Create and start an actor for a machine.
285
- * Delegates to the shared runtime kernel with actor-specific lifecycle hooks.
286
- */
287
- const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machine, options) {
288
- const lifecycle = options?.lifecycle;
289
- const initial = options?.initialState ?? machine.initial;
290
- yield* Effect.annotateCurrentSpan("effect_machine.actor.id", id);
291
- yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", initial._tag);
292
- const { system, implicitSystemScope } = yield* resolveActorSystem();
293
- const inspectorValue = Option.getOrUndefined(yield* Effect.serviceOption(Inspector));
294
- const childrenMap = /* @__PURE__ */ new Map();
295
- const pendingReplies = /* @__PURE__ */ new Set();
296
- const listeners = /* @__PURE__ */ new Set();
297
- const transitionsPubSub = yield* PubSub.unbounded();
298
- const hooks = inspectorValue !== void 0 ? buildInspectionHooks(id, inspectorValue) : void 0;
299
- const machineWithState = initial !== machine.initial ? Object.create(machine, { initial: {
300
- value: initial,
301
- enumerable: true
302
- } }) : machine;
303
- const stateRef = yield* SubscriptionRef.make(initial);
304
- const stoppedRef = yield* Ref.make(false);
305
- const initialQueue = yield* Queue.unbounded();
306
- const eventQueueRef = yield* Ref.make(initialQueue);
307
- const terminalExitDeferred = yield* Deferred.make();
308
- let stopEmitted = false;
309
- let generation = 0;
310
- const runtimeRef = { current: void 0 };
311
- /** Build lifecycle hooks for a generation */
312
- const buildLifecycle = () => {
313
- stopEmitted = false;
314
- return {
315
- onEvent: inspectorValue !== void 0 ? (state, event) => emitWithTimestamp(inspectorValue, (timestamp) => ({
316
- type: "@machine.event",
317
- actorId: id,
318
- state,
319
- event,
320
- timestamp
321
- })) : void 0,
322
- onStateChange: (result, event) => Effect.gen(function* () {
323
- notifyListeners(listeners, result.newState);
324
- if (lifecycle?.durability !== void 0 && result.transitioned) {
325
- const durability = lifecycle.durability;
326
- if (durability.shouldSave === void 0 || durability.shouldSave(result.newState, result.previousState)) yield* durability.save({
327
- actorId: id,
328
- generation,
329
- previousState: result.previousState,
330
- nextState: result.newState,
331
- event
332
- });
333
- }
334
- yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", true);
335
- if (result.lifecycleRan) {
336
- yield* Effect.annotateCurrentSpan("effect_machine.state.from", result.previousState._tag);
337
- yield* Effect.annotateCurrentSpan("effect_machine.state.to", result.newState._tag);
338
- }
339
- }),
340
- onProcessed: (result, event) => result.transitioned ? PubSub.publish(transitionsPubSub, {
341
- fromState: result.previousState,
342
- toState: result.newState,
343
- event
344
- }).pipe(Effect.asVoid) : Effect.void,
345
- onFinal: inspectorValue !== void 0 ? (state) => Effect.gen(function* () {
346
- stopEmitted = true;
347
- yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
348
- type: "@machine.stop",
349
- actorId: id,
350
- finalState: state,
351
- timestamp
352
- }));
353
- }) : void 0,
354
- onShutdown: () => Effect.gen(function* () {
355
- if (!stopEmitted) {
356
- const finalState = yield* SubscriptionRef.get(stateRef);
357
- yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
358
- type: "@machine.stop",
359
- actorId: id,
360
- finalState,
361
- timestamp
362
- }));
363
- }
364
- yield* settlePendingReplies(pendingReplies, id);
365
- }),
366
- onInitialSpawnEffects: inspectorValue !== void 0 ? (state) => emitWithTimestamp(inspectorValue, (timestamp) => ({
367
- type: "@machine.effect",
368
- actorId: id,
369
- effectType: "spawn",
370
- state,
371
- timestamp
372
- })) : void 0
373
- };
374
- };
375
- /** Create a single runtime generation. machineForGen is machineWithState for initial, machine for restarts. */
376
- const spawnGeneration = (machineForGen) => Ref.get(eventQueueRef).pipe(Effect.flatMap((currentQueue) => createRuntime(machineForGen, system, {
377
- actorId: id,
378
- hooks,
379
- skipFinalizer: true,
380
- cellResources: {
381
- stateRef,
382
- stoppedRef,
383
- eventQueue: currentQueue
384
- },
385
- lifecycle: buildLifecycle(),
386
- wrapProcess: (state, event, inner) => Effect.withSpan("effect-machine.event.process", { attributes: {
387
- "effect_machine.actor.id": id,
388
- "effect_machine.state.current": state._tag,
389
- "effect_machine.event.type": event._tag
390
- } })(inner.pipe(Effect.tap((r) => Effect.annotateCurrentSpan("effect_machine.transition.matched", r.result.transitioned)))),
391
- onChildSpawned: (childId, child) => Effect.gen(function* () {
392
- childrenMap.set(childId, child);
393
- const maybeScope = yield* Effect.serviceOption(Scope.Scope);
394
- if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, Effect.sync(() => {
395
- childrenMap.delete(childId);
396
- }));
397
- })
398
- })));
399
- runtimeRef.current = yield* spawnGeneration(machineWithState);
400
- const supervision = options?.supervision;
401
- const supervisorFiberRef = { current: void 0 };
402
- const stop = Effect.gen(function* () {
403
- if (supervisorFiberRef.current !== void 0) yield* Fiber.interrupt(supervisorFiberRef.current);
404
- const currentRuntime = runtimeRef.current;
405
- if (currentRuntime !== void 0) yield* currentRuntime.stop;
406
- yield* Deferred.succeed(terminalExitDeferred, { _tag: "Stopped" });
407
- if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
408
- }).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid);
409
- const isHydrated = options?.initialState !== void 0;
410
- return buildActorRefCore(id, machine, stateRef, eventQueueRef, stoppedRef, listeners, stop, Effect.gen(function* () {
411
- if (lifecycle?.recovery !== void 0 && !isHydrated) {
412
- const resolved = yield* lifecycle.recovery.resolve({
413
- actorId: id,
414
- generation,
415
- machineInitial: machine.initial
416
- });
417
- if (Option.isSome(resolved)) {
418
- yield* SubscriptionRef.set(stateRef, resolved.value);
419
- runtimeRef.current = yield* spawnGeneration(Object.create(machine, { initial: {
420
- value: resolved.value,
421
- enumerable: true
422
- } }));
423
- }
424
- }
425
- const currentState = yield* SubscriptionRef.get(stateRef);
426
- yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
427
- type: "@machine.spawn",
428
- actorId: id,
429
- initialState: currentState,
430
- timestamp
431
- }));
432
- if (supervision !== void 0) supervisorFiberRef.current = yield* Effect.forkDaemon(runSupervisionLoop({
433
- supervision,
434
- machine,
435
- id,
436
- runtimeRef,
437
- terminalExitDeferred,
438
- pendingReplies,
439
- eventQueueRef,
440
- stateRef,
441
- stoppedRef,
442
- childrenMap,
443
- listeners,
444
- spawnGeneration,
445
- lifecycle,
446
- onRestart: options?.onRestart
447
- }));
448
- else {
449
- const currentRuntime = runtimeRef.current;
450
- if (currentRuntime !== void 0) yield* Effect.forkDaemon(Deferred.await(currentRuntime.exitDeferred).pipe(Effect.tap((exit) => Deferred.succeed(terminalExitDeferred, exit))));
451
- }
452
- const currentRuntime = runtimeRef.current;
453
- if (currentRuntime !== void 0) yield* currentRuntime.start;
454
- }).pipe(Effect.withSpan("effect-machine.actor.start"), Effect.asVoid), system, childrenMap, pendingReplies, transitionsPubSub, terminalExitDeferred);
455
- });
456
- /** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
457
- const settlePendingReplies = (pendingReplies, actorId) => Effect.sync(() => {
458
- const error = new ActorStoppedError({ actorId });
459
- for (const deferred of pendingReplies) Effect.runFork(Deferred.fail(deferred, error));
460
- pendingReplies.clear();
461
- });
462
- /** Notify all system event listeners (sync). */
463
- const notifySystemListeners = (listeners, event) => {
464
- for (const listener of listeners) try {
465
- listener(event);
466
- } catch {}
467
- };
468
- const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
469
- const actorsMap = MutableHashMap.empty();
470
- const withSpawnGate = (yield* Effect.makeSemaphore(1)).withPermits(1);
471
- const eventPubSub = yield* PubSub.unbounded();
472
- const eventListeners = /* @__PURE__ */ new Set();
473
- const emitSystemEvent = (event) => Effect.sync(() => notifySystemListeners(eventListeners, event)).pipe(Effect.andThen(PubSub.publish(eventPubSub, event)), Effect.catchAllCause(() => Effect.void), Effect.asVoid);
474
- yield* Effect.addFinalizer(() => {
475
- const stops = [];
476
- MutableHashMap.forEach(actorsMap, (actor) => {
477
- stops.push(actor.stop);
478
- });
479
- return Effect.all(stops, { concurrency: "unbounded" }).pipe(Effect.andThen(PubSub.shutdown(eventPubSub)), Effect.asVoid);
480
- });
481
- /** Check for duplicate ID, register actor, attach scope cleanup if available */
482
- const registerActor = Effect.fn("effect-machine.actorSystem.register")(function* (id, actor) {
483
- if (MutableHashMap.has(actorsMap, id)) {
484
- yield* actor.stop;
485
- return yield* new DuplicateActorError({ actorId: id });
486
- }
487
- const actorRef = actor;
488
- MutableHashMap.set(actorsMap, id, actorRef);
489
- yield* emitSystemEvent({
490
- _tag: "ActorSpawned",
491
- id,
492
- actor: actorRef
493
- });
494
- const maybeScope = yield* Effect.serviceOption(ActorScope);
495
- if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, Effect.gen(function* () {
496
- if (MutableHashMap.has(actorsMap, id)) {
497
- yield* emitSystemEvent({
498
- _tag: "ActorStopped",
499
- id,
500
- actor: actorRef,
501
- exit: { _tag: "Stopped" }
502
- });
503
- MutableHashMap.remove(actorsMap, id);
504
- }
505
- yield* actor.stop;
506
- }));
507
- return actor;
508
- });
509
- const spawnRegular = Effect.fn("effect-machine.actorSystem.spawnRegular")(function* (id, machine, spawnOptions) {
510
- if (MutableHashMap.has(actorsMap, id)) return yield* new DuplicateActorError({ actorId: id });
511
- const materialized = spawnOptions?.slots !== void 0 ? materializeMachine(machine, spawnOptions.slots) : machine;
512
- let actorRef;
513
- const actor = yield* createActor(id, materialized, {
514
- supervision: spawnOptions?.supervision,
515
- lifecycle: spawnOptions?.lifecycle,
516
- onRestart: spawnOptions?.supervision !== void 0 ? (generation, exit) => actorRef !== void 0 ? emitSystemEvent({
517
- _tag: "ActorRestarted",
518
- id,
519
- actor: actorRef,
520
- generation,
521
- exit
522
- }) : Effect.void : void 0
523
- });
524
- actorRef = actor;
525
- yield* registerActor(id, actor);
526
- yield* actor.start.pipe(Effect.catchAllCause((cause) => actor.stop.pipe(Effect.andThen(Effect.failCause(cause)))));
527
- return actor;
528
- });
529
- const spawn = (id, machine, options) => withSpawnGate(spawnRegular(id, machine, options));
530
- const get = Effect.fn("effect-machine.actorSystem.get")(function* (id) {
531
- return yield* Effect.sync(() => MutableHashMap.get(actorsMap, id));
532
- });
533
- const stop = Effect.fn("effect-machine.actorSystem.stop")(function* (id) {
534
- const maybeActor = MutableHashMap.get(actorsMap, id);
535
- if (Option.isNone(maybeActor)) return false;
536
- const actor = maybeActor.value;
537
- MutableHashMap.remove(actorsMap, id);
538
- yield* emitSystemEvent({
539
- _tag: "ActorStopped",
540
- id,
541
- actor,
542
- exit: { _tag: "Stopped" }
543
- });
544
- yield* actor.stop;
545
- return true;
546
- });
547
- return ActorSystem.of({
548
- spawn,
549
- get,
550
- stop,
551
- events: Stream.fromPubSub(eventPubSub),
552
- get actors() {
553
- const snapshot = /* @__PURE__ */ new Map();
554
- MutableHashMap.forEach(actorsMap, (actor, id) => {
555
- snapshot.set(id, actor);
556
- });
557
- return snapshot;
558
- },
559
- subscribe: (fn) => {
560
- eventListeners.add(fn);
561
- return () => {
562
- eventListeners.delete(fn);
563
- };
564
- }
565
- });
566
- });
567
- /**
568
- * Create an ActorSystem instance. Must be run in a Scope.
569
- * @internal — use Default layer for normal usage
570
- */
571
- const makeSystem = make;
572
- /**
573
- * Default ActorSystem layer
574
- */
575
- const Default = Layer.scoped(ActorSystem, make());
576
- //#endregion
577
- export { ActorScope, ActorSystem, Default, buildActorRefCore, createActor, makeSystem, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };
@@ -1,15 +0,0 @@
1
- import { PersistedEvent, PersistenceAdapter, Snapshot } from "../persistence.js";
2
- import { Effect, Layer, Ref } from "effect";
3
-
4
- //#region src/cluster/adapters/in-memory.d.ts
5
- interface EntityStore {
6
- snapshot: Snapshot<unknown> | undefined;
7
- events: Array<PersistedEvent<unknown>>;
8
- }
9
- declare const makeInMemoryPersistenceAdapter: Effect.Effect<{
10
- adapter: PersistenceAdapter;
11
- storeRef: Ref.Ref<Map<string, EntityStore>>;
12
- layer: Layer.Layer<PersistenceAdapter, never, never>;
13
- }, never, never>;
14
- //#endregion
15
- export { makeInMemoryPersistenceAdapter };
@@ -1,62 +0,0 @@
1
- import { VersionConflictError } from "../../errors.js";
2
- import { PersistenceAdapter } from "../persistence.js";
3
- import { Effect, Layer, Option, Ref } from "effect";
4
- //#region src/cluster/adapters/in-memory.ts
5
- /**
6
- * In-memory persistence adapter for testing and development (v3).
7
- *
8
- * @module
9
- */
10
- const makeKey = (key) => `${key.entityType}/${key.entityId}`;
11
- const getOrCreate = (store, key) => {
12
- let entry = store.get(key);
13
- if (entry === void 0) {
14
- entry = {
15
- snapshot: void 0,
16
- events: []
17
- };
18
- store.set(key, entry);
19
- }
20
- return entry;
21
- };
22
- const makeInMemoryPersistenceAdapter = Effect.gen(function* () {
23
- const store = /* @__PURE__ */ new Map();
24
- const storeRef = yield* Ref.make(store);
25
- const adapter = {
26
- saveSnapshot: (key, snapshot) => Effect.gen(function* () {
27
- const entry = getOrCreate(yield* Ref.get(storeRef), makeKey(key));
28
- if (entry.snapshot !== void 0 && snapshot.version < entry.snapshot.version) return yield* new VersionConflictError({
29
- expected: snapshot.version,
30
- actual: entry.snapshot.version
31
- });
32
- entry.snapshot = snapshot;
33
- }),
34
- loadSnapshot: (key) => Effect.gen(function* () {
35
- const entry = (yield* Ref.get(storeRef)).get(makeKey(key));
36
- return Option.fromNullable(entry?.snapshot);
37
- }),
38
- appendEvents: (key, events, expectedVersion) => Effect.gen(function* () {
39
- const entry = getOrCreate(yield* Ref.get(storeRef), makeKey(key));
40
- const lastEvent = entry.events[entry.events.length - 1];
41
- const currentVersion = lastEvent !== void 0 ? lastEvent.version : 0;
42
- if (currentVersion !== expectedVersion) return yield* new VersionConflictError({
43
- expected: expectedVersion,
44
- actual: currentVersion
45
- });
46
- for (const event of events) entry.events.push(event);
47
- }),
48
- loadEvents: (key, afterVersion) => Effect.gen(function* () {
49
- const entry = (yield* Ref.get(storeRef)).get(makeKey(key));
50
- if (entry === void 0) return [];
51
- if (afterVersion === void 0) return entry.events;
52
- return entry.events.filter((e) => e.version > afterVersion);
53
- })
54
- };
55
- return {
56
- adapter,
57
- storeRef,
58
- layer: Layer.succeed(PersistenceAdapter, adapter)
59
- };
60
- });
61
- //#endregion
62
- export { makeInMemoryPersistenceAdapter };
@@ -1,49 +0,0 @@
1
- import { ExtractReply, ReplyTypeBrand } from "../internal/brands.js";
2
- import { NoReplyError } from "../errors.js";
3
- import { Effect } from "effect";
4
- import { RpcClient } from "effect/unstable/rpc";
5
-
6
- //#region src/cluster/entity-actor-ref.d.ts
7
- /**
8
- * Typed client wrapper for remote entity machines.
9
- *
10
- * Unlike local `ActorRef`, this communicates over cluster RPCs.
11
- * Only operations that make sense over the network are exposed.
12
- *
13
- * @example
14
- * ```ts
15
- * const ref = yield* EntityActorRef.make(OrderEntity, OrderEntityLayer, "order-123")
16
- * yield* ref.send(OrderEvent.Ship({ trackingId: "abc" }))
17
- * const state = yield* ref.snapshot
18
- * ```
19
- */
20
- interface EntityActorRef<State extends {
21
- readonly _tag: string;
22
- }, Event extends {
23
- readonly _tag: string;
24
- }> {
25
- readonly entityId: string;
26
- /** Send event (fire-and-forget). Returns new state after processing. */
27
- readonly send: (event: Event) => Effect.Effect<State>;
28
- /** Send event and get typed domain reply (via Event.reply() schema). */
29
- readonly ask: <E extends Event & ReplyTypeBrand<unknown>>(event: E) => Effect.Effect<ExtractReply<E>, NoReplyError>;
30
- /** Get current state. */
31
- readonly snapshot: Effect.Effect<State>;
32
- }
33
- /**
34
- * Create an EntityActorRef from a test client factory and entity ID.
35
- *
36
- * @example
37
- * ```ts
38
- * const makeClient = yield* Entity.makeTestClient(entity, entityLayer)
39
- * const ref = yield* makeEntityActorRef(makeClient, "order-123")
40
- * yield* ref.send(OrderEvent.Process)
41
- * ```
42
- */
43
- declare const makeEntityActorRef: <State extends {
44
- readonly _tag: string;
45
- }, Event extends {
46
- readonly _tag: string;
47
- }>(client: RpcClient.RpcClient<any>, entityId: string) => EntityActorRef<State, Event>;
48
- //#endregion
49
- export { EntityActorRef, makeEntityActorRef };