effect-machine 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/actor.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { ActorStoppedError, DuplicateActorError } from "./errors.js";
2
- import { resolveTransition } from "./internal/transition.js";
2
+ import { resolveTransition, resolveTransitionEffect } from "./internal/transition.js";
3
3
  import { emitWithTimestamp, makeInspectionHooks } from "./internal/inspection.js";
4
4
  import { Inspector } from "./inspection.js";
5
+ import { ActorExit } from "./supervision.js";
5
6
  import { createRuntime } from "./internal/runtime.js";
6
- import { Context, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, PubSub, Queue, Ref, Schedule, Scope, Semaphore, Stream, SubscriptionRef } from "effect";
7
+ import { Cause, Context, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, PubSub, Queue, Ref, Schedule, Scope, Semaphore, Stream, SubscriptionRef } from "effect";
7
8
  //#region src/actor.ts
8
9
  /**
9
10
  * Actor system: spawning, lifecycle, and event processing.
@@ -35,11 +36,20 @@ const notifyListeners = (listeners, state) => {
35
36
  listener(state);
36
37
  } catch {}
37
38
  };
39
+ const toActorExit = (machine, exit) => {
40
+ if (exit._tag === "Final") return ActorExit.Final(exit.state, machine._output(exit.state));
41
+ if (exit._tag === "Defect") return {
42
+ _tag: "Defect",
43
+ cause: exit.cause,
44
+ phase: exit.phase
45
+ };
46
+ return { _tag: "Stopped" };
47
+ };
38
48
  /**
39
49
  * Build core ActorRef methods.
40
50
  */
41
- const buildActorRefCore = (cell, stop, start) => {
42
- const { id, machine, stateRef, runtimeRef, listeners, system } = cell;
51
+ const buildActorRefCore = (cell, stop, start, serviceContext) => {
52
+ const { id, machine, stateRef, runtimeRef, listeners, system, lifecycleRef, latestTransitionRef } = cell;
43
53
  const send = (event) => Effect.gen(function* () {
44
54
  const runtime = runtimeRef.current;
45
55
  if (runtime !== void 0) yield* runtime.send(event);
@@ -59,7 +69,8 @@ const buildActorRefCore = (cell, stop, start) => {
59
69
  reply: void 0,
60
70
  postponed: false,
61
71
  lifecycleRan: false,
62
- isFinal: machine._isFinal(currentState._tag)
72
+ isFinal: machine._isFinal(currentState._tag),
73
+ transitions: []
63
74
  };
64
75
  });
65
76
  const call = (event) => Effect.suspend(() => {
@@ -76,10 +87,11 @@ const buildActorRefCore = (cell, stop, start) => {
76
87
  const matches = Effect.fn("effect-machine.actor.matches")(function* (tag) {
77
88
  return (yield* SubscriptionRef.get(stateRef))._tag === tag;
78
89
  });
79
- const can = Effect.fn("effect-machine.actor.can")(function* (event) {
90
+ const canEffect = Effect.fn("effect-machine.actor.can")(function* (event) {
80
91
  const state = yield* SubscriptionRef.get(stateRef);
81
- return resolveTransition(machine, state, event) !== void 0;
92
+ return (yield* resolveTransitionEffect(machine, state, event)) !== void 0;
82
93
  });
94
+ const can = (event) => canEffect(event).pipe(Effect.provide(serviceContext));
83
95
  const waitFor = Effect.fn("effect-machine.actor.waitFor")(function* (predicateOrState) {
84
96
  let predicate;
85
97
  if (typeof predicateOrState === "function" && !("_tag" in predicateOrState)) predicate = predicateOrState;
@@ -101,18 +113,53 @@ const buildActorRefCore = (cell, stop, start) => {
101
113
  return result;
102
114
  });
103
115
  const awaitFinal = waitFor((state) => machine._isFinal(state._tag)).pipe(Effect.withSpan("effect-machine.actor.awaitFinal"));
116
+ const awaitOutput = Deferred.await(cell.terminalExitDeferred).pipe(Effect.flatMap((exit) => {
117
+ if (exit._tag === "Final") return Effect.succeed(exit.output);
118
+ if (exit._tag === "Defect") return Effect.die(Cause.squash(exit.cause));
119
+ return ActorStoppedError.make({ actorId: id });
120
+ }), Effect.withSpan("effect-machine.actor.awaitOutput"));
104
121
  const sendAndWait = Effect.fn("effect-machine.actor.sendAndWait")(function* (event, predicateOrState) {
105
122
  yield* send(event);
106
123
  if (predicateOrState !== void 0) return yield* waitFor(predicateOrState);
107
124
  return yield* awaitFinal;
108
125
  });
109
126
  const transitions = Stream.fromPubSub(cell.transitions);
127
+ const subscribe = (listener) => {
128
+ listeners.add(listener);
129
+ return () => {
130
+ listeners.delete(listener);
131
+ };
132
+ };
133
+ const sendClient = (event) => {
134
+ runtimeRef.current?.sendSync(event);
135
+ };
136
+ const stopClient = () => {
137
+ Effect.runFork(stop);
138
+ };
139
+ const getSnapshot = () => Effect.runSync(SubscriptionRef.get(stateRef));
140
+ const matchesClient = (tag) => getSnapshot()._tag === tag;
141
+ const canSync = (event) => resolveTransition(machine, getSnapshot(), event) !== void 0;
142
+ const getLifecycle = () => Effect.runSync(SubscriptionRef.get(lifecycleRef));
143
+ const getLatestTransition = () => Effect.runSync(SubscriptionRef.get(latestTransitionRef));
144
+ const client = {
145
+ send: sendClient,
146
+ stop: stopClient,
147
+ getSnapshot,
148
+ matches: matchesClient,
149
+ canSync,
150
+ can: (event) => Effect.runPromise(can(event)),
151
+ getLifecycle,
152
+ getLatestTransition,
153
+ subscribe
154
+ };
110
155
  return {
111
156
  id,
112
157
  send,
113
158
  call,
114
159
  ask,
115
160
  state: stateRef,
161
+ lifecycle: lifecycleRef,
162
+ latestTransition: latestTransitionRef,
116
163
  stop,
117
164
  start,
118
165
  snapshot,
@@ -122,26 +169,20 @@ const buildActorRefCore = (cell, stop, start) => {
122
169
  transitions,
123
170
  waitFor,
124
171
  awaitFinal,
172
+ awaitOutput,
125
173
  sendAndWait,
126
- subscribe: (fn) => {
127
- listeners.add(fn);
128
- return () => {
129
- listeners.delete(fn);
130
- };
131
- },
174
+ subscribe,
175
+ client,
132
176
  awaitExit: Deferred.await(cell.terminalExitDeferred),
133
177
  drain: Effect.suspend(() => runtimeRef.current?.drain ?? Effect.void),
134
178
  sync: {
135
- send: (event) => {
136
- runtimeRef.current?.sendSync(event);
137
- },
138
- stop: () => Effect.runFork(stop),
139
- snapshot: () => Effect.runSync(SubscriptionRef.get(stateRef)),
140
- matches: (tag) => Effect.runSync(SubscriptionRef.get(stateRef))._tag === tag,
141
- can: (event) => {
142
- const state = Effect.runSync(SubscriptionRef.get(stateRef));
143
- return resolveTransition(machine, state, event) !== void 0;
144
- }
179
+ send: sendClient,
180
+ stop: stopClient,
181
+ snapshot: getSnapshot,
182
+ matches: matchesClient,
183
+ can: canSync,
184
+ lifecycle: getLifecycle,
185
+ latestTransition: getLatestTransition
145
186
  },
146
187
  system,
147
188
  children: cell.children
@@ -175,7 +216,7 @@ const runSupervisionLoop = (cell, options) => Effect.gen(function* () {
175
216
  if (currentRuntime === void 0) return;
176
217
  const generationExit = yield* Deferred.await(currentRuntime.exitDeferred);
177
218
  if (generationExit._tag !== "Defect") {
178
- yield* Deferred.succeed(cell.terminalExitDeferred, generationExit);
219
+ yield* Deferred.succeed(cell.terminalExitDeferred, toActorExit(cell.machine, generationExit));
179
220
  return;
180
221
  }
181
222
  if (options.supervision.shouldRestart !== void 0 && !options.supervision.shouldRestart(generationExit)) {
@@ -188,26 +229,33 @@ const runSupervisionLoop = (cell, options) => Effect.gen(function* () {
188
229
  }
189
230
  const nextGeneration = cell.generation.current + 1;
190
231
  cell.generation.current = nextGeneration;
191
- let restartState = cell.machine.initial;
232
+ let restartState = cell.machineInitial;
192
233
  if (options.lifecycle?.recovery !== void 0) {
193
234
  const resolved = yield* options.lifecycle.recovery.resolve({
194
235
  actorId: cell.id,
195
236
  generation: nextGeneration,
196
- machineInitial: cell.machine.initial
237
+ machineInitial: cell.machineInitial
197
238
  });
198
239
  if (Option.isSome(resolved)) restartState = resolved.value;
199
240
  }
200
241
  yield* currentRuntime.settlePendingRequests;
242
+ yield* SubscriptionRef.set(cell.lifecycleRef, {
243
+ _tag: "Starting",
244
+ generation: nextGeneration
245
+ });
201
246
  const freshQueue = yield* Queue.unbounded();
202
247
  yield* Ref.set(cell.eventQueueRef, freshQueue);
203
248
  yield* SubscriptionRef.set(cell.stateRef, restartState);
204
249
  yield* Ref.set(cell.stoppedRef, false);
205
250
  cell.children.clear();
206
- let machineForRestart = cell.machine;
207
- if (restartState !== cell.machine.initial) machineForRestart = cell.machine._withInitial(restartState);
208
- const newRuntime = yield* options.spawnGeneration(machineForRestart);
251
+ const newRuntime = yield* options.spawnGeneration(cell.machine);
209
252
  cell.runtimeRef.current = newRuntime;
210
253
  yield* newRuntime.start;
254
+ const restartExit = yield* Deferred.poll(newRuntime.exitDeferred);
255
+ if (Option.isNone(restartExit)) yield* SubscriptionRef.set(cell.lifecycleRef, {
256
+ _tag: "Active",
257
+ generation: nextGeneration
258
+ });
211
259
  if (options.onRestart !== void 0) yield* options.onRestart(nextGeneration, generationExit);
212
260
  notifyListeners(cell.listeners, restartState);
213
261
  }
@@ -217,9 +265,9 @@ const runSupervisionLoop = (cell, options) => Effect.gen(function* () {
217
265
  * Delegates to the shared runtime kernel with actor-specific lifecycle hooks.
218
266
  */
219
267
  const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machine, options) {
220
- const lifecycle = options?.lifecycle;
268
+ const lifecycle = options.lifecycle;
221
269
  const serviceContext = yield* Effect.context();
222
- const initial = options?.initialState ?? machine.initial;
270
+ const initial = options.initialState;
223
271
  yield* Effect.annotateCurrentSpan("effect_machine.actor.id", id);
224
272
  yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", initial._tag);
225
273
  const { system, implicitSystemScope } = yield* resolveActorSystem();
@@ -227,22 +275,24 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
227
275
  const childrenMap = /* @__PURE__ */ new Map();
228
276
  const listeners = /* @__PURE__ */ new Set();
229
277
  const transitionsPubSub = yield* PubSub.unbounded();
230
- let hooks = void 0;
231
- if (inspectorValue !== void 0) hooks = makeInspectionHooks(id, inspectorValue);
232
- let machineWithState = machine;
233
- if (initial !== machine.initial) machineWithState = machine._withInitial(initial);
278
+ const generation = { current: 0 };
279
+ const inspectionHooks = (runtimeGeneration) => {
280
+ if (inspectorValue === void 0) return void 0;
281
+ return makeInspectionHooks(id, inspectorValue, () => runtimeGeneration);
282
+ };
234
283
  const stateRef = yield* SubscriptionRef.make(initial);
284
+ const lifecycleRef = yield* SubscriptionRef.make({ _tag: "Created" });
285
+ const latestTransitionRef = yield* SubscriptionRef.make(void 0);
235
286
  const stoppedRef = yield* Ref.make(false);
236
287
  const initialQueue = yield* Queue.unbounded();
237
288
  const eventQueueRef = yield* Ref.make(initialQueue);
238
289
  const terminalExitDeferred = yield* Deferred.make();
239
- let stopEmitted = false;
240
- const generation = { current: 0 };
241
290
  const runtimeRef = { current: void 0 };
242
291
  const supervisorFiberRef = { current: void 0 };
243
292
  const cell = {
244
293
  id,
245
294
  machine,
295
+ machineInitial: options.machineInitial,
246
296
  stateRef,
247
297
  stoppedRef,
248
298
  eventQueueRef,
@@ -251,16 +301,19 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
251
301
  listeners,
252
302
  children: childrenMap,
253
303
  transitions: transitionsPubSub,
304
+ lifecycleRef,
305
+ latestTransitionRef,
254
306
  system,
255
307
  generation
256
308
  };
257
309
  /** Build lifecycle hooks for a generation */
258
- const buildRuntimeLifecycle = () => {
259
- stopEmitted = false;
310
+ const buildRuntimeLifecycle = (runtimeGeneration) => {
311
+ let stopEmitted = false;
260
312
  let onEvent = void 0;
261
313
  if (inspectorValue !== void 0) onEvent = (state, event) => emitWithTimestamp(inspectorValue, (timestamp) => ({
262
314
  type: "@machine.event",
263
315
  actorId: id,
316
+ generation: runtimeGeneration,
264
317
  state,
265
318
  event,
266
319
  timestamp
@@ -271,6 +324,7 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
271
324
  yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
272
325
  type: "@machine.stop",
273
326
  actorId: id,
327
+ generation: runtimeGeneration,
274
328
  finalState: state,
275
329
  timestamp
276
330
  }));
@@ -279,32 +333,39 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
279
333
  if (inspectorValue !== void 0) onInitialSpawnEffects = (state) => emitWithTimestamp(inspectorValue, (timestamp) => ({
280
334
  type: "@machine.effect",
281
335
  actorId: id,
336
+ generation: runtimeGeneration,
282
337
  effectType: "spawn",
283
338
  state,
284
339
  timestamp
285
340
  }));
286
341
  return {
287
342
  onEvent,
288
- onStateChange: (result, event) => {
343
+ onStateChange: (result, event) => Effect.gen(function* () {
344
+ const latest = result.transitions.at(-1);
345
+ if (latest !== void 0) yield* SubscriptionRef.set(latestTransitionRef, {
346
+ fromState: latest.previousState,
347
+ toState: latest.newState,
348
+ event: latest.event
349
+ });
289
350
  notifyListeners(listeners, result.newState);
290
351
  const durability = lifecycle?.durability;
291
352
  if (durability === void 0 || !result.transitioned) return;
292
353
  if (!(durability.shouldSave === void 0 || durability.shouldSave(result.newState, result.previousState))) return;
293
- return durability.save({
354
+ yield* durability.save({
294
355
  actorId: id,
295
- generation: generation.current,
356
+ generation: runtimeGeneration,
296
357
  previousState: result.previousState,
297
358
  nextState: result.newState,
298
359
  event
299
360
  });
300
- },
301
- onProcessed: (result, event) => {
361
+ }),
362
+ onProcessed: (result, _event) => {
302
363
  if (!result.transitioned || transitionsPubSub.subscribers.size === 0) return;
303
- return PubSub.publish(transitionsPubSub, {
304
- fromState: result.previousState,
305
- toState: result.newState,
306
- event
307
- }).pipe(Effect.asVoid);
364
+ return Effect.forEach(result.transitions, (transition) => PubSub.publish(transitionsPubSub, {
365
+ fromState: transition.previousState,
366
+ toState: transition.newState,
367
+ event: transition.event
368
+ }), { discard: true });
308
369
  },
309
370
  onFinal,
310
371
  onShutdown: () => Effect.gen(function* () {
@@ -313,6 +374,7 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
313
374
  yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
314
375
  type: "@machine.stop",
315
376
  actorId: id,
377
+ generation: runtimeGeneration,
316
378
  finalState,
317
379
  timestamp
318
380
  }));
@@ -322,45 +384,52 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
322
384
  };
323
385
  };
324
386
  /** Create a single runtime generation. machineForGen is machineWithState for initial, machine for restarts. */
325
- const spawnGeneration = (machineForGen) => Ref.get(eventQueueRef).pipe(Effect.flatMap((currentQueue) => createRuntime(machineForGen, system, {
326
- actorId: id,
327
- hooks,
328
- skipFinalizer: true,
329
- cellResources: {
330
- stateRef,
331
- stoppedRef,
332
- eventQueue: currentQueue
333
- },
334
- lifecycle: buildRuntimeLifecycle(),
335
- onChildSpawned: (childId, child) => Effect.gen(function* () {
336
- childrenMap.set(childId, child);
337
- const maybeScope = yield* Effect.serviceOption(Scope.Scope);
338
- if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, Effect.sync(() => {
339
- childrenMap.delete(childId);
340
- }));
341
- })
342
- })));
343
- runtimeRef.current = yield* spawnGeneration(machineWithState);
344
- const supervision = options?.supervision;
387
+ const spawnGeneration = (machineForGen) => {
388
+ const runtimeGeneration = generation.current;
389
+ return Ref.get(eventQueueRef).pipe(Effect.flatMap((currentQueue) => createRuntime(machineForGen, system, {
390
+ actorId: id,
391
+ generation: runtimeGeneration,
392
+ hooks: inspectionHooks(runtimeGeneration),
393
+ skipFinalizer: true,
394
+ cellResources: {
395
+ stateRef,
396
+ stoppedRef,
397
+ eventQueue: currentQueue
398
+ },
399
+ lifecycle: buildRuntimeLifecycle(runtimeGeneration),
400
+ onChildSpawned: (childId, child) => Effect.gen(function* () {
401
+ childrenMap.set(childId, child);
402
+ const maybeScope = yield* Effect.serviceOption(Scope.Scope);
403
+ if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, Effect.sync(() => {
404
+ childrenMap.delete(childId);
405
+ }));
406
+ })
407
+ })));
408
+ };
409
+ runtimeRef.current = yield* spawnGeneration(machine);
410
+ const supervision = options.supervision;
345
411
  const stop = Effect.fn("effect-machine.actor.stop")(function* () {
346
412
  if (supervisorFiberRef.current !== void 0) yield* Fiber.interrupt(supervisorFiberRef.current);
347
413
  const currentRuntime = runtimeRef.current;
348
414
  if (currentRuntime !== void 0) yield* currentRuntime.stop;
349
- yield* Deferred.succeed(terminalExitDeferred, { _tag: "Stopped" });
415
+ yield* Deferred.succeed(terminalExitDeferred, ActorExit.Stopped);
350
416
  if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
351
417
  })().pipe(Effect.provide(serviceContext), Effect.asVoid);
352
- const isHydrated = options?.initialState !== void 0;
418
+ const isHydrated = options.hydrated === true;
353
419
  const start = Effect.fn("effect-machine.actor.start")(function* () {
420
+ yield* SubscriptionRef.set(lifecycleRef, {
421
+ _tag: "Starting",
422
+ generation: generation.current
423
+ });
354
424
  if (lifecycle?.recovery !== void 0 && !isHydrated) {
355
425
  const resolved = yield* lifecycle.recovery.resolve({
356
426
  actorId: id,
357
427
  generation: generation.current,
358
- machineInitial: machine.initial
428
+ machineInitial: options.machineInitial
359
429
  });
360
430
  if (Option.isSome(resolved)) {
361
431
  yield* SubscriptionRef.set(stateRef, resolved.value);
362
- const recoveredMachine = machine._withInitial(resolved.value);
363
- const newRuntime = yield* spawnGeneration(recoveredMachine);
432
+ const newRuntime = yield* spawnGeneration(machine);
364
433
  runtimeRef.current = newRuntime;
365
434
  }
366
435
  }
@@ -368,6 +437,7 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
368
437
  yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
369
438
  type: "@machine.spawn",
370
439
  actorId: id,
440
+ generation: generation.current,
371
441
  initialState: currentState,
372
442
  timestamp
373
443
  }));
@@ -375,16 +445,24 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
375
445
  supervision,
376
446
  spawnGeneration,
377
447
  lifecycle,
378
- onRestart: options?.onRestart
448
+ onRestart: options.onRestart
379
449
  }));
380
450
  else {
381
451
  const currentRuntime = runtimeRef.current;
382
- if (currentRuntime !== void 0) yield* Effect.forkDetach(Deferred.await(currentRuntime.exitDeferred).pipe(Effect.tap((exit) => Deferred.succeed(terminalExitDeferred, exit))));
452
+ if (currentRuntime !== void 0) yield* Effect.forkDetach(Deferred.await(currentRuntime.exitDeferred).pipe(Effect.tap((exit) => Deferred.succeed(terminalExitDeferred, toActorExit(machine, exit)))));
383
453
  }
384
454
  const currentRuntime = runtimeRef.current;
385
- if (currentRuntime !== void 0) yield* currentRuntime.start;
455
+ if (currentRuntime !== void 0) {
456
+ yield* currentRuntime.start;
457
+ const currentExit = yield* Deferred.poll(currentRuntime.exitDeferred);
458
+ if (Option.isNone(currentExit)) yield* SubscriptionRef.set(lifecycleRef, {
459
+ _tag: "Active",
460
+ generation: generation.current
461
+ });
462
+ }
386
463
  })().pipe(Effect.provide(serviceContext), Effect.asVoid);
387
- return buildActorRefCore(cell, stop, start);
464
+ yield* Effect.forkDetach(Deferred.await(terminalExitDeferred).pipe(Effect.flatMap((exit) => SubscriptionRef.set(lifecycleRef, exit)), Effect.provide(serviceContext)));
465
+ return buildActorRefCore(cell, stop, start, serviceContext);
388
466
  });
389
467
  /** Notify all system event listeners (sync). */
390
468
  const notifySystemListeners = (listeners, event) => {
@@ -418,6 +496,17 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
418
496
  id,
419
497
  actor: actorRef
420
498
  });
499
+ yield* Effect.forkDetach(actorRef.awaitExit.pipe(Effect.flatMap((exit) => {
500
+ const registered = MutableHashMap.get(actorsMap, id);
501
+ if (Option.isNone(registered) || registered.value !== actorRef) return Effect.void;
502
+ MutableHashMap.remove(actorsMap, id);
503
+ return emitSystemEvent({
504
+ _tag: "ActorStopped",
505
+ id,
506
+ actor: actorRef,
507
+ exit
508
+ });
509
+ })));
421
510
  const maybeScope = yield* Effect.serviceOption(ActorScope);
422
511
  if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, Effect.gen(function* () {
423
512
  if (MutableHashMap.has(actorsMap, id)) {
@@ -448,7 +537,12 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
448
537
  exit
449
538
  });
450
539
  };
540
+ const machineInitial = machine._initial(spawnOptions?.input);
541
+ const initialState = spawnOptions?.hydrate ?? machineInitial;
451
542
  const actor = yield* createActor(id, machine, {
543
+ initialState,
544
+ machineInitial,
545
+ hydrated: spawnOptions?.hydrate !== void 0,
452
546
  supervision: spawnOptions?.supervision,
453
547
  lifecycle: spawnOptions?.lifecycle,
454
548
  onRestart
package/dist/atom.d.ts ADDED
@@ -0,0 +1,56 @@
1
+ import { ActorLifecycle, ActorRef, TransitionInfo } from "./actor.js";
2
+ import * as Atom from "effect/unstable/reactivity/Atom";
3
+ import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
4
+ //#region src/atom.d.ts
5
+ /**
6
+ * A writable Atom projection of an actor.
7
+ *
8
+ * The Atom value is the current actor state. Atom writes send actor events.
9
+ */
10
+ type ActorAtom<State, Event> = Atom.Writable<State, Event>;
11
+ /**
12
+ * Make a writable Atom from an actor.
13
+ *
14
+ * The actor stays the single state owner. The Atom follows recovery,
15
+ * supervision restarts, and normal transitions through the actor's
16
+ * SubscriptionRef.
17
+ */
18
+ declare const make: <State extends {
19
+ readonly _tag: string;
20
+ }, Event, Output>(actor: ActorRef<State, Event, Output>) => ActorAtom<State, Event>;
21
+ /**
22
+ * Select part of an actor state.
23
+ *
24
+ * The selected Atom stays writable. Writes still send events to the actor.
25
+ * The equality function controls when Atom subscribers receive a new value.
26
+ */
27
+ declare const select: {
28
+ <State, Selection>(selector: (state: State) => Selection, equals?: (value: Selection, next: Selection) => boolean): <Event>(self: ActorAtom<State, Event>) => ActorAtom<Selection, Event>;
29
+ <State, Event, Selection>(self: ActorAtom<State, Event>, selector: (state: State) => Selection, equals?: (value: Selection, next: Selection) => boolean): ActorAtom<Selection, Event>;
30
+ };
31
+ /** Observe actor lifecycle without coupling it to domain state. */
32
+ declare const lifecycle: <State extends {
33
+ readonly _tag: string;
34
+ }, Event, Output>(actor: ActorRef<State, Event, Output>) => Atom.Atom<ActorLifecycle<State, Output>>;
35
+ /** Observe the latest accepted edge. The value remains after actor exit. */
36
+ declare const latestTransition: <State extends {
37
+ readonly _tag: string;
38
+ }, Event, Output>(actor: ActorRef<State, Event, Output>) => Atom.Atom<TransitionInfo<State, Event> | undefined>;
39
+ /** A reactive result for whether an actor can accept one event. */
40
+ type CanAtom = Atom.Atom<AsyncResult.AsyncResult<boolean>>;
41
+ /**
42
+ * Observe whether an event has an enabled transition.
43
+ *
44
+ * The Atom reevaluates after each actor state change. It supports pure and
45
+ * Effect predicates. Effect predicates use the context captured by the actor.
46
+ */
47
+ declare const can: {
48
+ <Event>(event: Event): <State extends {
49
+ readonly _tag: string;
50
+ }, Output>(actor: ActorRef<State, Event, Output>) => CanAtom;
51
+ <State extends {
52
+ readonly _tag: string;
53
+ }, Event, Output>(actor: ActorRef<State, Event, Output>, event: Event): CanAtom;
54
+ };
55
+ //#endregion
56
+ export { ActorAtom, CanAtom, can, latestTransition, lifecycle, make, select };
package/dist/atom.js ADDED
@@ -0,0 +1,47 @@
1
+ import { dual } from "effect/Function";
2
+ import * as Atom from "effect/unstable/reactivity/Atom";
3
+ import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
4
+ //#region src/atom.ts
5
+ /**
6
+ * Effect Atom integration for actors.
7
+ *
8
+ * The adapter keeps the actor as the state owner. Atom registries observe the
9
+ * actor's SubscriptionRef and write events through its synchronous boundary.
10
+ */
11
+ /**
12
+ * Make a writable Atom from an actor.
13
+ *
14
+ * The actor stays the single state owner. The Atom follows recovery,
15
+ * supervision restarts, and normal transitions through the actor's
16
+ * SubscriptionRef.
17
+ */
18
+ const make = (actor) => {
19
+ const state = Atom.subscriptionRef(actor.state);
20
+ return Atom.writable((get) => get(state), (_ctx, event) => actor.client.send(event));
21
+ };
22
+ /**
23
+ * Select part of an actor state.
24
+ *
25
+ * The selected Atom stays writable. Writes still send events to the actor.
26
+ * The equality function controls when Atom subscribers receive a new value.
27
+ */
28
+ const select = dual((args) => Atom.isAtom(args[0]), (self, selector, equals = Object.is) => Atom.withEquality(Atom.map(self, selector), equals));
29
+ /** Observe actor lifecycle without coupling it to domain state. */
30
+ const lifecycle = (actor) => Atom.subscriptionRef(actor.lifecycle);
31
+ /** Observe the latest accepted edge. The value remains after actor exit. */
32
+ const latestTransition = (actor) => Atom.subscriptionRef(actor.latestTransition);
33
+ /**
34
+ * Observe whether an event has an enabled transition.
35
+ *
36
+ * The Atom reevaluates after each actor state change. It supports pure and
37
+ * Effect predicates. Effect predicates use the context captured by the actor.
38
+ */
39
+ const can = dual(2, (actor, event) => {
40
+ const state = Atom.subscriptionRef(actor.state);
41
+ return Atom.make((get) => {
42
+ get(state);
43
+ return actor.can(event);
44
+ }).pipe(Atom.withEquality((value, next) => AsyncResult.isSuccess(value) && AsyncResult.isSuccess(next) && Object.is(value.value, next.value)));
45
+ });
46
+ //#endregion
47
+ export { can, latestTransition, lifecycle, make, select };
@@ -7,7 +7,7 @@ import { Rpc } from "effect/unstable/rpc";
7
7
  /**
8
8
  * Options for EntityMachine.layer
9
9
  */
10
- interface EntityMachineOptions<S> {
10
+ interface EntityMachineBaseOptions<S> {
11
11
  /**
12
12
  * Initialize state from entity ID.
13
13
  * Called once when entity is first activated.
@@ -38,6 +38,12 @@ interface EntityMachineOptions<S> {
38
38
  */
39
39
  readonly persistence?: EntityPersistenceConfig;
40
40
  }
41
+ type EntityMachineOptions<S, Input = void> = EntityMachineBaseOptions<S> & ([Input] extends [void] ? {
42
+ readonly input?: never;
43
+ } : {
44
+ /** Map the entity ID to the machine input. */
45
+ readonly input: (entityId: string) => Input;
46
+ });
41
47
  /**
42
48
  * Create an Entity layer that wires a machine to handle RPC calls.
43
49
  *
@@ -59,7 +65,7 @@ declare const EntityMachine: {
59
65
  readonly _tag: string;
60
66
  }, E extends {
61
67
  readonly _tag: string;
62
- }, R, EntityType extends string, Rpcs extends Rpc.Any>(entity: Entity.Entity<EntityType, Rpcs>, machine: Machine<S, E, R, any, any>, options?: EntityMachineOptions<S>) => Layer.Layer<never, never, R>;
68
+ }, R, Input, Output, EntityType extends string, Rpcs extends Rpc.Any>(entity: Entity.Entity<EntityType, Rpcs>, machine: Machine<S, E, R, any, any, Input, Output>, ...optionsArgument: [Input] extends [void] ? [options?: EntityMachineOptions<S, Input>] : [options: EntityMachineOptions<S, Input>]) => Layer.Layer<never, never, R>;
63
69
  };
64
70
  //#endregion
65
- export { EntityMachine, EntityMachineOptions };
71
+ export { EntityMachine, EntityMachineBaseOptions, EntityMachineOptions };
@@ -35,7 +35,8 @@ import { Entity } from "effect/unstable/cluster";
35
35
  * })
36
36
  * ```
37
37
  */
38
- const EntityMachine = { layer: (entity, machine, options) => {
38
+ const EntityMachine = { layer: (entity, machine, ...optionsArgument) => {
39
+ const options = optionsArgument[0];
39
40
  const persistence = options?.persistence;
40
41
  const build = Effect.gen(function* () {
41
42
  const entityId = yield* Effect.serviceOption(Entity.CurrentAddress).pipe(Effect.map((opt) => {
@@ -43,23 +44,22 @@ const EntityMachine = { layer: (entity, machine, options) => {
43
44
  return "";
44
45
  }));
45
46
  const inspector = Option.getOrUndefined(yield* Effect.serviceOption(Inspector));
47
+ const machineInitial = machine._initial(options?.input?.(entityId));
46
48
  const existingSystem = yield* Effect.serviceOption(ActorSystem);
47
49
  let system;
48
50
  if (Option.isSome(existingSystem)) system = existingSystem.value;
49
51
  else system = yield* makeSystem();
50
- const persistCtx = yield* hydratePersistence(persistence, entity, entityId, machine, options?.initializeState);
52
+ const persistCtx = yield* hydratePersistence(persistence, entity, entityId, machine, machineInitial, options?.initializeState);
51
53
  let initialState = persistCtx.hydratedState;
52
54
  if (initialState === void 0 && options?.initializeState !== void 0) initialState = options.initializeState(entityId);
53
- let machineWithState = machine;
54
- if (initialState !== void 0) machineWithState = machine._withInitial(initialState);
55
55
  const versionRef = yield* Ref.make(persistCtx.initialVersion);
56
- const computedInitial = initialState ?? machine.initial;
56
+ const computedInitial = initialState ?? machineInitial;
57
57
  const stateRef = yield* SubscriptionRef.make(computedInitial);
58
58
  const stoppedRef = yield* Ref.make(false);
59
59
  const eventQueue = yield* Queue.unbounded();
60
60
  let hooks = void 0;
61
61
  if (inspector !== void 0) hooks = makeInspectionHooks(entityId, inspector);
62
- const runtime = yield* createRuntime(machineWithState, system, {
62
+ const runtime = yield* createRuntime(machine, system, {
63
63
  actorId: entityId,
64
64
  hooks,
65
65
  childIdPrefix: `${entityId}/`,
@@ -152,7 +152,7 @@ const noPersistence = {
152
152
  initialVersion: 0
153
153
  };
154
154
  /** Load snapshot/journal and compute hydrated state. */
155
- const hydratePersistence = (persistence, entityDef, entityId, machine, initializeState) => Effect.gen(function* () {
155
+ const hydratePersistence = (persistence, entityDef, entityId, machine, machineInitial, initializeState) => Effect.gen(function* () {
156
156
  if (persistence === void 0) return noPersistence;
157
157
  const adapter = yield* PersistenceAdapter;
158
158
  const key = {
@@ -164,7 +164,7 @@ const hydratePersistence = (persistence, entityDef, entityId, machine, initializ
164
164
  let baseState;
165
165
  if (Option.isSome(maybeSnapshot)) baseState = maybeSnapshot.value.state;
166
166
  else if (initializeState !== void 0) baseState = initializeState(entityId);
167
- else baseState = machine.initial;
167
+ else baseState = machineInitial;
168
168
  let snapshotVersion = 0;
169
169
  if (Option.isSome(maybeSnapshot)) snapshotVersion = maybeSnapshot.value.version;
170
170
  const events = yield* adapter.loadEvents(key, snapshotVersion);
@@ -2,5 +2,5 @@ import { EntityPersistenceConfig, PersistedEvent, PersistenceAdapter, Persistenc
2
2
  import { makeInMemoryPersistenceAdapter } from "./adapters/in-memory.js";
3
3
  import { EntityRpcs, ToEntityOptions, toEntity } from "./to-entity.js";
4
4
  import { EntityActorRef, makeEntityActorRef } from "./entity-actor-ref.js";
5
- import { EntityMachine, EntityMachineOptions } from "./entity-machine.js";
6
- export { type EntityActorRef, EntityMachine, type EntityMachineOptions, type EntityPersistenceConfig, type EntityRpcs, type PersistedEvent, PersistenceAdapter, type PersistenceAdapterService as PersistenceAdapterInterface, type PersistenceKey, type Snapshot, type ToEntityOptions, makeEntityActorRef, makeInMemoryPersistenceAdapter, toEntity };
5
+ import { EntityMachine, EntityMachineBaseOptions, EntityMachineOptions } from "./entity-machine.js";
6
+ export { type EntityActorRef, EntityMachine, type EntityMachineBaseOptions, type EntityMachineOptions, type EntityPersistenceConfig, type EntityRpcs, type PersistedEvent, PersistenceAdapter, type PersistenceAdapterService as PersistenceAdapterInterface, type PersistenceKey, type Snapshot, type ToEntityOptions, makeEntityActorRef, makeInMemoryPersistenceAdapter, toEntity };