effect-machine 0.22.0 → 0.23.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/README.md CHANGED
@@ -95,6 +95,10 @@ Effect Machine does not add an action queue or a second context system.
95
95
 
96
96
  Effect requirements remain in `R`. A machine cannot start until the application provides every required service. Effectful transition handlers must have `never` in their error channel. Convert expected failures to states or events.
97
97
 
98
+ Machine-lifetime backgrounds can read `self.state` and `self.latestTransition`. These are the
99
+ actor-owned subscription refs. They stay stable across supervision generations. Treat them as
100
+ read-only and use `SubscriptionRef.get` or `SubscriptionRef.changes` to observe them.
101
+
98
102
  Read [the Effect model](./docs/effect-model.md) and [async work ownership](./docs/async-work.md).
99
103
 
100
104
  ## Guards and stable state
package/dist/actor.js CHANGED
@@ -259,6 +259,7 @@ const runSupervisionLoop = (cell, options) => Effect.gen(function* () {
259
259
  const freshQueue = yield* Queue.unbounded();
260
260
  yield* Ref.set(cell.eventQueueRef, freshQueue);
261
261
  yield* SubscriptionRef.set(cell.stateRef, restartState);
262
+ yield* SubscriptionRef.set(cell.latestTransitionRef, void 0);
262
263
  yield* Ref.set(cell.stoppedRef, false);
263
264
  cell.children.clear();
264
265
  const newRuntime = yield* options.spawnGeneration(cell.machine);
@@ -354,12 +355,6 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
354
355
  return {
355
356
  onEvent,
356
357
  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
- });
363
358
  notifyListeners(listeners, result.newState);
364
359
  const durability = lifecycle?.durability;
365
360
  if (durability === void 0 || !result.transitioned) return;
@@ -406,6 +401,7 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
406
401
  skipFinalizer: true,
407
402
  cellResources: {
408
403
  stateRef,
404
+ latestTransitionRef,
409
405
  stoppedRef,
410
406
  eventQueue: currentQueue
411
407
  },
@@ -55,6 +55,7 @@ const EntityMachine = { layer: (entity, machine, ...optionsArgument) => {
55
55
  const versionRef = yield* Ref.make(persistCtx.initialVersion);
56
56
  const computedInitial = initialState ?? machineInitial;
57
57
  const stateRef = yield* SubscriptionRef.make(computedInitial);
58
+ const latestTransitionRef = yield* SubscriptionRef.make(void 0);
58
59
  const stoppedRef = yield* Ref.make(false);
59
60
  const eventQueue = yield* Queue.unbounded();
60
61
  let hooks = void 0;
@@ -65,6 +66,7 @@ const EntityMachine = { layer: (entity, machine, ...optionsArgument) => {
65
66
  childIdPrefix: `${entityId}/`,
66
67
  cellResources: {
67
68
  stateRef,
69
+ latestTransitionRef,
68
70
  stoppedRef,
69
71
  eventQueue
70
72
  }
@@ -55,7 +55,7 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
55
55
  const generation = config.generation ?? 0;
56
56
  const services = yield* Effect.context();
57
57
  const fork = Effect.runForkWith(services);
58
- const { stateRef, stoppedRef, eventQueue } = config.cellResources;
58
+ const { stateRef, latestTransitionRef, stoppedRef, eventQueue } = config.cellResources;
59
59
  const pendingRequests = /* @__PURE__ */ new Set();
60
60
  const exitDeferred = yield* Deferred.make();
61
61
  const actorScope = yield* Scope.make();
@@ -72,6 +72,8 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
72
72
  let spawn = defaultSpawn;
73
73
  if (onChildSpawned !== void 0) spawn = (childId, childMachine) => defaultSpawn(childId, childMachine).pipe(Effect.tap((child) => onChildSpawned(childId, child)));
74
74
  const self = {
75
+ state: stateRef,
76
+ latestTransition: latestTransitionRef,
75
77
  send: selfSend,
76
78
  spawn,
77
79
  reply: (value) => Effect.sync(() => {
@@ -117,6 +119,12 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
117
119
  else initialResult = initialProcessing;
118
120
  if (initialResult.transitioned) {
119
121
  yield* SubscriptionRef.set(stateRef, initialResult.newState);
122
+ const latest = initialResult.transitions.at(-1);
123
+ if (latest !== void 0) yield* SubscriptionRef.set(latestTransitionRef, {
124
+ fromState: latest.previousState,
125
+ toState: latest.newState,
126
+ event: latest.event
127
+ });
120
128
  if (lifecycle?.onStateChange !== void 0) {
121
129
  const stateChange = lifecycle.onStateChange(initialResult, initEvent);
122
130
  if (isEffect(stateChange)) yield* stateChange;
@@ -164,7 +172,7 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
164
172
  return Effect.void;
165
173
  })), Effect.asVoid)
166
174
  };
167
- const loopFiber = yield* runtimeEventLoop(machine, stateRef, eventQueue, pendingRequests, stoppedRef, self, stateScopeRef, actorId, generation, system, exitDeferred, augmentedHooks, deferredReplyRef, lifecycle, fork).pipe(Effect.provide(services), Effect.forkDetach);
175
+ const loopFiber = yield* runtimeEventLoop(machine, stateRef, latestTransitionRef, eventQueue, pendingRequests, stoppedRef, self, stateScopeRef, actorId, generation, system, exitDeferred, augmentedHooks, deferredReplyRef, lifecycle, fork).pipe(Effect.provide(services), Effect.forkDetach);
168
176
  loopFiberRef.current = loopFiber;
169
177
  if (backgroundFibers.length > 0) yield* Effect.raceAll(backgroundFibers.map((fiber) => Fiber.await(fiber).pipe(Effect.flatMap((exit) => {
170
178
  if (exit._tag === "Failure" && !Cause.hasInterruptsOnly(exit.cause)) return setExit(RuntimeExit.Defect(exit.cause, "background")).pipe(Effect.andThen(Ref.set(stoppedRef, true)), Effect.andThen(Fiber.interrupt(loopFiber)));
@@ -190,7 +198,7 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
190
198
  }).pipe(Effect.asVoid);
191
199
  if (config.skipFinalizer !== true) yield* Effect.addFinalizer(() => stop);
192
200
  return {
193
- ...makeHandle(actorId, stateRef, stoppedRef, eventQueue, pendingRequests, exitDeferred),
201
+ ...makeHandle(actorId, stateRef, latestTransitionRef, stoppedRef, eventQueue, pendingRequests, exitDeferred),
194
202
  stop: stop.pipe(Effect.provide(services)),
195
203
  start: start.pipe(Effect.provide(services))
196
204
  };
@@ -199,7 +207,7 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
199
207
  * Build the runtime handle.
200
208
  * Shared between initial-final and normal paths.
201
209
  */
202
- const makeHandle = (actorId, stateRef, stoppedRef, eventQueue, pendingRequests, exitDeferred) => {
210
+ const makeHandle = (actorId, stateRef, latestTransitionRef, stoppedRef, eventQueue, pendingRequests, exitDeferred) => {
203
211
  const track = (deferred, settle) => {
204
212
  pendingRequests.add(settle);
205
213
  return Deferred.await(deferred).pipe(Effect.ensuring(Effect.sync(() => pendingRequests.delete(settle))));
@@ -261,6 +269,7 @@ const makeHandle = (actorId, stateRef, stoppedRef, eventQueue, pendingRequests,
261
269
  },
262
270
  getState: SubscriptionRef.get(stateRef),
263
271
  stateRef,
272
+ latestTransitionRef,
264
273
  stop: Effect.void,
265
274
  start: Effect.void,
266
275
  settlePendingRequests: settlePendingRequests(pendingRequests, actorId),
@@ -272,7 +281,7 @@ const settlePendingRequests = (pendingRequests, actorId) => Effect.gen(function*
272
281
  for (const settle of pendingRequests) yield* settle(error);
273
282
  pendingRequests.clear();
274
283
  });
275
- const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function* (machine, stateRef, eventQueue, pendingRequests, stoppedRef, self, stateScopeRef, actorId, generation, system, exitDeferred, hooks, deferredReplyRef, lifecycle, fork) {
284
+ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function* (machine, stateRef, latestTransitionRef, eventQueue, pendingRequests, stoppedRef, self, stateScopeRef, actorId, generation, system, exitDeferred, hooks, deferredReplyRef, lifecycle, fork) {
276
285
  const forkEffect = fork ?? Effect.runFork;
277
286
  /** Set the exit deferred exactly once. */
278
287
  const setExit = (exit) => Deferred.succeed(exitDeferred, exit).pipe(Effect.asVoid);
@@ -320,7 +329,15 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
320
329
  let result;
321
330
  if (isEffect(processing)) result = yield* processing;
322
331
  else result = processing;
323
- if (result.transitioned) yield* SubscriptionRef.set(stateRef, result.newState);
332
+ if (result.transitioned) {
333
+ yield* SubscriptionRef.set(stateRef, result.newState);
334
+ const latest = result.transitions.at(-1);
335
+ if (latest !== void 0) yield* SubscriptionRef.set(latestTransitionRef, {
336
+ fromState: latest.previousState,
337
+ toState: latest.newState,
338
+ event: latest.event
339
+ });
340
+ }
324
341
  if (lifecycle?.onStateChange !== void 0 && result.transitioned) {
325
342
  const stateChange = lifecycle.onStateChange(result, event);
326
343
  if (isEffect(stateChange)) yield* stateChange;
package/dist/machine.d.ts CHANGED
@@ -3,15 +3,19 @@ import { BrandedEvent, BrandedState, ExtractReply, TaggedOrConstructor } from ".
3
3
  import { MachineEventSchema, MachineStateSchema, VariantsUnion } from "./schema.js";
4
4
  import { ActorStoppedError, DuplicateActorError } from "./errors.js";
5
5
  import { Supervision } from "./supervision.js";
6
- import { ActorRef, ActorSystemService } from "./actor.js";
7
- import { Duration, Effect, Option, Schema, Scope } from "effect";
6
+ import { ActorRef, ActorSystemService, TransitionInfo } from "./actor.js";
7
+ import { Duration, Effect, Option, Schema, Scope, SubscriptionRef } from "effect";
8
8
  declare namespace machine_d_exports {
9
9
  export { DeferReplyResult, Durability, DurabilityCommit, FinalContext, GuardPredicate, HandlerContext, InputMakeConfig, Lifecycle, Machine, MachineRef, MakeConfig, Recovery, RecoveryContext, ReplayOptions, ReplyResult, SpawnOptions, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, TransitionHandler, deferReply, make, replay, reply, run, scoped, spawn };
10
10
  }
11
11
  /**
12
12
  * Self reference for sending events back to the machine
13
13
  */
14
- interface MachineRef<Event> {
14
+ interface MachineRef<Event, State = never> {
15
+ /** Actor-owned current state. Consumers must treat this ref as read-only. */
16
+ readonly state: SubscriptionRef.SubscriptionRef<State>;
17
+ /** Actor-owned latest accepted transition. Consumers must treat this ref as read-only. */
18
+ readonly latestTransition: SubscriptionRef.SubscriptionRef<TransitionInfo<State, Event> | undefined>;
15
19
  readonly send: (event: Event) => Effect.Effect<void>;
16
20
  readonly spawn: <S2 extends {
17
21
  readonly _tag: string;
@@ -40,7 +44,7 @@ interface StateHandlerContext<State, Event> {
40
44
  readonly generation: number;
41
45
  readonly state: State;
42
46
  readonly event: Event;
43
- readonly self: MachineRef<Event>;
47
+ readonly self: MachineRef<Event, State>;
44
48
  readonly system: ActorSystemService;
45
49
  }
46
50
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-machine",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/cevr/effect-machine.git"