effect-machine 0.19.0 → 0.21.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.
@@ -13,6 +14,19 @@ import { Context, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option,
13
14
  * - ActorSystem service (spawn/stop/get actors)
14
15
  * - Actor creation and event loop
15
16
  */
17
+ /** A typed identity for an actor stored in an ActorSystem. */
18
+ var ActorSystemKey = class {
19
+ id;
20
+ constructor(id) {
21
+ this.id = id;
22
+ }
23
+ };
24
+ /** Create a typed ActorSystem identity. */
25
+ const actorSystemKey = (id) => new ActorSystemKey(id);
26
+ const actorSystemId = (id) => {
27
+ if (typeof id === "string") return id;
28
+ return id.id;
29
+ };
16
30
  /**
17
31
  * ActorSystem service tag
18
32
  */
@@ -35,11 +49,20 @@ const notifyListeners = (listeners, state) => {
35
49
  listener(state);
36
50
  } catch {}
37
51
  };
52
+ const toActorExit = (machine, exit) => {
53
+ if (exit._tag === "Final") return ActorExit.Final(exit.state, machine._output(exit.state));
54
+ if (exit._tag === "Defect") return {
55
+ _tag: "Defect",
56
+ cause: exit.cause,
57
+ phase: exit.phase
58
+ };
59
+ return { _tag: "Stopped" };
60
+ };
38
61
  /**
39
62
  * Build core ActorRef methods.
40
63
  */
41
- const buildActorRefCore = (cell, stop, start) => {
42
- const { id, machine, stateRef, runtimeRef, listeners, system } = cell;
64
+ const buildActorRefCore = (cell, stop, start, serviceContext) => {
65
+ const { id, machine, stateRef, runtimeRef, listeners, system, lifecycleRef, latestTransitionRef } = cell;
43
66
  const send = (event) => Effect.gen(function* () {
44
67
  const runtime = runtimeRef.current;
45
68
  if (runtime !== void 0) yield* runtime.send(event);
@@ -59,7 +82,8 @@ const buildActorRefCore = (cell, stop, start) => {
59
82
  reply: void 0,
60
83
  postponed: false,
61
84
  lifecycleRan: false,
62
- isFinal: machine._isFinal(currentState._tag)
85
+ isFinal: machine._isFinal(currentState._tag),
86
+ transitions: []
63
87
  };
64
88
  });
65
89
  const call = (event) => Effect.suspend(() => {
@@ -76,10 +100,11 @@ const buildActorRefCore = (cell, stop, start) => {
76
100
  const matches = Effect.fn("effect-machine.actor.matches")(function* (tag) {
77
101
  return (yield* SubscriptionRef.get(stateRef))._tag === tag;
78
102
  });
79
- const can = Effect.fn("effect-machine.actor.can")(function* (event) {
103
+ const canEffect = Effect.fn("effect-machine.actor.can")(function* (event) {
80
104
  const state = yield* SubscriptionRef.get(stateRef);
81
- return resolveTransition(machine, state, event) !== void 0;
105
+ return (yield* resolveTransitionEffect(machine, state, event)) !== void 0;
82
106
  });
107
+ const can = (event) => canEffect(event).pipe(Effect.provide(serviceContext));
83
108
  const waitFor = Effect.fn("effect-machine.actor.waitFor")(function* (predicateOrState) {
84
109
  let predicate;
85
110
  if (typeof predicateOrState === "function" && !("_tag" in predicateOrState)) predicate = predicateOrState;
@@ -101,18 +126,53 @@ const buildActorRefCore = (cell, stop, start) => {
101
126
  return result;
102
127
  });
103
128
  const awaitFinal = waitFor((state) => machine._isFinal(state._tag)).pipe(Effect.withSpan("effect-machine.actor.awaitFinal"));
129
+ const awaitOutput = Deferred.await(cell.terminalExitDeferred).pipe(Effect.flatMap((exit) => {
130
+ if (exit._tag === "Final") return Effect.succeed(exit.output);
131
+ if (exit._tag === "Defect") return Effect.die(Cause.squash(exit.cause));
132
+ return ActorStoppedError.make({ actorId: id });
133
+ }), Effect.withSpan("effect-machine.actor.awaitOutput"));
104
134
  const sendAndWait = Effect.fn("effect-machine.actor.sendAndWait")(function* (event, predicateOrState) {
105
135
  yield* send(event);
106
136
  if (predicateOrState !== void 0) return yield* waitFor(predicateOrState);
107
137
  return yield* awaitFinal;
108
138
  });
109
139
  const transitions = Stream.fromPubSub(cell.transitions);
140
+ const subscribe = (listener) => {
141
+ listeners.add(listener);
142
+ return () => {
143
+ listeners.delete(listener);
144
+ };
145
+ };
146
+ const sendClient = (event) => {
147
+ runtimeRef.current?.sendSync(event);
148
+ };
149
+ const stopClient = () => {
150
+ Effect.runFork(stop);
151
+ };
152
+ const getSnapshot = () => Effect.runSync(SubscriptionRef.get(stateRef));
153
+ const matchesClient = (tag) => getSnapshot()._tag === tag;
154
+ const canSync = (event) => resolveTransition(machine, getSnapshot(), event) !== void 0;
155
+ const getLifecycle = () => Effect.runSync(SubscriptionRef.get(lifecycleRef));
156
+ const getLatestTransition = () => Effect.runSync(SubscriptionRef.get(latestTransitionRef));
157
+ const client = {
158
+ send: sendClient,
159
+ stop: stopClient,
160
+ getSnapshot,
161
+ matches: matchesClient,
162
+ canSync,
163
+ can: (event) => Effect.runPromise(can(event)),
164
+ getLifecycle,
165
+ getLatestTransition,
166
+ subscribe
167
+ };
110
168
  return {
111
169
  id,
112
170
  send,
113
171
  call,
114
172
  ask,
115
173
  state: stateRef,
174
+ lifecycle: lifecycleRef,
175
+ latestTransition: latestTransitionRef,
116
176
  stop,
117
177
  start,
118
178
  snapshot,
@@ -122,26 +182,20 @@ const buildActorRefCore = (cell, stop, start) => {
122
182
  transitions,
123
183
  waitFor,
124
184
  awaitFinal,
185
+ awaitOutput,
125
186
  sendAndWait,
126
- subscribe: (fn) => {
127
- listeners.add(fn);
128
- return () => {
129
- listeners.delete(fn);
130
- };
131
- },
187
+ subscribe,
188
+ client,
132
189
  awaitExit: Deferred.await(cell.terminalExitDeferred),
133
190
  drain: Effect.suspend(() => runtimeRef.current?.drain ?? Effect.void),
134
191
  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
- }
192
+ send: sendClient,
193
+ stop: stopClient,
194
+ snapshot: getSnapshot,
195
+ matches: matchesClient,
196
+ can: canSync,
197
+ lifecycle: getLifecycle,
198
+ latestTransition: getLatestTransition
145
199
  },
146
200
  system,
147
201
  children: cell.children
@@ -175,7 +229,7 @@ const runSupervisionLoop = (cell, options) => Effect.gen(function* () {
175
229
  if (currentRuntime === void 0) return;
176
230
  const generationExit = yield* Deferred.await(currentRuntime.exitDeferred);
177
231
  if (generationExit._tag !== "Defect") {
178
- yield* Deferred.succeed(cell.terminalExitDeferred, generationExit);
232
+ yield* Deferred.succeed(cell.terminalExitDeferred, toActorExit(cell.machine, generationExit));
179
233
  return;
180
234
  }
181
235
  if (options.supervision.shouldRestart !== void 0 && !options.supervision.shouldRestart(generationExit)) {
@@ -188,26 +242,33 @@ const runSupervisionLoop = (cell, options) => Effect.gen(function* () {
188
242
  }
189
243
  const nextGeneration = cell.generation.current + 1;
190
244
  cell.generation.current = nextGeneration;
191
- let restartState = cell.machine.initial;
245
+ let restartState = cell.machineInitial;
192
246
  if (options.lifecycle?.recovery !== void 0) {
193
247
  const resolved = yield* options.lifecycle.recovery.resolve({
194
248
  actorId: cell.id,
195
249
  generation: nextGeneration,
196
- machineInitial: cell.machine.initial
250
+ machineInitial: cell.machineInitial
197
251
  });
198
252
  if (Option.isSome(resolved)) restartState = resolved.value;
199
253
  }
200
254
  yield* currentRuntime.settlePendingRequests;
255
+ yield* SubscriptionRef.set(cell.lifecycleRef, {
256
+ _tag: "Starting",
257
+ generation: nextGeneration
258
+ });
201
259
  const freshQueue = yield* Queue.unbounded();
202
260
  yield* Ref.set(cell.eventQueueRef, freshQueue);
203
261
  yield* SubscriptionRef.set(cell.stateRef, restartState);
204
262
  yield* Ref.set(cell.stoppedRef, false);
205
263
  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);
264
+ const newRuntime = yield* options.spawnGeneration(cell.machine);
209
265
  cell.runtimeRef.current = newRuntime;
210
266
  yield* newRuntime.start;
267
+ const restartExit = yield* Deferred.poll(newRuntime.exitDeferred);
268
+ if (Option.isNone(restartExit)) yield* SubscriptionRef.set(cell.lifecycleRef, {
269
+ _tag: "Active",
270
+ generation: nextGeneration
271
+ });
211
272
  if (options.onRestart !== void 0) yield* options.onRestart(nextGeneration, generationExit);
212
273
  notifyListeners(cell.listeners, restartState);
213
274
  }
@@ -217,9 +278,9 @@ const runSupervisionLoop = (cell, options) => Effect.gen(function* () {
217
278
  * Delegates to the shared runtime kernel with actor-specific lifecycle hooks.
218
279
  */
219
280
  const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machine, options) {
220
- const lifecycle = options?.lifecycle;
281
+ const lifecycle = options.lifecycle;
221
282
  const serviceContext = yield* Effect.context();
222
- const initial = options?.initialState ?? machine.initial;
283
+ const initial = options.initialState;
223
284
  yield* Effect.annotateCurrentSpan("effect_machine.actor.id", id);
224
285
  yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", initial._tag);
225
286
  const { system, implicitSystemScope } = yield* resolveActorSystem();
@@ -227,22 +288,24 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
227
288
  const childrenMap = /* @__PURE__ */ new Map();
228
289
  const listeners = /* @__PURE__ */ new Set();
229
290
  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);
291
+ const generation = { current: 0 };
292
+ const inspectionHooks = (runtimeGeneration) => {
293
+ if (inspectorValue === void 0) return void 0;
294
+ return makeInspectionHooks(id, inspectorValue, () => runtimeGeneration);
295
+ };
234
296
  const stateRef = yield* SubscriptionRef.make(initial);
297
+ const lifecycleRef = yield* SubscriptionRef.make({ _tag: "Created" });
298
+ const latestTransitionRef = yield* SubscriptionRef.make(void 0);
235
299
  const stoppedRef = yield* Ref.make(false);
236
300
  const initialQueue = yield* Queue.unbounded();
237
301
  const eventQueueRef = yield* Ref.make(initialQueue);
238
302
  const terminalExitDeferred = yield* Deferred.make();
239
- let stopEmitted = false;
240
- const generation = { current: 0 };
241
303
  const runtimeRef = { current: void 0 };
242
304
  const supervisorFiberRef = { current: void 0 };
243
305
  const cell = {
244
306
  id,
245
307
  machine,
308
+ machineInitial: options.machineInitial,
246
309
  stateRef,
247
310
  stoppedRef,
248
311
  eventQueueRef,
@@ -251,16 +314,19 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
251
314
  listeners,
252
315
  children: childrenMap,
253
316
  transitions: transitionsPubSub,
317
+ lifecycleRef,
318
+ latestTransitionRef,
254
319
  system,
255
320
  generation
256
321
  };
257
322
  /** Build lifecycle hooks for a generation */
258
- const buildRuntimeLifecycle = () => {
259
- stopEmitted = false;
323
+ const buildRuntimeLifecycle = (runtimeGeneration) => {
324
+ let stopEmitted = false;
260
325
  let onEvent = void 0;
261
326
  if (inspectorValue !== void 0) onEvent = (state, event) => emitWithTimestamp(inspectorValue, (timestamp) => ({
262
327
  type: "@machine.event",
263
328
  actorId: id,
329
+ generation: runtimeGeneration,
264
330
  state,
265
331
  event,
266
332
  timestamp
@@ -271,6 +337,7 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
271
337
  yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
272
338
  type: "@machine.stop",
273
339
  actorId: id,
340
+ generation: runtimeGeneration,
274
341
  finalState: state,
275
342
  timestamp
276
343
  }));
@@ -279,32 +346,39 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
279
346
  if (inspectorValue !== void 0) onInitialSpawnEffects = (state) => emitWithTimestamp(inspectorValue, (timestamp) => ({
280
347
  type: "@machine.effect",
281
348
  actorId: id,
349
+ generation: runtimeGeneration,
282
350
  effectType: "spawn",
283
351
  state,
284
352
  timestamp
285
353
  }));
286
354
  return {
287
355
  onEvent,
288
- onStateChange: (result, event) => {
356
+ onStateChange: (result, event) => Effect.gen(function* () {
357
+ const latest = result.transitions.at(-1);
358
+ if (latest !== void 0) yield* SubscriptionRef.set(latestTransitionRef, {
359
+ fromState: latest.previousState,
360
+ toState: latest.newState,
361
+ event: latest.event
362
+ });
289
363
  notifyListeners(listeners, result.newState);
290
364
  const durability = lifecycle?.durability;
291
365
  if (durability === void 0 || !result.transitioned) return;
292
366
  if (!(durability.shouldSave === void 0 || durability.shouldSave(result.newState, result.previousState))) return;
293
- return durability.save({
367
+ yield* durability.save({
294
368
  actorId: id,
295
- generation: generation.current,
369
+ generation: runtimeGeneration,
296
370
  previousState: result.previousState,
297
371
  nextState: result.newState,
298
372
  event
299
373
  });
300
- },
301
- onProcessed: (result, event) => {
374
+ }),
375
+ onProcessed: (result, _event) => {
302
376
  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);
377
+ return Effect.forEach(result.transitions, (transition) => PubSub.publish(transitionsPubSub, {
378
+ fromState: transition.previousState,
379
+ toState: transition.newState,
380
+ event: transition.event
381
+ }), { discard: true });
308
382
  },
309
383
  onFinal,
310
384
  onShutdown: () => Effect.gen(function* () {
@@ -313,6 +387,7 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
313
387
  yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
314
388
  type: "@machine.stop",
315
389
  actorId: id,
390
+ generation: runtimeGeneration,
316
391
  finalState,
317
392
  timestamp
318
393
  }));
@@ -322,45 +397,52 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
322
397
  };
323
398
  };
324
399
  /** 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;
400
+ const spawnGeneration = (machineForGen) => {
401
+ const runtimeGeneration = generation.current;
402
+ return Ref.get(eventQueueRef).pipe(Effect.flatMap((currentQueue) => createRuntime(machineForGen, system, {
403
+ actorId: id,
404
+ generation: runtimeGeneration,
405
+ hooks: inspectionHooks(runtimeGeneration),
406
+ skipFinalizer: true,
407
+ cellResources: {
408
+ stateRef,
409
+ stoppedRef,
410
+ eventQueue: currentQueue
411
+ },
412
+ lifecycle: buildRuntimeLifecycle(runtimeGeneration),
413
+ onChildSpawned: (childId, child) => Effect.gen(function* () {
414
+ childrenMap.set(childId, child);
415
+ const maybeScope = yield* Effect.serviceOption(Scope.Scope);
416
+ if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, Effect.sync(() => {
417
+ childrenMap.delete(childId);
418
+ }));
419
+ })
420
+ })));
421
+ };
422
+ runtimeRef.current = yield* spawnGeneration(machine);
423
+ const supervision = options.supervision;
345
424
  const stop = Effect.fn("effect-machine.actor.stop")(function* () {
346
425
  if (supervisorFiberRef.current !== void 0) yield* Fiber.interrupt(supervisorFiberRef.current);
347
426
  const currentRuntime = runtimeRef.current;
348
427
  if (currentRuntime !== void 0) yield* currentRuntime.stop;
349
- yield* Deferred.succeed(terminalExitDeferred, { _tag: "Stopped" });
428
+ yield* Deferred.succeed(terminalExitDeferred, ActorExit.Stopped);
350
429
  if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
351
430
  })().pipe(Effect.provide(serviceContext), Effect.asVoid);
352
- const isHydrated = options?.initialState !== void 0;
431
+ const isHydrated = options.hydrated === true;
353
432
  const start = Effect.fn("effect-machine.actor.start")(function* () {
433
+ yield* SubscriptionRef.set(lifecycleRef, {
434
+ _tag: "Starting",
435
+ generation: generation.current
436
+ });
354
437
  if (lifecycle?.recovery !== void 0 && !isHydrated) {
355
438
  const resolved = yield* lifecycle.recovery.resolve({
356
439
  actorId: id,
357
440
  generation: generation.current,
358
- machineInitial: machine.initial
441
+ machineInitial: options.machineInitial
359
442
  });
360
443
  if (Option.isSome(resolved)) {
361
444
  yield* SubscriptionRef.set(stateRef, resolved.value);
362
- const recoveredMachine = machine._withInitial(resolved.value);
363
- const newRuntime = yield* spawnGeneration(recoveredMachine);
445
+ const newRuntime = yield* spawnGeneration(machine);
364
446
  runtimeRef.current = newRuntime;
365
447
  }
366
448
  }
@@ -368,6 +450,7 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
368
450
  yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
369
451
  type: "@machine.spawn",
370
452
  actorId: id,
453
+ generation: generation.current,
371
454
  initialState: currentState,
372
455
  timestamp
373
456
  }));
@@ -375,16 +458,24 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
375
458
  supervision,
376
459
  spawnGeneration,
377
460
  lifecycle,
378
- onRestart: options?.onRestart
461
+ onRestart: options.onRestart
379
462
  }));
380
463
  else {
381
464
  const currentRuntime = runtimeRef.current;
382
- if (currentRuntime !== void 0) yield* Effect.forkDetach(Deferred.await(currentRuntime.exitDeferred).pipe(Effect.tap((exit) => Deferred.succeed(terminalExitDeferred, exit))));
465
+ if (currentRuntime !== void 0) yield* Effect.forkDetach(Deferred.await(currentRuntime.exitDeferred).pipe(Effect.tap((exit) => Deferred.succeed(terminalExitDeferred, toActorExit(machine, exit)))));
383
466
  }
384
467
  const currentRuntime = runtimeRef.current;
385
- if (currentRuntime !== void 0) yield* currentRuntime.start;
468
+ if (currentRuntime !== void 0) {
469
+ yield* currentRuntime.start;
470
+ const currentExit = yield* Deferred.poll(currentRuntime.exitDeferred);
471
+ if (Option.isNone(currentExit)) yield* SubscriptionRef.set(lifecycleRef, {
472
+ _tag: "Active",
473
+ generation: generation.current
474
+ });
475
+ }
386
476
  })().pipe(Effect.provide(serviceContext), Effect.asVoid);
387
- return buildActorRefCore(cell, stop, start);
477
+ yield* Effect.forkDetach(Deferred.await(terminalExitDeferred).pipe(Effect.flatMap((exit) => SubscriptionRef.set(lifecycleRef, exit)), Effect.provide(serviceContext)));
478
+ return buildActorRefCore(cell, stop, start, serviceContext);
388
479
  });
389
480
  /** Notify all system event listeners (sync). */
390
481
  const notifySystemListeners = (listeners, event) => {
@@ -418,6 +509,17 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
418
509
  id,
419
510
  actor: actorRef
420
511
  });
512
+ yield* Effect.forkDetach(actorRef.awaitExit.pipe(Effect.flatMap((exit) => {
513
+ const registered = MutableHashMap.get(actorsMap, id);
514
+ if (Option.isNone(registered) || registered.value !== actorRef) return Effect.void;
515
+ MutableHashMap.remove(actorsMap, id);
516
+ return emitSystemEvent({
517
+ _tag: "ActorStopped",
518
+ id,
519
+ actor: actorRef,
520
+ exit
521
+ });
522
+ })));
421
523
  const maybeScope = yield* Effect.serviceOption(ActorScope);
422
524
  if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, Effect.gen(function* () {
423
525
  if (MutableHashMap.has(actorsMap, id)) {
@@ -448,7 +550,12 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
448
550
  exit
449
551
  });
450
552
  };
553
+ const machineInitial = machine._initial(spawnOptions?.input);
554
+ const initialState = spawnOptions?.hydrate ?? machineInitial;
451
555
  const actor = yield* createActor(id, machine, {
556
+ initialState,
557
+ machineInitial,
558
+ hydrated: spawnOptions?.hydrate !== void 0,
452
559
  supervision: spawnOptions?.supervision,
453
560
  lifecycle: spawnOptions?.lifecycle,
454
561
  onRestart
@@ -458,11 +565,34 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
458
565
  yield* actor.start.pipe(Effect.catchCause((cause) => actor.stop.pipe(Effect.andThen(Effect.failCause(cause)))));
459
566
  return actor;
460
567
  });
461
- const spawn = (id, machine, options) => withSpawnGate(spawnRegular(id, machine, options));
462
- const get = Effect.fn("effect-machine.actorSystem.get")(function* (id) {
463
- return yield* Effect.sync(() => MutableHashMap.get(actorsMap, id));
464
- });
465
- const stop = Effect.fn("effect-machine.actorSystem.stop")(function* (id) {
568
+ const spawn = (idOrKey, machine, options) => {
569
+ const id = actorSystemId(idOrKey);
570
+ return withSpawnGate(spawnRegular(id, machine, options));
571
+ };
572
+ function get(idOrKey) {
573
+ return Effect.sync(() => MutableHashMap.get(actorsMap, actorSystemId(idOrKey)));
574
+ }
575
+ const sameActor = (left, right) => {
576
+ if (Option.isNone(left)) return Option.isNone(right);
577
+ return Option.isSome(right) && Object.is(left.value, right.value);
578
+ };
579
+ function watch(idOrKey) {
580
+ const id = actorSystemId(idOrKey);
581
+ return Stream.callback((queue) => Effect.acquireRelease(Effect.sync(() => {
582
+ const unsubscribe = (event) => {
583
+ if (event.id !== id) return;
584
+ if (event._tag === "ActorSpawned") Queue.offerUnsafe(queue, Option.some(event.actor));
585
+ else if (event._tag === "ActorStopped") Queue.offerUnsafe(queue, Option.none());
586
+ };
587
+ eventListeners.add(unsubscribe);
588
+ Queue.offerUnsafe(queue, MutableHashMap.get(actorsMap, id));
589
+ return () => {
590
+ eventListeners.delete(unsubscribe);
591
+ };
592
+ }), (unsubscribe) => Effect.sync(unsubscribe))).pipe(Stream.changesWith(sameActor));
593
+ }
594
+ const stop = Effect.fn("effect-machine.actorSystem.stop")(function* (idOrKey) {
595
+ const id = actorSystemId(idOrKey);
466
596
  const maybeActor = MutableHashMap.get(actorsMap, id);
467
597
  if (Option.isNone(maybeActor)) return false;
468
598
  const actor = maybeActor.value;
@@ -479,6 +609,7 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
479
609
  return ActorSystem.of({
480
610
  spawn,
481
611
  get,
612
+ watch,
482
613
  stop,
483
614
  events: Stream.fromPubSub(eventPubSub),
484
615
  get actors() {
@@ -506,4 +637,4 @@ const makeSystem = make;
506
637
  */
507
638
  const Default = Layer.effect(ActorSystem, make());
508
639
  //#endregion
509
- export { ActorScope, ActorSystem, Default, createActor, makeSystem };
640
+ export { ActorScope, ActorSystem, ActorSystemKey, Default, actorSystemKey, createActor, makeSystem };
package/dist/atom.d.ts CHANGED
@@ -1,5 +1,8 @@
1
- import { ActorRef } from "./actor.js";
1
+ import { ActorLifecycle, ActorRef, ActorSystemKey, ActorSystemService, TransitionInfo } from "./actor.js";
2
+ import * as Option from "effect/Option";
2
3
  import * as Atom from "effect/unstable/reactivity/Atom";
4
+ import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
5
+ import * as Cause from "effect/Cause";
3
6
  //#region src/atom.d.ts
4
7
  /**
5
8
  * A writable Atom projection of an actor.
@@ -7,6 +10,32 @@ import * as Atom from "effect/unstable/reactivity/Atom";
7
10
  * The Atom value is the current actor state. Atom writes send actor events.
8
11
  */
9
12
  type ActorAtom<State, Event> = Atom.Writable<State, Event>;
13
+ /** A reactive ActorSystem lookup for one typed actor identity. */
14
+ type ActorLookupAtom<State extends {
15
+ readonly _tag: string;
16
+ }, Event, Output> = Atom.Atom<AsyncResult.AsyncResult<Option.Option<ActorRef<State, Event, Output>>, Cause.NoSuchElementError>>;
17
+ /** A Suspense-ready actor acquisition that follows later actor generations. */
18
+ type ActorAcquireAtom<State extends {
19
+ readonly _tag: string;
20
+ }, Event, Output> = Atom.Atom<AsyncResult.AsyncResult<ActorRef<State, Event, Output>, Cause.NoSuchElementError>>;
21
+ /** Observe the current ActorRef for one ActorSystem key. */
22
+ declare const fromSystem: {
23
+ <State extends {
24
+ readonly _tag: string;
25
+ }, Event, Output>(key: ActorSystemKey<State, Event, Output>): (system: ActorSystemService) => ActorLookupAtom<State, Event, Output>;
26
+ <State extends {
27
+ readonly _tag: string;
28
+ }, Event, Output>(system: ActorSystemService, key: ActorSystemKey<State, Event, Output>): ActorLookupAtom<State, Event, Output>;
29
+ };
30
+ /** Suspend while an ActorSystem key is absent and follow later generations. */
31
+ declare const acquire: {
32
+ <State extends {
33
+ readonly _tag: string;
34
+ }, Event, Output>(key: ActorSystemKey<State, Event, Output>): (system: ActorSystemService) => ActorAcquireAtom<State, Event, Output>;
35
+ <State extends {
36
+ readonly _tag: string;
37
+ }, Event, Output>(system: ActorSystemService, key: ActorSystemKey<State, Event, Output>): ActorAcquireAtom<State, Event, Output>;
38
+ };
10
39
  /**
11
40
  * Make a writable Atom from an actor.
12
41
  *
@@ -16,7 +45,7 @@ type ActorAtom<State, Event> = Atom.Writable<State, Event>;
16
45
  */
17
46
  declare const make: <State extends {
18
47
  readonly _tag: string;
19
- }, Event>(actor: ActorRef<State, Event>) => ActorAtom<State, Event>;
48
+ }, Event, Output>(actor: ActorRef<State, Event, Output>) => ActorAtom<State, Event>;
20
49
  /**
21
50
  * Select part of an actor state.
22
51
  *
@@ -27,5 +56,29 @@ declare const select: {
27
56
  <State, Selection>(selector: (state: State) => Selection, equals?: (value: Selection, next: Selection) => boolean): <Event>(self: ActorAtom<State, Event>) => ActorAtom<Selection, Event>;
28
57
  <State, Event, Selection>(self: ActorAtom<State, Event>, selector: (state: State) => Selection, equals?: (value: Selection, next: Selection) => boolean): ActorAtom<Selection, Event>;
29
58
  };
59
+ /** Observe actor lifecycle without coupling it to domain state. */
60
+ declare const lifecycle: <State extends {
61
+ readonly _tag: string;
62
+ }, Event, Output>(actor: ActorRef<State, Event, Output>) => Atom.Atom<ActorLifecycle<State, Output>>;
63
+ /** Observe the latest accepted edge. The value remains after actor exit. */
64
+ declare const latestTransition: <State extends {
65
+ readonly _tag: string;
66
+ }, Event, Output>(actor: ActorRef<State, Event, Output>) => Atom.Atom<TransitionInfo<State, Event> | undefined>;
67
+ /** A reactive result for whether an actor can accept one event. */
68
+ type CanAtom = Atom.Atom<AsyncResult.AsyncResult<boolean>>;
69
+ /**
70
+ * Observe whether an event has an enabled transition.
71
+ *
72
+ * The Atom reevaluates after each actor state change. It supports pure and
73
+ * Effect predicates. Effect predicates use the context captured by the actor.
74
+ */
75
+ declare const can: {
76
+ <Event>(event: Event): <State extends {
77
+ readonly _tag: string;
78
+ }, Output>(actor: ActorRef<State, Event, Output>) => CanAtom;
79
+ <State extends {
80
+ readonly _tag: string;
81
+ }, Event, Output>(actor: ActorRef<State, Event, Output>, event: Event): CanAtom;
82
+ };
30
83
  //#endregion
31
- export { ActorAtom, make, select };
84
+ export { ActorAcquireAtom, ActorAtom, ActorLookupAtom, CanAtom, acquire, can, fromSystem, latestTransition, lifecycle, make, select };