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/v3/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, Context, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, PubSub, Queue, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect";
6
+ import { Cause, Context, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, PubSub, Queue, Ref, Runtime, Schema, Scope, Stream, SubscriptionRef } from "effect";
10
7
  //#region src/actor.ts
11
8
  /**
12
9
  * Actor system: spawning, lifecycle, and event processing.
@@ -31,7 +28,7 @@ const notifyListeners = (listeners, state) => {
31
28
  /**
32
29
  * Build core ActorRef methods shared between regular and persistent actors.
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, {
@@ -121,6 +118,7 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
121
118
  matches,
122
119
  can,
123
120
  changes: stateRef.changes,
121
+ transitions: transitionsPubSub !== void 0 ? Stream.fromPubSub(transitionsPubSub) : Stream.empty,
124
122
  waitFor,
125
123
  awaitFinal,
126
124
  sendAndWait,
@@ -151,7 +149,8 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
151
149
  /**
152
150
  * Create and start an actor for a machine
153
151
  */
154
- const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machine) {
152
+ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machine, options) {
153
+ const initial = options?.initialState ?? machine.initial;
155
154
  yield* Effect.annotateCurrentSpan("effect_machine.actor.id", id);
156
155
  const existingSystem = yield* Effect.serviceOption(ActorSystem);
157
156
  let system;
@@ -186,20 +185,20 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
186
185
  return child;
187
186
  })
188
187
  };
189
- yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", machine.initial._tag);
188
+ yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", initial._tag);
190
189
  yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
191
190
  type: "@machine.spawn",
192
191
  actorId: id,
193
- initialState: machine.initial,
192
+ initialState: initial,
194
193
  timestamp
195
194
  }));
196
- const stateRef = yield* SubscriptionRef.make(machine.initial);
195
+ const stateRef = yield* SubscriptionRef.make(initial);
197
196
  const listeners = /* @__PURE__ */ new Set();
198
197
  const backgroundFibers = [];
199
198
  const initEvent = { _tag: INTERNAL_INIT_EVENT };
200
199
  const ctx = {
201
200
  actorId: id,
202
- state: machine.initial,
201
+ state: initial,
203
202
  event: initEvent,
204
203
  self,
205
204
  system
@@ -208,7 +207,7 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
208
207
  for (const bg of machine.backgroundEffects) {
209
208
  const fiber = yield* Effect.forkDaemon(bg.handler({
210
209
  actorId: id,
211
- state: machine.initial,
210
+ state: initial,
212
211
  event: initEvent,
213
212
  self,
214
213
  effects: effectSlots,
@@ -217,14 +216,14 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
217
216
  backgroundFibers.push(fiber);
218
217
  }
219
218
  const stateScopeRef = { current: yield* Scope.make() };
220
- yield* runSpawnEffectsWithInspection(machine, machine.initial, initEvent, self, stateScopeRef.current, id, inspectorValue, system);
221
- if (machine.finalStates.has(machine.initial._tag)) {
219
+ yield* runSpawnEffectsWithInspection(machine, initial, initEvent, self, stateScopeRef.current, id, inspectorValue, system);
220
+ if (machine.finalStates.has(initial._tag)) {
222
221
  yield* Scope.close(stateScopeRef.current, Exit.void);
223
222
  yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
224
223
  yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
225
224
  type: "@machine.stop",
226
225
  actorId: id,
227
- finalState: machine.initial,
226
+ finalState: initial,
228
227
  timestamp
229
228
  }));
230
229
  yield* Ref.set(stoppedRef, true);
@@ -232,7 +231,8 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
232
231
  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());
233
232
  }
234
233
  const pendingReplies = /* @__PURE__ */ new Set();
235
- const loopFiber = yield* Effect.forkDaemon(eventLoop(machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, id, inspectorValue, system, pendingReplies));
234
+ const transitionsPubSub = yield* PubSub.unbounded();
235
+ const loopFiber = yield* Effect.forkDaemon(eventLoop(machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, id, inspectorValue, system, pendingReplies, transitionsPubSub));
236
236
  return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, Effect.gen(function* () {
237
237
  const finalState = yield* SubscriptionRef.get(stateRef);
238
238
  yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
@@ -247,7 +247,7 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
247
247
  yield* Scope.close(stateScopeRef.current, Exit.void);
248
248
  yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
249
249
  if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
250
- }).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap, pendingReplies);
250
+ }).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap, pendingReplies, transitionsPubSub);
251
251
  });
252
252
  /** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
253
253
  const settlePendingReplies = (pendingReplies, actorId) => Effect.sync(() => {
@@ -260,7 +260,7 @@ const settlePendingReplies = (pendingReplies, actorId) => Effect.sync(() => {
260
260
  * Includes postpone buffer — events matching postpone rules are buffered
261
261
  * and drained after state tag changes (gen_statem semantics).
262
262
  */
263
- const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, actorId, inspector, system, pendingReplies) {
263
+ const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, actorId, inspector, system, pendingReplies, transitionsPubSub) {
264
264
  const postponed = [];
265
265
  const hasPostponeRules = machine.postponeRules.length > 0;
266
266
  const processQueued = Effect.fn("effect-machine.actor.processQueued")(function* (queued) {
@@ -276,6 +276,7 @@ const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine
276
276
  lifecycleRan: false,
277
277
  isFinal: false,
278
278
  hasReply: false,
279
+ deferReply: false,
279
280
  reply: void 0,
280
281
  postponed: true
281
282
  };
@@ -296,13 +297,29 @@ const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine
296
297
  yield* Deferred.succeed(queued.reply, result);
297
298
  break;
298
299
  case "ask":
299
- if (result.hasReply) yield* Deferred.succeed(queued.reply, result.reply);
300
- else yield* Deferred.fail(queued.reply, new NoReplyError({
300
+ if (result.hasReply) {
301
+ const replySchema = machine._replySchemas?.get(event._tag);
302
+ if (replySchema !== void 0) {
303
+ let decoded;
304
+ try {
305
+ decoded = Schema.decodeUnknownSync(replySchema)(result.reply);
306
+ } catch (decodeError) {
307
+ yield* Deferred.die(queued.reply, decodeError);
308
+ return yield* Effect.die(decodeError);
309
+ }
310
+ yield* Deferred.succeed(queued.reply, decoded);
311
+ } else yield* Deferred.succeed(queued.reply, result.reply);
312
+ } else yield* Deferred.fail(queued.reply, new NoReplyError({
301
313
  actorId,
302
314
  eventTag: event._tag
303
315
  }));
304
316
  break;
305
317
  }
318
+ if (result.transitioned) yield* PubSub.publish(transitionsPubSub, {
319
+ fromState: result.previousState,
320
+ toState: result.newState,
321
+ event
322
+ });
306
323
  return {
307
324
  shouldStop,
308
325
  stateChanged: result.lifecycleRan
@@ -318,15 +335,21 @@ const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine
318
335
  yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
319
336
  return;
320
337
  }
321
- if (stateChanged && postponed.length > 0) {
338
+ let drainTriggered = stateChanged;
339
+ while (drainTriggered && postponed.length > 0) {
340
+ drainTriggered = false;
322
341
  const drained = postponed.splice(0);
323
- for (const entry of drained) if ((yield* processQueued(entry)).shouldStop) {
324
- yield* Ref.set(stoppedRef, true);
325
- settlePostponedBuffer(postponed, pendingReplies, actorId);
326
- yield* settlePendingReplies(pendingReplies, actorId);
327
- yield* Scope.close(stateScopeRef.current, Exit.void);
328
- yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
329
- return;
342
+ for (const entry of drained) {
343
+ const drain = yield* processQueued(entry);
344
+ if (drain.shouldStop) {
345
+ yield* Ref.set(stoppedRef, true);
346
+ settlePostponedBuffer(postponed, pendingReplies, actorId);
347
+ yield* settlePendingReplies(pendingReplies, actorId);
348
+ yield* Scope.close(stateScopeRef.current, Exit.void);
349
+ yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
350
+ return;
351
+ }
352
+ if (drain.stateChanged) drainTriggered = true;
330
353
  }
331
354
  }
332
355
  }
@@ -482,25 +505,7 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
482
505
  if (MutableHashMap.has(actorsMap, id)) return yield* new DuplicateActorError({ actorId: id });
483
506
  return yield* registerActor(id, yield* createActor(id, built._inner));
484
507
  });
485
- const spawnPersistent = Effect.fn("effect-machine.actorSystem.spawnPersistent")(function* (id, persistentMachine) {
486
- if (MutableHashMap.has(actorsMap, id)) return yield* new DuplicateActorError({ actorId: id });
487
- const adapter = yield* PersistenceAdapterTag;
488
- const maybeSnapshot = yield* adapter.loadSnapshot(id, persistentMachine.persistence.stateSchema);
489
- return yield* registerActor(id, yield* createPersistentActor(id, persistentMachine, maybeSnapshot, yield* adapter.loadEvents(id, persistentMachine.persistence.eventSchema, Option.isSome(maybeSnapshot) ? maybeSnapshot.value.version : void 0)));
490
- });
491
- const spawnImpl = Effect.fn("effect-machine.actorSystem.spawn")(function* (id, machine) {
492
- if (isPersistentMachine(machine)) return yield* spawnPersistent(id, machine);
493
- return yield* spawnRegular(id, machine);
494
- });
495
- function spawn(id, machine) {
496
- return withSpawnGate(spawnImpl(id, machine));
497
- }
498
- const restoreImpl = Effect.fn("effect-machine.actorSystem.restore")(function* (id, persistentMachine) {
499
- const maybeActor = yield* restorePersistentActor(id, persistentMachine);
500
- if (Option.isSome(maybeActor)) yield* registerActor(id, maybeActor.value);
501
- return maybeActor;
502
- });
503
- const restore = (id, persistentMachine) => withSpawnGate(restoreImpl(id, persistentMachine));
508
+ const spawn = (id, machine) => withSpawnGate(spawnRegular(id, machine));
504
509
  const get = Effect.fn("effect-machine.actorSystem.get")(function* (id) {
505
510
  return yield* Effect.sync(() => MutableHashMap.get(actorsMap, id));
506
511
  });
@@ -517,55 +522,8 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
517
522
  yield* actor.stop;
518
523
  return true;
519
524
  });
520
- const listPersisted = Effect.fn("effect-machine.actorSystem.listPersisted")(function* () {
521
- const adapter = yield* PersistenceAdapterTag;
522
- if (adapter.listActors === void 0) return [];
523
- return yield* adapter.listActors();
524
- });
525
- const restoreMany = Effect.fn("effect-machine.actorSystem.restoreMany")(function* (ids, persistentMachine) {
526
- const restored = [];
527
- const failed = [];
528
- for (const id of ids) {
529
- if (MutableHashMap.has(actorsMap, id)) continue;
530
- const result = yield* Effect.either(restore(id, persistentMachine));
531
- if (result._tag === "Left") failed.push({
532
- id,
533
- error: result.left
534
- });
535
- else if (Option.isSome(result.right)) restored.push(result.right.value);
536
- else failed.push({
537
- id,
538
- error: new PersistenceError({
539
- operation: "restore",
540
- actorId: id,
541
- message: "No persisted state found"
542
- })
543
- });
544
- }
545
- return {
546
- restored,
547
- failed
548
- };
549
- });
550
- const restoreAll = Effect.fn("effect-machine.actorSystem.restoreAll")(function* (persistentMachine, options) {
551
- const adapter = yield* PersistenceAdapterTag;
552
- if (adapter.listActors === void 0) return {
553
- restored: [],
554
- failed: []
555
- };
556
- const machineType = persistentMachine.persistence.machineType;
557
- if (machineType === void 0) return yield* new PersistenceError({
558
- operation: "restoreAll",
559
- actorId: "*",
560
- message: "restoreAll requires explicit machineType in persistence config"
561
- });
562
- let filtered = (yield* adapter.listActors()).filter((meta) => meta.machineType === machineType);
563
- if (options?.filter !== void 0) filtered = filtered.filter(options.filter);
564
- return yield* restoreMany(filtered.map((meta) => meta.id), persistentMachine);
565
- });
566
525
  return ActorSystem.of({
567
526
  spawn,
568
- restore,
569
527
  get,
570
528
  stop,
571
529
  events: Stream.fromPubSub(eventPubSub),
@@ -581,10 +539,7 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
581
539
  return () => {
582
540
  eventListeners.delete(fn);
583
541
  };
584
- },
585
- listPersisted,
586
- restoreMany,
587
- restoreAll
542
+ }
588
543
  });
589
544
  });
590
545
  /**
@@ -0,0 +1,15 @@
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 };
@@ -0,0 +1,62 @@
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 };
@@ -0,0 +1,49 @@
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 };
@@ -0,0 +1,19 @@
1
+ //#region src/cluster/entity-actor-ref.ts
2
+ /**
3
+ * Create an EntityActorRef from a test client factory and entity ID.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * const makeClient = yield* Entity.makeTestClient(entity, entityLayer)
8
+ * const ref = yield* makeEntityActorRef(makeClient, "order-123")
9
+ * yield* ref.send(OrderEvent.Process)
10
+ * ```
11
+ */
12
+ const makeEntityActorRef = (client, entityId) => ({
13
+ entityId,
14
+ send: (event) => client.Send({ event }),
15
+ ask: ((event) => client.Ask({ event })),
16
+ snapshot: client.GetState()
17
+ });
18
+ //#endregion
19
+ 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/cluster";
6
6
  import { Rpc } from "@effect/rpc";
7
7
 
@@ -13,77 +13,62 @@ 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.toLayer.
25
+ */
26
+ readonly maxIdleTime?: Duration.DurationInput;
27
+ /**
28
+ * Concurrency for handler execution.
29
+ * Forwarded to Entity.toLayer.
30
+ */
31
+ readonly concurrency?: number | "unbounded";
32
+ /**
33
+ * Mailbox capacity. Default: "unbounded".
34
+ * Forwarded to Entity.toLayer.
35
+ */
36
+ readonly mailboxCapacity?: number | "unbounded";
37
+ /**
38
+ * Disable fatal defects (defects won't crash the entity activation).
39
+ * Forwarded to Entity.toLayer.
40
+ */
41
+ readonly disableFatalDefects?: boolean;
42
+ /**
43
+ * Retry policy for defects (schedule for restarting after defect).
44
+ * Forwarded to Entity.toLayer.
45
+ */
46
+ readonly defectRetryPolicy?: Schedule.Schedule<any, unknown>;
47
+ /**
48
+ * Persistence configuration. When set, requires PersistenceAdapter in R.
49
+ */
50
+ readonly persistence?: EntityPersistenceConfig;
44
51
  }
45
52
  /**
46
53
  * Create an Entity layer that wires a machine to handle RPC calls.
47
54
  *
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
55
+ * v3: Uses `Entity.toLayer` with handler objects backed by the runtime kernel.
54
56
  *
55
57
  * @example
56
58
  * ```ts
57
- * const OrderEntity = toEntity(orderMachine, {
58
- * type: "Order",
59
- * stateSchema: OrderState,
60
- * eventSchema: OrderEvent,
61
- * })
59
+ * const OrderEntity = toEntity(orderMachine, { type: "Order" })
62
60
  *
63
61
  * const OrderEntityLayer = EntityMachine.layer(OrderEntity, orderMachine, {
64
62
  * initializeState: (entityId) => OrderState.Pending({ orderId: entityId }),
65
63
  * })
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
64
  * ```
73
65
  */
74
66
  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
67
  layer: <S extends {
83
68
  readonly _tag: string;
84
69
  }, E extends {
85
70
  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>;
71
+ }, 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
72
  };
88
73
  //#endregion
89
74
  export { EntityMachine, EntityMachineOptions };