effect-machine 0.10.0 → 0.12.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 (81) hide show
  1. package/README.md +65 -62
  2. package/dist/actor.d.ts +35 -97
  3. package/dist/actor.js +69 -98
  4. package/dist/cluster/adapters/in-memory.d.ts +28 -0
  5. package/dist/cluster/adapters/in-memory.js +79 -0
  6. package/dist/cluster/entity-actor-ref.d.ts +56 -0
  7. package/dist/cluster/entity-actor-ref.js +33 -0
  8. package/dist/cluster/entity-machine.d.ts +31 -49
  9. package/dist/cluster/entity-machine.js +167 -52
  10. package/dist/cluster/index.d.ts +5 -2
  11. package/dist/cluster/index.js +4 -1
  12. package/dist/cluster/persistence.d.ts +49 -0
  13. package/dist/cluster/persistence.js +18 -0
  14. package/dist/cluster/to-entity.d.ts +9 -3
  15. package/dist/cluster/to-entity.js +16 -4
  16. package/dist/errors.d.ts +12 -1
  17. package/dist/errors.js +8 -1
  18. package/dist/index.d.ts +5 -8
  19. package/dist/index.js +1 -6
  20. package/dist/internal/brands.d.ts +14 -1
  21. package/dist/internal/runtime.d.ts +67 -0
  22. package/dist/internal/runtime.js +248 -0
  23. package/dist/internal/transition.d.ts +6 -1
  24. package/dist/internal/transition.js +16 -4
  25. package/dist/internal/utils.d.ts +42 -6
  26. package/dist/internal/utils.js +28 -6
  27. package/dist/machine.d.ts +55 -46
  28. package/dist/machine.js +74 -13
  29. package/dist/schema.d.ts +35 -34
  30. package/dist/schema.js +32 -3
  31. package/dist/testing.js +4 -2
  32. package/package.json +4 -4
  33. package/v3/dist/actor.d.ts +29 -96
  34. package/v3/dist/actor.js +52 -97
  35. package/v3/dist/cluster/adapters/in-memory.d.ts +15 -0
  36. package/v3/dist/cluster/adapters/in-memory.js +62 -0
  37. package/v3/dist/cluster/entity-actor-ref.d.ts +49 -0
  38. package/v3/dist/cluster/entity-actor-ref.js +19 -0
  39. package/v3/dist/cluster/entity-machine.d.ts +34 -49
  40. package/v3/dist/cluster/entity-machine.js +134 -50
  41. package/v3/dist/cluster/index.d.ts +5 -2
  42. package/v3/dist/cluster/index.js +4 -1
  43. package/v3/dist/cluster/persistence.d.ts +48 -0
  44. package/v3/dist/cluster/persistence.js +14 -0
  45. package/v3/dist/cluster/to-entity.d.ts +5 -2
  46. package/v3/dist/cluster/to-entity.js +12 -4
  47. package/v3/dist/errors.d.ts +16 -1
  48. package/v3/dist/errors.js +8 -1
  49. package/v3/dist/index.d.ts +5 -8
  50. package/v3/dist/index.js +1 -6
  51. package/v3/dist/internal/brands.d.ts +15 -1
  52. package/v3/dist/internal/runtime.d.ts +65 -0
  53. package/v3/dist/internal/runtime.js +236 -0
  54. package/v3/dist/internal/transition.d.ts +5 -0
  55. package/v3/dist/internal/transition.js +15 -3
  56. package/v3/dist/internal/utils.d.ts +42 -6
  57. package/v3/dist/internal/utils.js +28 -6
  58. package/v3/dist/machine.d.ts +48 -46
  59. package/v3/dist/machine.js +71 -13
  60. package/v3/dist/schema.d.ts +35 -34
  61. package/v3/dist/schema.js +29 -3
  62. package/dist/persistence/adapter.d.ts +0 -135
  63. package/dist/persistence/adapter.js +0 -25
  64. package/dist/persistence/adapters/in-memory.d.ts +0 -32
  65. package/dist/persistence/adapters/in-memory.js +0 -174
  66. package/dist/persistence/index.d.ts +0 -5
  67. package/dist/persistence/index.js +0 -5
  68. package/dist/persistence/persistent-actor.d.ts +0 -50
  69. package/dist/persistence/persistent-actor.js +0 -404
  70. package/dist/persistence/persistent-machine.d.ts +0 -105
  71. package/dist/persistence/persistent-machine.js +0 -22
  72. package/v3/dist/persistence/adapter.d.ts +0 -138
  73. package/v3/dist/persistence/adapter.js +0 -25
  74. package/v3/dist/persistence/adapters/in-memory.d.ts +0 -32
  75. package/v3/dist/persistence/adapters/in-memory.js +0 -174
  76. package/v3/dist/persistence/index.d.ts +0 -5
  77. package/v3/dist/persistence/index.js +0 -5
  78. package/v3/dist/persistence/persistent-actor.d.ts +0 -50
  79. package/v3/dist/persistence/persistent-actor.js +0 -404
  80. package/v3/dist/persistence/persistent-machine.d.ts +0 -105
  81. package/v3/dist/persistence/persistent-machine.js +0 -22
package/dist/actor.js CHANGED
@@ -1,12 +1,9 @@
1
1
  import { Inspector } from "./inspection.js";
2
2
  import { INTERNAL_INIT_EVENT } from "./internal/utils.js";
3
3
  import { ActorStoppedError, DuplicateActorError, NoReplyError } from "./errors.js";
4
- import { isPersistentMachine } from "./persistence/persistent-machine.js";
5
4
  import { emitWithTimestamp } from "./internal/inspection.js";
6
5
  import { processEventCore, resolveTransition, runSpawnEffects, shouldPostpone } from "./internal/transition.js";
7
- import { PersistenceAdapterTag, PersistenceError } from "./persistence/adapter.js";
8
- import { createPersistentActor, restorePersistentActor } from "./persistence/persistent-actor.js";
9
- import { Cause, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, PubSub, Queue, Ref, Scope, Semaphore, ServiceMap, Stream, SubscriptionRef } from "effect";
6
+ import { Cause, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, PubSub, Queue, Ref, Schema, Scope, Semaphore, ServiceMap, Stream, SubscriptionRef } from "effect";
10
7
  //#region src/actor.ts
11
8
  /**
12
9
  * Actor system: spawning, lifecycle, and event processing.
@@ -29,9 +26,9 @@ const notifyListeners = (listeners, state) => {
29
26
  } catch {}
30
27
  };
31
28
  /**
32
- * Build core ActorRef methods shared between regular and persistent actors.
29
+ * Build core ActorRef methods.
33
30
  */
34
- const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listeners, stop, system, childrenMap, pendingReplies) => {
31
+ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listeners, stop, system, childrenMap, pendingReplies, transitionsPubSub) => {
35
32
  const send = Effect.fn("effect-machine.actor.send")(function* (event) {
36
33
  if (yield* Ref.get(stoppedRef)) return;
37
34
  yield* Queue.offer(eventQueue, {
@@ -119,6 +116,7 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
119
116
  matches,
120
117
  can,
121
118
  changes: SubscriptionRef.changes(stateRef),
119
+ transitions: transitionsPubSub !== void 0 ? Stream.fromPubSub(transitionsPubSub) : Stream.empty,
122
120
  waitFor,
123
121
  awaitFinal,
124
122
  sendAndWait,
@@ -149,7 +147,8 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
149
147
  /**
150
148
  * Create and start an actor for a machine
151
149
  */
152
- const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machine) {
150
+ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machine, options) {
151
+ const initial = options?.initialState ?? machine.initial;
153
152
  yield* Effect.annotateCurrentSpan("effect_machine.actor.id", id);
154
153
  const existingSystem = yield* Effect.serviceOption(ActorSystem);
155
154
  let system;
@@ -164,6 +163,7 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
164
163
  const eventQueue = yield* Queue.unbounded();
165
164
  const stoppedRef = yield* Ref.make(false);
166
165
  const childrenMap = /* @__PURE__ */ new Map();
166
+ const deferredReplyRef = { current: void 0 };
167
167
  const selfSend = Effect.fn("effect-machine.actor.self.send")(function* (event) {
168
168
  if (yield* Ref.get(stoppedRef)) return;
169
169
  yield* Queue.offer(eventQueue, {
@@ -182,22 +182,31 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
182
182
  childrenMap.delete(childId);
183
183
  }));
184
184
  return child;
185
+ }),
186
+ reply: (value) => Effect.sync(() => {
187
+ const deferred = deferredReplyRef.current;
188
+ if (deferred !== void 0) {
189
+ deferredReplyRef.current = void 0;
190
+ Effect.runFork(Deferred.succeed(deferred, value));
191
+ return true;
192
+ }
193
+ return false;
185
194
  })
186
195
  };
187
- yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", machine.initial._tag);
196
+ yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", initial._tag);
188
197
  yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
189
198
  type: "@machine.spawn",
190
199
  actorId: id,
191
- initialState: machine.initial,
200
+ initialState: initial,
192
201
  timestamp
193
202
  }));
194
- const stateRef = yield* SubscriptionRef.make(machine.initial);
203
+ const stateRef = yield* SubscriptionRef.make(initial);
195
204
  const listeners = /* @__PURE__ */ new Set();
196
205
  const backgroundFibers = [];
197
206
  const initEvent = { _tag: INTERNAL_INIT_EVENT };
198
207
  const ctx = {
199
208
  actorId: id,
200
- state: machine.initial,
209
+ state: initial,
201
210
  event: initEvent,
202
211
  self,
203
212
  system
@@ -206,7 +215,7 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
206
215
  for (const bg of machine.backgroundEffects) {
207
216
  const fiber = yield* Effect.forkDetach(bg.handler({
208
217
  actorId: id,
209
- state: machine.initial,
218
+ state: initial,
210
219
  event: initEvent,
211
220
  self,
212
221
  effects: effectSlots,
@@ -215,14 +224,14 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
215
224
  backgroundFibers.push(fiber);
216
225
  }
217
226
  const stateScopeRef = { current: yield* Scope.make() };
218
- yield* runSpawnEffectsWithInspection(machine, machine.initial, initEvent, self, stateScopeRef.current, id, inspectorValue, system);
219
- if (machine.finalStates.has(machine.initial._tag)) {
227
+ yield* runSpawnEffectsWithInspection(machine, initial, initEvent, self, stateScopeRef.current, id, inspectorValue, system);
228
+ if (machine.finalStates.has(initial._tag)) {
220
229
  yield* Scope.close(stateScopeRef.current, Exit.void);
221
230
  yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
222
231
  yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
223
232
  type: "@machine.stop",
224
233
  actorId: id,
225
- finalState: machine.initial,
234
+ finalState: initial,
226
235
  timestamp
227
236
  }));
228
237
  yield* Ref.set(stoppedRef, true);
@@ -230,7 +239,8 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
230
239
  return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, Ref.set(stoppedRef, true).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap, /* @__PURE__ */ new Set());
231
240
  }
232
241
  const pendingReplies = /* @__PURE__ */ new Set();
233
- const loopFiber = yield* Effect.forkDetach(eventLoop(machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, id, inspectorValue, system, pendingReplies));
242
+ const transitionsPubSub = yield* PubSub.unbounded();
243
+ const loopFiber = yield* Effect.forkDetach(eventLoop(machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, id, inspectorValue, system, pendingReplies, transitionsPubSub, deferredReplyRef));
234
244
  return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, Effect.gen(function* () {
235
245
  const finalState = yield* SubscriptionRef.get(stateRef);
236
246
  yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
@@ -245,7 +255,7 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
245
255
  yield* Scope.close(stateScopeRef.current, Exit.void);
246
256
  yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
247
257
  if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
248
- }).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap, pendingReplies);
258
+ }).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap, pendingReplies, transitionsPubSub);
249
259
  });
250
260
  /** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
251
261
  const settlePendingReplies = (pendingReplies, actorId) => Effect.sync(() => {
@@ -258,7 +268,7 @@ const settlePendingReplies = (pendingReplies, actorId) => Effect.sync(() => {
258
268
  * Includes postpone buffer — events matching postpone rules are buffered
259
269
  * and drained after state tag changes (gen_statem semantics).
260
270
  */
261
- const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, actorId, inspector, system, pendingReplies) {
271
+ const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, actorId, inspector, system, pendingReplies, transitionsPubSub, deferredReplyRef) {
262
272
  const postponed = [];
263
273
  const hasPostponeRules = machine.postponeRules.length > 0;
264
274
  const processQueued = Effect.fn("effect-machine.actor.processQueued")(function* (queued) {
@@ -274,6 +284,7 @@ const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine
274
284
  lifecycleRan: false,
275
285
  isFinal: false,
276
286
  hasReply: false,
287
+ deferReply: false,
277
288
  reply: void 0,
278
289
  postponed: true
279
290
  };
@@ -294,13 +305,30 @@ const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine
294
305
  yield* Deferred.succeed(queued.reply, result);
295
306
  break;
296
307
  case "ask":
297
- if (result.hasReply) yield* Deferred.succeed(queued.reply, result.reply);
308
+ if (result.hasReply) {
309
+ const replySchema = machine._replySchemas?.get(event._tag);
310
+ if (replySchema !== void 0) {
311
+ let decoded;
312
+ try {
313
+ decoded = Schema.decodeUnknownSync(replySchema)(result.reply);
314
+ } catch (decodeError) {
315
+ yield* Deferred.die(queued.reply, decodeError);
316
+ return yield* Effect.die(decodeError);
317
+ }
318
+ yield* Deferred.succeed(queued.reply, decoded);
319
+ } else yield* Deferred.succeed(queued.reply, result.reply);
320
+ } else if (result.deferReply) deferredReplyRef.current = queued.reply;
298
321
  else yield* Deferred.fail(queued.reply, new NoReplyError({
299
322
  actorId,
300
323
  eventTag: event._tag
301
324
  }));
302
325
  break;
303
326
  }
327
+ if (result.transitioned) yield* PubSub.publish(transitionsPubSub, {
328
+ fromState: result.previousState,
329
+ toState: result.newState,
330
+ event
331
+ });
304
332
  return {
305
333
  shouldStop,
306
334
  stateChanged: result.lifecycleRan
@@ -316,15 +344,21 @@ const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine
316
344
  yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
317
345
  return;
318
346
  }
319
- if (stateChanged && postponed.length > 0) {
347
+ let drainTriggered = stateChanged;
348
+ while (drainTriggered && postponed.length > 0) {
349
+ drainTriggered = false;
320
350
  const drained = postponed.splice(0);
321
- for (const entry of drained) if ((yield* processQueued(entry)).shouldStop) {
322
- yield* Ref.set(stoppedRef, true);
323
- settlePostponedBuffer(postponed, pendingReplies, actorId);
324
- yield* settlePendingReplies(pendingReplies, actorId);
325
- yield* Scope.close(stateScopeRef.current, Exit.void);
326
- yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
327
- return;
351
+ for (const entry of drained) {
352
+ const drain = yield* processQueued(entry);
353
+ if (drain.shouldStop) {
354
+ yield* Ref.set(stoppedRef, true);
355
+ settlePostponedBuffer(postponed, pendingReplies, actorId);
356
+ yield* settlePendingReplies(pendingReplies, actorId);
357
+ yield* Scope.close(stateScopeRef.current, Exit.void);
358
+ yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
359
+ return;
360
+ }
361
+ if (drain.stateChanged) drainTriggered = true;
328
362
  }
329
363
  }
330
364
  }
@@ -480,25 +514,7 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
480
514
  if (MutableHashMap.has(actorsMap, id)) return yield* new DuplicateActorError({ actorId: id });
481
515
  return yield* registerActor(id, yield* createActor(id, built._inner));
482
516
  });
483
- const spawnPersistent = Effect.fn("effect-machine.actorSystem.spawnPersistent")(function* (id, persistentMachine) {
484
- if (MutableHashMap.has(actorsMap, id)) return yield* new DuplicateActorError({ actorId: id });
485
- const adapter = yield* PersistenceAdapterTag;
486
- const maybeSnapshot = yield* adapter.loadSnapshot(id, persistentMachine.persistence.stateSchema);
487
- return yield* registerActor(id, yield* createPersistentActor(id, persistentMachine, maybeSnapshot, yield* adapter.loadEvents(id, persistentMachine.persistence.eventSchema, Option.isSome(maybeSnapshot) ? maybeSnapshot.value.version : void 0)));
488
- });
489
- const spawnImpl = Effect.fn("effect-machine.actorSystem.spawn")(function* (id, machine) {
490
- if (isPersistentMachine(machine)) return yield* spawnPersistent(id, machine);
491
- return yield* spawnRegular(id, machine);
492
- });
493
- function spawn(id, machine) {
494
- return withSpawnGate(spawnImpl(id, machine));
495
- }
496
- const restoreImpl = Effect.fn("effect-machine.actorSystem.restore")(function* (id, persistentMachine) {
497
- const maybeActor = yield* restorePersistentActor(id, persistentMachine);
498
- if (Option.isSome(maybeActor)) yield* registerActor(id, maybeActor.value);
499
- return maybeActor;
500
- });
501
- const restore = (id, persistentMachine) => withSpawnGate(restoreImpl(id, persistentMachine));
517
+ const spawn = (id, machine) => withSpawnGate(spawnRegular(id, machine));
502
518
  const get = Effect.fn("effect-machine.actorSystem.get")(function* (id) {
503
519
  return yield* Effect.sync(() => MutableHashMap.get(actorsMap, id));
504
520
  });
@@ -515,55 +531,8 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
515
531
  yield* actor.stop;
516
532
  return true;
517
533
  });
518
- const listPersisted = Effect.fn("effect-machine.actorSystem.listPersisted")(function* () {
519
- const adapter = yield* PersistenceAdapterTag;
520
- if (adapter.listActors === void 0) return [];
521
- return yield* adapter.listActors();
522
- });
523
- const restoreMany = Effect.fn("effect-machine.actorSystem.restoreMany")(function* (ids, persistentMachine) {
524
- const restored = [];
525
- const failed = [];
526
- for (const id of ids) {
527
- if (MutableHashMap.has(actorsMap, id)) continue;
528
- const result = yield* Effect.result(restore(id, persistentMachine));
529
- if (result._tag === "Failure") failed.push({
530
- id,
531
- error: result.failure
532
- });
533
- else if (Option.isSome(result.success)) restored.push(result.success.value);
534
- else failed.push({
535
- id,
536
- error: new PersistenceError({
537
- operation: "restore",
538
- actorId: id,
539
- message: "No persisted state found"
540
- })
541
- });
542
- }
543
- return {
544
- restored,
545
- failed
546
- };
547
- });
548
- const restoreAll = Effect.fn("effect-machine.actorSystem.restoreAll")(function* (persistentMachine, options) {
549
- const adapter = yield* PersistenceAdapterTag;
550
- if (adapter.listActors === void 0) return {
551
- restored: [],
552
- failed: []
553
- };
554
- const machineType = persistentMachine.persistence.machineType;
555
- if (machineType === void 0) return yield* new PersistenceError({
556
- operation: "restoreAll",
557
- actorId: "*",
558
- message: "restoreAll requires explicit machineType in persistence config"
559
- });
560
- let filtered = (yield* adapter.listActors()).filter((meta) => meta.machineType === machineType);
561
- if (options?.filter !== void 0) filtered = filtered.filter(options.filter);
562
- return yield* restoreMany(filtered.map((meta) => meta.id), persistentMachine);
563
- });
564
534
  return ActorSystem.of({
565
535
  spawn,
566
- restore,
567
536
  get,
568
537
  stop,
569
538
  events: Stream.fromPubSub(eventPubSub),
@@ -579,15 +548,17 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
579
548
  return () => {
580
549
  eventListeners.delete(fn);
581
550
  };
582
- },
583
- listPersisted,
584
- restoreMany,
585
- restoreAll
551
+ }
586
552
  });
587
553
  });
588
554
  /**
555
+ * Create an ActorSystem instance. Must be run in a Scope.
556
+ * @internal — use Default layer for normal usage
557
+ */
558
+ const makeSystem = make;
559
+ /**
589
560
  * Default ActorSystem layer
590
561
  */
591
562
  const Default = Layer.effect(ActorSystem, make());
592
563
  //#endregion
593
- export { ActorSystem, Default, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };
564
+ export { ActorSystem, Default, buildActorRefCore, createActor, makeSystem, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };
@@ -0,0 +1,28 @@
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
+ /**
10
+ * Create an in-memory persistence adapter.
11
+ *
12
+ * Returns a Layer providing `PersistenceAdapter` and a ref
13
+ * to the backing store for test assertions.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * const { layer, storeRef } = yield* makeInMemoryPersistenceAdapter
18
+ * // Use layer to provide PersistenceAdapter
19
+ * // Inspect storeRef for test assertions
20
+ * ```
21
+ */
22
+ declare const makeInMemoryPersistenceAdapter: Effect.Effect<{
23
+ adapter: PersistenceAdapter;
24
+ storeRef: Ref.Ref<Map<string, EntityStore>>;
25
+ layer: Layer.Layer<PersistenceAdapter, never, never>;
26
+ }, never, never>;
27
+ //#endregion
28
+ export { makeInMemoryPersistenceAdapter };
@@ -0,0 +1,79 @@
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.
7
+ *
8
+ * Backed by a simple Map — state is lost on process exit.
9
+ * Supports both snapshot and journal strategies with proper
10
+ * version checking (CAS on appends, monotonic on snapshots).
11
+ *
12
+ * @module
13
+ */
14
+ const makeKey = (key) => `${key.entityType}/${key.entityId}`;
15
+ const getOrCreate = (store, key) => {
16
+ let entry = store.get(key);
17
+ if (entry === void 0) {
18
+ entry = {
19
+ snapshot: void 0,
20
+ events: []
21
+ };
22
+ store.set(key, entry);
23
+ }
24
+ return entry;
25
+ };
26
+ /**
27
+ * Create an in-memory persistence adapter.
28
+ *
29
+ * Returns a Layer providing `PersistenceAdapter` and a ref
30
+ * to the backing store for test assertions.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * const { layer, storeRef } = yield* makeInMemoryPersistenceAdapter
35
+ * // Use layer to provide PersistenceAdapter
36
+ * // Inspect storeRef for test assertions
37
+ * ```
38
+ */
39
+ const makeInMemoryPersistenceAdapter = Effect.gen(function* () {
40
+ const store = /* @__PURE__ */ new Map();
41
+ const storeRef = yield* Ref.make(store);
42
+ const adapter = {
43
+ saveSnapshot: (key, snapshot) => Effect.gen(function* () {
44
+ const entry = getOrCreate(yield* Ref.get(storeRef), makeKey(key));
45
+ if (entry.snapshot !== void 0 && snapshot.version < entry.snapshot.version) return yield* new VersionConflictError({
46
+ expected: snapshot.version,
47
+ actual: entry.snapshot.version
48
+ });
49
+ entry.snapshot = snapshot;
50
+ }),
51
+ loadSnapshot: (key) => Effect.gen(function* () {
52
+ const entry = (yield* Ref.get(storeRef)).get(makeKey(key));
53
+ return Option.fromNullishOr(entry?.snapshot);
54
+ }),
55
+ appendEvents: (key, events, expectedVersion) => Effect.gen(function* () {
56
+ const entry = getOrCreate(yield* Ref.get(storeRef), makeKey(key));
57
+ const lastEvent = entry.events[entry.events.length - 1];
58
+ const currentVersion = lastEvent !== void 0 ? lastEvent.version : 0;
59
+ if (currentVersion !== expectedVersion) return yield* new VersionConflictError({
60
+ expected: expectedVersion,
61
+ actual: currentVersion
62
+ });
63
+ for (const event of events) entry.events.push(event);
64
+ }),
65
+ loadEvents: (key, afterVersion) => Effect.gen(function* () {
66
+ const entry = (yield* Ref.get(storeRef)).get(makeKey(key));
67
+ if (entry === void 0) return [];
68
+ if (afterVersion === void 0) return entry.events;
69
+ return entry.events.filter((e) => e.version > afterVersion);
70
+ })
71
+ };
72
+ return {
73
+ adapter,
74
+ storeRef,
75
+ layer: Layer.succeed(PersistenceAdapter, adapter)
76
+ };
77
+ });
78
+ //#endregion
79
+ export { makeInMemoryPersistenceAdapter };
@@ -0,0 +1,56 @@
1
+ import { ExtractReply, ReplyTypeBrand } from "../internal/brands.js";
2
+ import { ActorStoppedError, NoReplyError } from "../errors.js";
3
+ import { EntityRpcs } from "./to-entity.js";
4
+ import { Effect, Stream } from "effect";
5
+ import { RpcClient } from "effect/unstable/rpc";
6
+
7
+ //#region src/cluster/entity-actor-ref.d.ts
8
+ /**
9
+ * Typed client wrapper for remote entity machines.
10
+ *
11
+ * Unlike local `ActorRef`, this communicates over cluster RPCs.
12
+ * Only operations that make sense over the network are exposed.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const ref = makeEntityActorRef(client, "order-123")
17
+ * yield* ref.send(OrderEvent.Ship({ trackingId: "abc" }))
18
+ * const state = yield* ref.snapshot
19
+ * yield* ref.waitFor((s) => s._tag === "Shipped")
20
+ * ```
21
+ */
22
+ interface EntityActorRef<State extends {
23
+ readonly _tag: string;
24
+ }, Event extends {
25
+ readonly _tag: string;
26
+ }> {
27
+ readonly entityId: string;
28
+ /** Send event. Returns new state after processing. */
29
+ readonly send: (event: Event) => Effect.Effect<State>;
30
+ /** Send event and get typed domain reply (via Event.reply() schema). */
31
+ readonly ask: <E extends Event & ReplyTypeBrand<unknown>>(event: E) => Effect.Effect<ExtractReply<E>, NoReplyError>;
32
+ /** Get current state. */
33
+ readonly snapshot: Effect.Effect<State>;
34
+ /** Stream of state changes (via WatchState streaming RPC). */
35
+ readonly watch: Stream.Stream<State>;
36
+ /** Wait for a state matching the predicate. Snapshots first, then watches stream. */
37
+ readonly waitFor: (predicate: (state: State) => boolean) => Effect.Effect<State, ActorStoppedError>;
38
+ }
39
+ /**
40
+ * Create an EntityActorRef from a RPC client.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * const makeClient = yield* Entity.makeTestClient(entity, entityLayer)
45
+ * const client = yield* makeClient("order-123")
46
+ * const ref = makeEntityActorRef(client, "order-123")
47
+ * yield* ref.send(OrderEvent.Process)
48
+ * ```
49
+ */
50
+ declare const makeEntityActorRef: <State extends {
51
+ readonly _tag: string;
52
+ }, Event extends {
53
+ readonly _tag: string;
54
+ }, Rpcs extends EntityRpcs<any, any>[number]>(client: RpcClient.RpcClient<Rpcs>, entityId: string) => EntityActorRef<State, Event>;
55
+ //#endregion
56
+ export { EntityActorRef, makeEntityActorRef };
@@ -0,0 +1,33 @@
1
+ import { ActorStoppedError } from "../errors.js";
2
+ import { Effect, Option, Stream } from "effect";
3
+ //#region src/cluster/entity-actor-ref.ts
4
+ /**
5
+ * Create an EntityActorRef from a RPC client.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const makeClient = yield* Entity.makeTestClient(entity, entityLayer)
10
+ * const client = yield* makeClient("order-123")
11
+ * const ref = makeEntityActorRef(client, "order-123")
12
+ * yield* ref.send(OrderEvent.Process)
13
+ * ```
14
+ */
15
+ const makeEntityActorRef = (client, entityId) => {
16
+ const c = client;
17
+ return {
18
+ entityId,
19
+ send: (event) => c.Send({ event }),
20
+ ask: ((event) => c.Ask({ event })),
21
+ snapshot: c.GetState(),
22
+ watch: c.WatchState(),
23
+ waitFor: (predicate) => Effect.gen(function* () {
24
+ const current = yield* c.GetState();
25
+ if (predicate(current)) return current;
26
+ const result = yield* c.WatchState().pipe(Stream.filter(predicate), Stream.take(1), Stream.runHead);
27
+ if (Option.isSome(result)) return result.value;
28
+ return yield* new ActorStoppedError({ actorId: entityId });
29
+ })
30
+ };
31
+ };
32
+ //#endregion
33
+ export { makeEntityActorRef };
@@ -1,7 +1,7 @@
1
- import { EffectsDef, GuardsDef } from "../slot.js";
2
1
  import { ProcessEventHooks } from "../internal/transition.js";
3
2
  import { Machine } from "../machine.js";
4
- import { Layer } from "effect";
3
+ import { EntityPersistenceConfig } from "./persistence.js";
4
+ import { Duration, Layer, Schedule } from "effect";
5
5
  import { Entity } from "effect/unstable/cluster";
6
6
  import { Rpc } from "effect/unstable/rpc";
7
7
 
@@ -13,77 +13,59 @@ interface EntityMachineOptions<S, E> {
13
13
  /**
14
14
  * Initialize state from entity ID.
15
15
  * Called once when entity is first activated.
16
- *
17
- * @example
18
- * ```ts
19
- * EntityMachine.layer(OrderEntity, orderMachine, {
20
- * initializeState: (entityId) => OrderState.Pending({ orderId: entityId }),
21
- * })
22
- * ```
23
16
  */
24
17
  readonly initializeState?: (entityId: string) => S;
25
18
  /**
26
19
  * Optional hooks for inspection/tracing.
27
- * Called at specific points during event processing.
28
- *
29
- * @example
30
- * ```ts
31
- * EntityMachine.layer(OrderEntity, orderMachine, {
32
- * hooks: {
33
- * onTransition: (from, to, event) =>
34
- * Effect.log(`Transition: ${from._tag} -> ${to._tag}`),
35
- * onSpawnEffect: (state) =>
36
- * Effect.log(`Running spawn effects for ${state._tag}`),
37
- * onError: ({ phase, state }) =>
38
- * Effect.log(`Defect in ${phase} at ${state._tag}`),
39
- * },
40
- * })
41
- * ```
42
20
  */
43
21
  readonly hooks?: ProcessEventHooks<S, E>;
22
+ /**
23
+ * Maximum idle time before entity deactivation.
24
+ * Forwarded to Entity.toLayerQueue.
25
+ */
26
+ readonly maxIdleTime?: Duration.Input;
27
+ /**
28
+ * Mailbox capacity. Default: "unbounded".
29
+ * Forwarded to Entity.toLayerQueue.
30
+ */
31
+ readonly mailboxCapacity?: number | "unbounded";
32
+ /**
33
+ * Disable fatal defects (defects won't crash the entity activation).
34
+ * Forwarded to Entity.toLayerQueue.
35
+ */
36
+ readonly disableFatalDefects?: boolean;
37
+ /**
38
+ * Retry policy for defects (schedule for restarting after defect).
39
+ * Forwarded to Entity.toLayerQueue.
40
+ */
41
+ readonly defectRetryPolicy?: Schedule.Schedule<any, unknown>;
42
+ /**
43
+ * Persistence configuration. When set, requires PersistenceAdapter in R.
44
+ */
45
+ readonly persistence?: EntityPersistenceConfig;
44
46
  }
45
47
  /**
46
48
  * Create an Entity layer that wires a machine to handle RPC calls.
47
49
  *
48
- * The layer:
49
- * - Maintains state via Ref per entity instance
50
- * - Resolves transitions using the indexed lookup
51
- * - Evaluates guards in registration order
52
- * - Runs lifecycle effects (onEnter/spawn)
53
- * - Processes internal events from spawn effects
50
+ * Uses `Entity.toLayerQueue` for a single serialized mailbox per entity.
51
+ * The runtime kernel handles event processing, postpone, background effects,
52
+ * spawn effects, and final state detection.
54
53
  *
55
54
  * @example
56
55
  * ```ts
57
- * const OrderEntity = toEntity(orderMachine, {
58
- * type: "Order",
59
- * stateSchema: OrderState,
60
- * eventSchema: OrderEvent,
61
- * })
56
+ * const OrderEntity = toEntity(orderMachine, { type: "Order" })
62
57
  *
63
58
  * const OrderEntityLayer = EntityMachine.layer(OrderEntity, orderMachine, {
64
59
  * initializeState: (entityId) => OrderState.Pending({ orderId: entityId }),
65
60
  * })
66
- *
67
- * // Use in cluster
68
- * const program = Effect.gen(function* () {
69
- * const client = yield* ShardingClient.client(OrderEntity)
70
- * yield* client.Send("order-123", { event: OrderEvent.Ship({ trackingId: "abc" }) })
71
- * })
72
61
  * ```
73
62
  */
74
63
  declare const EntityMachine: {
75
- /**
76
- * Create a layer that wires a machine to an Entity.
77
- *
78
- * @param entity - Entity created via toEntity()
79
- * @param machine - Machine with all effects provided
80
- * @param options - Optional configuration (state initializer, inspection hooks)
81
- */
82
64
  layer: <S extends {
83
65
  readonly _tag: string;
84
66
  }, E extends {
85
67
  readonly _tag: string;
86
- }, 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>;
68
+ }, R, EntityType extends string, Rpcs extends Rpc.Any>(entity: Entity.Entity<EntityType, Rpcs>, machine: Machine<S, E, R, any, any, any, any>, options?: EntityMachineOptions<S, E>) => Layer.Layer<never, never, R>;
87
69
  };
88
70
  //#endregion
89
71
  export { EntityMachine, EntityMachineOptions };