effect-machine 0.22.0 → 0.24.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
@@ -204,7 +208,8 @@ Read [Atom and UI integration](./docs/atom-and-ui.md) and browse [all examples](
204
208
  - Durability saves committed transitions.
205
209
  - Supervision restarts defects within an Effect `Schedule` budget.
206
210
  - Inspection reports events, named transition operations, transitions, named guards, tasks, Effects, errors, stops, and actor generations.
207
- - Dynamic Inspector hubs let lazy tools observe existing actors without actor restart or state duplication.
211
+ - Typed `inspect` spawn options observe one actor.
212
+ - `actor.system.inspect` lets a late tool observe all actors in one system.
208
213
 
209
214
  Read [Persistence and supervision](./docs/persistence-and-supervision.md) and [Inspection](./docs/inspection.md).
210
215
 
package/dist/actor.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { ExtractReply, ReplyTypeBrand } from "./internal/brands.js";
2
2
  import { ActorStoppedError, DuplicateActorError, NoReplyError } from "./errors.js";
3
+ import { InspectorService } from "./inspection.js";
3
4
  import { ActorExit, Supervision } from "./supervision.js";
4
5
  import { Lifecycle, Machine } from "./machine.js";
5
6
  import { ProcessEventResult } from "./internal/transition.js";
@@ -238,11 +239,22 @@ interface ActorSystemService {
238
239
  * Returns an unsubscribe function.
239
240
  */
240
241
  readonly subscribe: (fn: SystemEventListener) => () => void;
242
+ /**
243
+ * Inspect all actors in this system.
244
+ *
245
+ * Registration affects future events only. The returned function removes the inspector.
246
+ */
247
+ readonly inspect: (inspector: InspectorService<{
248
+ readonly _tag: string;
249
+ }, {
250
+ readonly _tag: string;
251
+ }>) => () => void;
241
252
  }
242
253
  type SystemSpawnOptions<S, E, Input> = {
243
254
  readonly supervision?: Supervision.Policy;
244
255
  readonly lifecycle?: Lifecycle<S, E>;
245
256
  readonly hydrate?: S;
257
+ readonly inspect?: InspectorService<S, E>;
246
258
  } & ([Input] extends [void] ? {
247
259
  readonly input?: never;
248
260
  } : {
@@ -278,6 +290,7 @@ declare const createActor: <S extends {
278
290
  hydrated?: boolean;
279
291
  supervision?: Supervision.Policy;
280
292
  lifecycle?: Lifecycle<S, E>;
293
+ inspect?: InspectorService<S, E>;
281
294
  /** @internal Called by system after each restart — emits ActorRestarted system event */
282
295
  onRestart?: (generation: number, exit: ActorExit<unknown>) => Effect.Effect<void>;
283
296
  }) => Effect.Effect<ActorRef<S, E, O>, never, R>;
package/dist/actor.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ActorStoppedError, DuplicateActorError } from "./errors.js";
2
2
  import { resolveTransition, resolveTransitionEffect } from "./internal/transition.js";
3
- import { emitWithTimestamp, makeInspectionHooks } from "./internal/inspection.js";
3
+ import { emitWithTimestamp, makeInspectionDispatcher, makeInspectionHooks } from "./internal/inspection.js";
4
4
  import { Inspector } from "./inspection.js";
5
5
  import { ActorExit } from "./supervision.js";
6
6
  import { createRuntime } from "./internal/runtime.js";
@@ -27,6 +27,7 @@ const actorSystemId = (id) => {
27
27
  if (typeof id === "string") return id;
28
28
  return id.id;
29
29
  };
30
+ const systemInspectorsBySystem = /* @__PURE__ */ new WeakMap();
30
31
  /**
31
32
  * ActorSystem service tag
32
33
  */
@@ -259,6 +260,7 @@ const runSupervisionLoop = (cell, options) => Effect.gen(function* () {
259
260
  const freshQueue = yield* Queue.unbounded();
260
261
  yield* Ref.set(cell.eventQueueRef, freshQueue);
261
262
  yield* SubscriptionRef.set(cell.stateRef, restartState);
263
+ yield* SubscriptionRef.set(cell.latestTransitionRef, void 0);
262
264
  yield* Ref.set(cell.stoppedRef, false);
263
265
  cell.children.clear();
264
266
  const newRuntime = yield* options.spawnGeneration(cell.machine);
@@ -279,20 +281,21 @@ const runSupervisionLoop = (cell, options) => Effect.gen(function* () {
279
281
  */
280
282
  const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machine, options) {
281
283
  const lifecycle = options.lifecycle;
282
- const serviceContext = yield* Effect.context();
284
+ const capturedContext = yield* Effect.context();
283
285
  const initial = options.initialState;
284
286
  yield* Effect.annotateCurrentSpan("effect_machine.actor.id", id);
285
287
  yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", initial._tag);
286
288
  const { system, implicitSystemScope } = yield* resolveActorSystem();
287
- const inspectorValue = Option.getOrUndefined(yield* Effect.serviceOption(Inspector));
289
+ const ambientInspector = Option.getOrUndefined(yield* Effect.serviceOption(Inspector));
290
+ const localInspector = options.inspect ?? ambientInspector;
291
+ const systemInspectors = systemInspectorsBySystem.get(system) ?? /* @__PURE__ */ new Set();
292
+ const inspectorValue = makeInspectionDispatcher(localInspector, systemInspectors);
293
+ const serviceContext = Context.add(capturedContext, Inspector, inspectorValue);
288
294
  const childrenMap = /* @__PURE__ */ new Map();
289
295
  const listeners = /* @__PURE__ */ new Set();
290
296
  const transitionsPubSub = yield* PubSub.unbounded();
291
297
  const generation = { current: 0 };
292
- const inspectionHooks = (runtimeGeneration) => {
293
- if (inspectorValue === void 0) return void 0;
294
- return makeInspectionHooks(id, inspectorValue, () => runtimeGeneration);
295
- };
298
+ const inspectionHooks = (runtimeGeneration) => makeInspectionHooks(id, inspectorValue, () => runtimeGeneration);
296
299
  const stateRef = yield* SubscriptionRef.make(initial);
297
300
  const lifecycleRef = yield* SubscriptionRef.make({ _tag: "Created" });
298
301
  const latestTransitionRef = yield* SubscriptionRef.make(void 0);
@@ -322,8 +325,7 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
322
325
  /** Build lifecycle hooks for a generation */
323
326
  const buildRuntimeLifecycle = (runtimeGeneration) => {
324
327
  let stopEmitted = false;
325
- let onEvent = void 0;
326
- if (inspectorValue !== void 0) onEvent = (state, event) => emitWithTimestamp(inspectorValue, (timestamp) => ({
328
+ const onEvent = (state, event) => emitWithTimestamp(inspectorValue, (timestamp) => ({
327
329
  type: "@machine.event",
328
330
  actorId: id,
329
331
  generation: runtimeGeneration,
@@ -331,8 +333,7 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
331
333
  event,
332
334
  timestamp
333
335
  }));
334
- let onFinal = void 0;
335
- if (inspectorValue !== void 0) onFinal = (state) => Effect.gen(function* () {
336
+ const onFinal = (state) => Effect.gen(function* () {
336
337
  stopEmitted = true;
337
338
  yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
338
339
  type: "@machine.stop",
@@ -342,8 +343,7 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
342
343
  timestamp
343
344
  }));
344
345
  });
345
- let onInitialSpawnEffects = void 0;
346
- if (inspectorValue !== void 0) onInitialSpawnEffects = (state) => emitWithTimestamp(inspectorValue, (timestamp) => ({
346
+ const onInitialSpawnEffects = (state) => emitWithTimestamp(inspectorValue, (timestamp) => ({
347
347
  type: "@machine.effect",
348
348
  actorId: id,
349
349
  generation: runtimeGeneration,
@@ -354,12 +354,6 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
354
354
  return {
355
355
  onEvent,
356
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
- });
363
357
  notifyListeners(listeners, result.newState);
364
358
  const durability = lifecycle?.durability;
365
359
  if (durability === void 0 || !result.transitioned) return;
@@ -406,6 +400,7 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
406
400
  skipFinalizer: true,
407
401
  cellResources: {
408
402
  stateRef,
403
+ latestTransitionRef,
409
404
  stoppedRef,
410
405
  eventQueue: currentQueue
411
406
  },
@@ -488,13 +483,14 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
488
483
  const withSpawnGate = (yield* Semaphore.make(1)).withPermits(1);
489
484
  const eventPubSub = yield* PubSub.unbounded();
490
485
  const eventListeners = /* @__PURE__ */ new Set();
486
+ const systemInspectors = /* @__PURE__ */ new Set();
491
487
  const emitSystemEvent = (event) => Effect.sync(() => notifySystemListeners(eventListeners, event)).pipe(Effect.andThen(PubSub.publish(eventPubSub, event)), Effect.catchCause(() => Effect.void), Effect.asVoid);
492
488
  yield* Effect.addFinalizer(() => {
493
489
  const stops = [];
494
490
  MutableHashMap.forEach(actorsMap, (actor) => {
495
491
  stops.push(actor.stop);
496
492
  });
497
- return Effect.all(stops).pipe(Effect.andThen(PubSub.shutdown(eventPubSub)), Effect.asVoid);
493
+ return Effect.all(stops).pipe(Effect.andThen(Effect.sync(() => systemInspectors.clear())), Effect.andThen(PubSub.shutdown(eventPubSub)), Effect.asVoid);
498
494
  });
499
495
  /** Check for duplicate ID, register actor, attach scope cleanup if available */
500
496
  const registerActor = Effect.fn("effect-machine.actorSystem.register")(function* (id, actor) {
@@ -558,6 +554,7 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
558
554
  hydrated: spawnOptions?.hydrate !== void 0,
559
555
  supervision: spawnOptions?.supervision,
560
556
  lifecycle: spawnOptions?.lifecycle,
557
+ inspect: spawnOptions?.inspect,
561
558
  onRestart
562
559
  });
563
560
  actorRef = actor;
@@ -606,7 +603,7 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
606
603
  yield* actor.stop;
607
604
  return true;
608
605
  });
609
- return ActorSystem.of({
606
+ const system = ActorSystem.of({
610
607
  spawn,
611
608
  get,
612
609
  watch,
@@ -624,8 +621,16 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
624
621
  return () => {
625
622
  eventListeners.delete(fn);
626
623
  };
624
+ },
625
+ inspect: (inspector) => {
626
+ systemInspectors.add(inspector);
627
+ return () => {
628
+ systemInspectors.delete(inspector);
629
+ };
627
630
  }
628
631
  });
632
+ systemInspectorsBySystem.set(system, systemInspectors);
633
+ return system;
629
634
  });
630
635
  /**
631
636
  * Create an ActorSystem instance. Must be run in a Scope.
@@ -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
  }
package/dist/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { DeferReplyResult, ReplyResult } from "./internal/utils.js";
2
2
  import { Event, MachineEventSchema, MachineStateSchema, ReplyFields, State } from "./schema.js";
3
3
  import { ActorStoppedError, AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, NoReplyError, PersistenceError, VersionConflictError } from "./errors.js";
4
+ import { AnyInspectionEvent, EffectEvent, ErrorEvent, EventReceivedEvent, GuardEvent, InspectionEvent, Inspector, InspectorHandler, InspectorService, OperationEvent, SpawnEvent, StopEvent, TaskEvent, TracingInspectorOptions, TransitionEvent, collectingInspector, combineInspectors, consoleInspector, makeInspector, makeInspectorEffect, tracingInspector } from "./inspection.js";
4
5
  import { ActorExit, DefectPhase, Supervision } from "./supervision.js";
5
6
  import { Durability, DurabilityCommit, FinalContext, GuardPredicate, HandlerContext, InputMakeConfig, Lifecycle, Machine, MachineRef, MakeConfig, Recovery, RecoveryContext, ReplayOptions, SpawnOptions, StateHandlerContext, TaskOptions, TimeoutConfig, machine_d_exports } from "./machine.js";
6
7
  import { ProcessEventResult } from "./internal/transition.js";
7
8
  import { ActorClient, ActorLifecycle, ActorRef, ActorRefSync, ActorScope, ActorSystem, ActorSystemKey, ActorSystemService, Default, SystemEvent, SystemEventListener, SystemSpawnOptions, TransitionInfo, actorSystemKey } from "./actor.js";
8
9
  import { InputTestHarnessOptions, SimulationOptions, SimulationResult, TestHarness, TestHarnessOptions, assertNeverReaches, assertPath, assertReaches, createTestHarness, simulate } from "./testing.js";
9
- import { AnyInspectionEvent, EffectEvent, ErrorEvent, EventReceivedEvent, GuardEvent, InspectionEvent, Inspector, InspectorHandler, InspectorHub, InspectorService, OperationEvent, SpawnEvent, StopEvent, TaskEvent, TracingInspectorOptions, TransitionEvent, collectingInspector, combineInspectors, consoleInspector, makeInspector, makeInspectorEffect, makeInspectorHub, tracingInspector } from "./inspection.js";
10
- export { type ActorClient, ActorExit, type ActorExit as ActorExitType, type ActorLifecycle, type ActorRef, type ActorRefSync, ActorScope, ActorStoppedError, type ActorSystemService as ActorSystem, Default as ActorSystemDefault, ActorSystemKey, ActorSystem as ActorSystemService, type AnyInspectionEvent, AssertionError, type DefectPhase, type DeferReplyResult, DuplicateActorError, type Durability, type DurabilityCommit, type EffectEvent, type ErrorEvent, Event, type EventReceivedEvent, type FinalContext, type GuardEvent, type GuardPredicate, type HandlerContext, type InputMakeConfig, type InputTestHarnessOptions, type InspectionEvent, type InspectorService as Inspector, type InspectorHandler, type InspectorHub, Inspector as InspectorService, InvalidSchemaError, type Lifecycle, machine_d_exports as Machine, type MachineEventSchema, type MachineRef, type MachineStateSchema, type Machine as MachineType, type MakeConfig, MissingMatchHandlerError, NoReplyError, type OperationEvent, PersistenceError, type ProcessEventResult, type Recovery, type RecoveryContext, type ReplayOptions, type ReplyFields, type ReplyResult, type SimulationOptions, type SimulationResult, type SpawnEvent, type SpawnOptions, State, type StateHandlerContext, type StopEvent, Supervision, type SystemEvent, type SystemEventListener, type SystemSpawnOptions, type TaskEvent, type TaskOptions, type TestHarness, type TestHarnessOptions, type TimeoutConfig, type TracingInspectorOptions, type TransitionEvent, type TransitionInfo, VersionConflictError, actorSystemKey, assertNeverReaches, assertPath, assertReaches, collectingInspector, combineInspectors, consoleInspector, createTestHarness, makeInspector, makeInspectorEffect, makeInspectorHub, simulate, tracingInspector };
10
+ export { type ActorClient, ActorExit, type ActorExit as ActorExitType, type ActorLifecycle, type ActorRef, type ActorRefSync, ActorScope, ActorStoppedError, type ActorSystemService as ActorSystem, Default as ActorSystemDefault, ActorSystemKey, ActorSystem as ActorSystemService, type AnyInspectionEvent, AssertionError, type DefectPhase, type DeferReplyResult, DuplicateActorError, type Durability, type DurabilityCommit, type EffectEvent, type ErrorEvent, Event, type EventReceivedEvent, type FinalContext, type GuardEvent, type GuardPredicate, type HandlerContext, type InputMakeConfig, type InputTestHarnessOptions, type InspectionEvent, type InspectorService as Inspector, type InspectorHandler, Inspector as InspectorService, InvalidSchemaError, type Lifecycle, machine_d_exports as Machine, type MachineEventSchema, type MachineRef, type MachineStateSchema, type Machine as MachineType, type MakeConfig, MissingMatchHandlerError, NoReplyError, type OperationEvent, PersistenceError, type ProcessEventResult, type Recovery, type RecoveryContext, type ReplayOptions, type ReplyFields, type ReplyResult, type SimulationOptions, type SimulationResult, type SpawnEvent, type SpawnOptions, State, type StateHandlerContext, type StopEvent, Supervision, type SystemEvent, type SystemEventListener, type SystemSpawnOptions, type TaskEvent, type TaskOptions, type TestHarness, type TestHarnessOptions, type TimeoutConfig, type TracingInspectorOptions, type TransitionEvent, type TransitionInfo, VersionConflictError, actorSystemKey, assertNeverReaches, assertPath, assertReaches, collectingInspector, combineInspectors, consoleInspector, createTestHarness, makeInspector, makeInspectorEffect, simulate, tracingInspector };
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { ActorStoppedError, AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, NoReplyError, PersistenceError, VersionConflictError } from "./errors.js";
2
2
  import { Event, State } from "./schema.js";
3
- import { Inspector, collectingInspector, combineInspectors, consoleInspector, makeInspector, makeInspectorEffect, makeInspectorHub, tracingInspector } from "./inspection.js";
3
+ import { Inspector, collectingInspector, combineInspectors, consoleInspector, makeInspector, makeInspectorEffect, tracingInspector } from "./inspection.js";
4
4
  import { ActorExit, Supervision } from "./supervision.js";
5
5
  import { ActorScope, ActorSystem, ActorSystemKey, Default, actorSystemKey } from "./actor.js";
6
6
  import { machine_exports } from "./machine.js";
7
7
  import { assertNeverReaches, assertPath, assertReaches, createTestHarness, simulate } from "./testing.js";
8
- export { ActorExit, ActorScope, ActorStoppedError, Default as ActorSystemDefault, ActorSystemKey, ActorSystem as ActorSystemService, AssertionError, DuplicateActorError, Event, Inspector as InspectorService, InvalidSchemaError, machine_exports as Machine, MissingMatchHandlerError, NoReplyError, PersistenceError, State, Supervision, VersionConflictError, actorSystemKey, assertNeverReaches, assertPath, assertReaches, collectingInspector, combineInspectors, consoleInspector, createTestHarness, makeInspector, makeInspectorEffect, makeInspectorHub, simulate, tracingInspector };
8
+ export { ActorExit, ActorScope, ActorStoppedError, Default as ActorSystemDefault, ActorSystemKey, ActorSystem as ActorSystemService, AssertionError, DuplicateActorError, Event, Inspector as InspectorService, InvalidSchemaError, machine_exports as Machine, MissingMatchHandlerError, NoReplyError, PersistenceError, State, Supervision, VersionConflictError, actorSystemKey, assertNeverReaches, assertPath, assertReaches, collectingInspector, combineInspectors, consoleInspector, createTestHarness, makeInspector, makeInspectorEffect, simulate, tracingInspector };
@@ -124,13 +124,6 @@ type InspectorHandler<S, E> = (event: InspectionEvent<S, E>) => void | Effect.Ef
124
124
  interface InspectorService<S, E> {
125
125
  readonly onInspect: InspectorHandler<S, E>;
126
126
  }
127
- /**
128
- * Dynamic Inspector fan-out for applications that load inspection consumers after actors start.
129
- */
130
- interface InspectorHub<S, E> {
131
- readonly inspector: InspectorService<S, E>;
132
- readonly register: (inspector: InspectorService<S, E>) => () => void;
133
- }
134
127
  declare const Inspector_base: Context.ServiceClass<Inspector, "effect-machine/inspection/Inspector", InspectorService<any, any>>;
135
128
  /**
136
129
  * Inspector service tag - optional service for machine introspection
@@ -156,17 +149,6 @@ declare const makeInspectorEffect: <S = {
156
149
  readonly _tag: string;
157
150
  }>(onInspect: (event: InspectionEvent<ResolveType<S>, ResolveType<E>>) => Effect.Effect<void>) => InspectorService<ResolveType<S>, ResolveType<E>>;
158
151
  declare const combineInspectors: <S, E>(...inspectors: ReadonlyArray<InspectorService<S, E>>) => InspectorService<S, E>;
159
- /**
160
- * Create an Inspector that accepts sinks throughout its lifetime.
161
- *
162
- * Provide `hub.inspector` before actor startup. Consumers can then register and unregister sinks
163
- * without restarting the actor. A sink failure does not affect the actor or other sinks.
164
- */
165
- declare const makeInspectorHub: <S = {
166
- readonly _tag: string;
167
- }, E = {
168
- readonly _tag: string;
169
- }>() => InspectorHub<ResolveType<S>, ResolveType<E>>;
170
152
  interface TracingInspectorOptions<S, E> {
171
153
  readonly spanName?: string | ((event: InspectionEvent<S, E>) => string);
172
154
  readonly attributes?: (event: InspectionEvent<S, E>) => Readonly<Record<string, string | number | boolean>>;
@@ -194,4 +176,4 @@ declare const collectingInspector: <S extends {
194
176
  readonly _tag: string;
195
177
  }>(events: InspectionEvent<S, E>[]) => InspectorService<S, E>;
196
178
  //#endregion
197
- export { AnyInspectionEvent, EffectEvent, ErrorEvent, EventReceivedEvent, GuardEvent, InspectionEvent, Inspector, InspectorHandler, InspectorHub, InspectorService, OperationEvent, SpawnEvent, StopEvent, TaskEvent, TracingInspectorOptions, TransitionEvent, collectingInspector, combineInspectors, consoleInspector, makeInspector, makeInspectorEffect, makeInspectorHub, tracingInspector };
179
+ export { AnyInspectionEvent, EffectEvent, ErrorEvent, EventReceivedEvent, GuardEvent, InspectionEvent, Inspector, InspectorHandler, InspectorService, OperationEvent, SpawnEvent, StopEvent, TaskEvent, TracingInspectorOptions, TransitionEvent, collectingInspector, combineInspectors, consoleInspector, makeInspector, makeInspectorEffect, tracingInspector };
@@ -1,3 +1,4 @@
1
+ import { runInspectors } from "./internal/inspection.js";
1
2
  import { Context, Effect, Option } from "effect";
2
3
  //#region src/inspection.ts
3
4
  /**
@@ -15,39 +16,7 @@ var Inspector = class extends Context.Service()("effect-machine/inspection/Inspe
15
16
  */
16
17
  const makeInspector = (onInspect) => ({ onInspect });
17
18
  const makeInspectorEffect = (onInspect) => ({ onInspect });
18
- const inspectionEffect = (inspector, event) => Effect.suspend(() => {
19
- const result = inspector.onInspect(event);
20
- if (Effect.isEffect(result)) return result;
21
- return Effect.void;
22
- });
23
- const combineInspectors = (...inspectors) => ({ onInspect: (event) => Effect.forEach(inspectors, (inspector) => inspectionEffect(inspector, event).pipe(Effect.ignoreCause), {
24
- concurrency: 16,
25
- discard: true
26
- }) });
27
- /**
28
- * Create an Inspector that accepts sinks throughout its lifetime.
29
- *
30
- * Provide `hub.inspector` before actor startup. Consumers can then register and unregister sinks
31
- * without restarting the actor. A sink failure does not affect the actor or other sinks.
32
- */
33
- const makeInspectorHub = () => {
34
- const inspectorByRegistration = /* @__PURE__ */ new Map();
35
- const inspector = { onInspect: (event) => Effect.forEach(Array.from(inspectorByRegistration.values()), (registeredInspector) => inspectionEffect(registeredInspector, event).pipe(Effect.ignoreCause), {
36
- concurrency: 16,
37
- discard: true
38
- }) };
39
- const register = (registeredInspector) => {
40
- const registration = Symbol();
41
- inspectorByRegistration.set(registration, registeredInspector);
42
- return () => {
43
- inspectorByRegistration.delete(registration);
44
- };
45
- };
46
- return {
47
- inspector,
48
- register
49
- };
50
- };
19
+ const combineInspectors = (...inspectors) => ({ onInspect: (event) => runInspectors(inspectors, event) });
51
20
  const inspectionSpanName = (event) => {
52
21
  switch (event.type) {
53
22
  case "@machine.spawn": return `Machine.inspect ${event.initialState._tag}`;
@@ -181,4 +150,4 @@ const collectingInspector = (events) => ({ onInspect: (event) => {
181
150
  events.push(event);
182
151
  } });
183
152
  //#endregion
184
- export { Inspector, collectingInspector, combineInspectors, consoleInspector, makeInspector, makeInspectorEffect, makeInspectorHub, tracingInspector };
153
+ export { Inspector, collectingInspector, combineInspectors, consoleInspector, makeInspector, makeInspectorEffect, tracingInspector };
@@ -1,7 +1,17 @@
1
+ import { InspectionEvent, InspectorService } from "../inspection.js";
1
2
  import { ProcessEventHooks } from "./transition.js";
2
- import { InspectorService } from "../inspection.js";
3
+ import { Effect } from "effect";
3
4
  //#region src/internal/inspection.d.ts
5
+ type Tagged = {
6
+ readonly _tag: string;
7
+ };
8
+ /** Run one inspector without letting its failure affect the machine. */
9
+ declare const runInspector: <S, E>(inspector: InspectorService<S, E>, event: InspectionEvent<S, E>) => Effect.Effect<void, never, never>;
10
+ /** Run inspectors in order and isolate each failure. */
11
+ declare const runInspectors: <S, E>(inspectors: Iterable<InspectorService<S, E>>, event: InspectionEvent<S, E>) => Effect.Effect<void, never, never>;
12
+ /** Build the actor-owned dispatcher for local and system inspection. */
13
+ declare const makeInspectionDispatcher: <S extends Tagged, E extends Tagged>(localInspector: InspectorService<S, E> | undefined, systemInspectors: ReadonlySet<InspectorService<Tagged, Tagged>>) => InspectorService<S, E>;
4
14
  /** Adapt the Inspector service to the transition kernel. */
5
15
  declare const makeInspectionHooks: <S, E>(actorId: string, inspector: InspectorService<S, E>, getGeneration?: () => number) => ProcessEventHooks<S, E>;
6
16
  //#endregion
7
- export { makeInspectionHooks };
17
+ export { makeInspectionDispatcher, makeInspectionHooks, runInspector, runInspectors };
@@ -1,14 +1,32 @@
1
1
  import { Cause, Clock, Effect } from "effect";
2
2
  //#region src/internal/inspection.ts
3
+ const inspectorActivity = /* @__PURE__ */ new WeakMap();
4
+ /** Run one inspector without letting its failure affect the machine. */
5
+ const runInspector = Effect.fn("effect-machine.runInspector")(function* (inspector, event) {
6
+ const result = yield* Effect.try(() => inspector.onInspect(event)).pipe(Effect.orElseSucceed(() => void 0));
7
+ if (Effect.isEffect(result)) yield* result.pipe(Effect.ignoreCause);
8
+ });
9
+ /** Run inspectors in order and isolate each failure. */
10
+ const runInspectors = Effect.fn("effect-machine.runInspectors")(function* (inspectors, event) {
11
+ for (const inspector of inspectors) yield* runInspector(inspector, event);
12
+ });
13
+ /** Build the actor-owned dispatcher for local and system inspection. */
14
+ const makeInspectionDispatcher = (localInspector, systemInspectors) => {
15
+ const inspector = { onInspect: (event) => Effect.gen(function* () {
16
+ if (localInspector !== void 0) yield* runInspector(localInspector, event);
17
+ for (const systemInspector of systemInspectors) yield* runInspector(systemInspector, event);
18
+ }) };
19
+ inspectorActivity.set(inspector, () => localInspector !== void 0 || systemInspectors.size > 0);
20
+ return inspector;
21
+ };
3
22
  /**
4
23
  * Emit an inspection event with timestamp from Clock.
5
24
  * @internal
6
25
  */
7
26
  const emitWithTimestamp = Effect.fn("effect-machine.emitWithTimestamp")(function* (inspector, makeEvent) {
8
- if (inspector === void 0) return;
27
+ if (inspector === void 0 || inspectorActivity.get(inspector)?.() === false) return;
9
28
  const event = makeEvent(yield* Clock.currentTimeMillis);
10
- const result = yield* Effect.try(() => inspector.onInspect(event)).pipe(Effect.orElseSucceed(() => void 0));
11
- if (Effect.isEffect(result)) yield* result.pipe(Effect.ignoreCause);
29
+ yield* runInspector(inspector, event);
12
30
  });
13
31
  /** Adapt the Inspector service to the transition kernel. */
14
32
  const makeInspectionHooks = (actorId, inspector, getGeneration = () => 0) => ({
@@ -60,4 +78,4 @@ const makeInspectionHooks = (actorId, inspector, getGeneration = () => 0) => ({
60
78
  }))
61
79
  });
62
80
  //#endregion
63
- export { emitWithTimestamp, makeInspectionHooks };
81
+ export { emitWithTimestamp, makeInspectionDispatcher, makeInspectionHooks, runInspector, runInspectors };
@@ -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;
@@ -70,7 +70,8 @@ const stubSystem = {
70
70
  get actors() {
71
71
  return /* @__PURE__ */ new Map();
72
72
  },
73
- subscribe: () => () => {}
73
+ subscribe: () => () => {},
74
+ inspect: () => () => {}
74
75
  };
75
76
  //#endregion
76
77
  export { INTERNAL_ENTER_EVENT, INTERNAL_INIT_EVENT, getTag, isDeferReplyResult, isEffect, isReplyResult, makeDeferReply, makeReply, stubSystem };
package/dist/machine.d.ts CHANGED
@@ -2,16 +2,21 @@ import { DeferReplyResult, ReplyResult, TransitionResult, makeDeferReply, makeRe
2
2
  import { BrandedEvent, BrandedState, ExtractReply, TaggedOrConstructor } from "./internal/brands.js";
3
3
  import { MachineEventSchema, MachineStateSchema, VariantsUnion } from "./schema.js";
4
4
  import { ActorStoppedError, DuplicateActorError } from "./errors.js";
5
+ import { InspectorService } from "./inspection.js";
5
6
  import { Supervision } from "./supervision.js";
6
- import { ActorRef, ActorSystemService } from "./actor.js";
7
- import { Duration, Effect, Option, Schema, Scope } from "effect";
7
+ import { ActorRef, ActorSystemService, TransitionInfo } from "./actor.js";
8
+ import { Duration, Effect, Option, Schema, Scope, SubscriptionRef } from "effect";
8
9
  declare namespace machine_d_exports {
9
10
  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
11
  }
11
12
  /**
12
13
  * Self reference for sending events back to the machine
13
14
  */
14
- interface MachineRef<Event> {
15
+ interface MachineRef<Event, State = never> {
16
+ /** Actor-owned current state. Consumers must treat this ref as read-only. */
17
+ readonly state: SubscriptionRef.SubscriptionRef<State>;
18
+ /** Actor-owned latest accepted transition. Consumers must treat this ref as read-only. */
19
+ readonly latestTransition: SubscriptionRef.SubscriptionRef<TransitionInfo<State, Event> | undefined>;
15
20
  readonly send: (event: Event) => Effect.Effect<void>;
16
21
  readonly spawn: <S2 extends {
17
22
  readonly _tag: string;
@@ -40,7 +45,7 @@ interface StateHandlerContext<State, Event> {
40
45
  readonly generation: number;
41
46
  readonly state: State;
42
47
  readonly event: Event;
43
- readonly self: MachineRef<Event>;
48
+ readonly self: MachineRef<Event, State>;
44
49
  readonly system: ActorSystemService;
45
50
  }
46
51
  /**
@@ -325,6 +330,7 @@ type SpawnOptions<S, E, Input> = {
325
330
  readonly hydrate?: S;
326
331
  readonly supervision?: Supervision.Policy;
327
332
  readonly lifecycle?: Lifecycle<S, E>;
333
+ readonly inspect?: InspectorService<S, E>;
328
334
  } & ([Input] extends [void] ? {
329
335
  readonly input?: never;
330
336
  } : {
@@ -339,6 +345,7 @@ type SpawnOptions<S, E, Input> = {
339
345
  *
340
346
  * // With lifecycle (recovery + durability)
341
347
  * const actor = yield* Machine.spawn(machine, {
348
+ * inspect: consoleInspector(),
342
349
  * lifecycle: {
343
350
  * recovery: { resolve: (ctx) => storage.get("actor-state") },
344
351
  * durability: { save: (commit) => storage.set("actor-state", commit.nextState) },
package/dist/machine.js CHANGED
@@ -443,7 +443,8 @@ const spawnImpl = Effect.fn("effect-machine.spawn")(function* (machine, idOrOpti
443
443
  machineInitial,
444
444
  hydrated: opts?.hydrate !== void 0,
445
445
  supervision: opts?.supervision,
446
- lifecycle: opts?.lifecycle
446
+ lifecycle: opts?.lifecycle,
447
+ inspect: opts?.inspect
447
448
  });
448
449
  const maybeScope = yield* Effect.serviceOption(ActorScope);
449
450
  if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, actor.stop);
@@ -458,6 +459,7 @@ const spawnImpl = Effect.fn("effect-machine.spawn")(function* (machine, idOrOpti
458
459
  *
459
460
  * // With lifecycle (recovery + durability)
460
461
  * const actor = yield* Machine.spawn(machine, {
462
+ * inspect: consoleInspector(),
461
463
  * lifecycle: {
462
464
  * recovery: { resolve: (ctx) => storage.get("actor-state") },
463
465
  * durability: { save: (commit) => storage.set("actor-state", commit.nextState) },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-machine",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/cevr/effect-machine.git"