effect-machine 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +24 -0
  2. package/dist/_virtual/_rolldown/runtime.js +18 -0
  3. package/dist/actor.d.ts +256 -0
  4. package/dist/actor.js +402 -0
  5. package/dist/cluster/entity-machine.d.ts +90 -0
  6. package/dist/cluster/entity-machine.js +80 -0
  7. package/dist/cluster/index.d.ts +3 -0
  8. package/dist/cluster/index.js +4 -0
  9. package/dist/cluster/to-entity.d.ts +64 -0
  10. package/dist/cluster/to-entity.js +53 -0
  11. package/dist/errors.d.ts +61 -0
  12. package/dist/errors.js +38 -0
  13. package/dist/index.d.ts +13 -0
  14. package/dist/index.js +14 -0
  15. package/dist/inspection.d.ts +125 -0
  16. package/dist/inspection.js +50 -0
  17. package/dist/internal/brands.d.ts +40 -0
  18. package/dist/internal/brands.js +0 -0
  19. package/dist/internal/inspection.d.ts +11 -0
  20. package/dist/internal/inspection.js +15 -0
  21. package/dist/internal/transition.d.ts +160 -0
  22. package/dist/internal/transition.js +238 -0
  23. package/dist/internal/utils.d.ts +60 -0
  24. package/dist/internal/utils.js +46 -0
  25. package/dist/machine.d.ts +278 -0
  26. package/dist/machine.js +317 -0
  27. package/{src/persistence/adapter.ts → dist/persistence/adapter.d.ts} +40 -72
  28. package/dist/persistence/adapter.js +27 -0
  29. package/dist/persistence/adapters/in-memory.d.ts +32 -0
  30. package/dist/persistence/adapters/in-memory.js +176 -0
  31. package/dist/persistence/index.d.ts +5 -0
  32. package/dist/persistence/index.js +6 -0
  33. package/dist/persistence/persistent-actor.d.ts +50 -0
  34. package/dist/persistence/persistent-actor.js +358 -0
  35. package/{src/persistence/persistent-machine.ts → dist/persistence/persistent-machine.d.ts} +28 -54
  36. package/dist/persistence/persistent-machine.js +24 -0
  37. package/dist/schema.d.ts +141 -0
  38. package/dist/schema.js +165 -0
  39. package/dist/slot.d.ts +130 -0
  40. package/dist/slot.js +99 -0
  41. package/dist/testing.d.ts +142 -0
  42. package/dist/testing.js +138 -0
  43. package/package.json +28 -14
  44. package/src/actor.ts +0 -1058
  45. package/src/cluster/entity-machine.ts +0 -201
  46. package/src/cluster/index.ts +0 -43
  47. package/src/cluster/to-entity.ts +0 -99
  48. package/src/errors.ts +0 -64
  49. package/src/index.ts +0 -105
  50. package/src/inspection.ts +0 -178
  51. package/src/internal/brands.ts +0 -51
  52. package/src/internal/inspection.ts +0 -18
  53. package/src/internal/transition.ts +0 -489
  54. package/src/internal/utils.ts +0 -80
  55. package/src/machine.ts +0 -836
  56. package/src/persistence/adapters/in-memory.ts +0 -294
  57. package/src/persistence/index.ts +0 -24
  58. package/src/persistence/persistent-actor.ts +0 -791
  59. package/src/schema.ts +0 -362
  60. package/src/slot.ts +0 -281
  61. package/src/testing.ts +0 -284
  62. package/tsconfig.json +0 -65
@@ -1,201 +0,0 @@
1
- /**
2
- * EntityMachine adapter - wires a machine to a cluster Entity layer.
3
- *
4
- * @module
5
- */
6
- import { Entity } from "@effect/cluster";
7
- import type { Rpc } from "@effect/rpc";
8
- import { Effect, type Layer, Queue, Ref, Scope } from "effect";
9
-
10
- import type { Machine, MachineRef } from "../machine.js";
11
- import { runSpawnEffects, processEventCore } from "../actor.js";
12
- import type { ProcessEventHooks } from "../actor.js";
13
- import type { GuardsDef, EffectsDef } from "../slot.js";
14
-
15
- /**
16
- * Options for EntityMachine.layer
17
- */
18
- export interface EntityMachineOptions<S, E> {
19
- /**
20
- * Initialize state from entity ID.
21
- * Called once when entity is first activated.
22
- *
23
- * @example
24
- * ```ts
25
- * EntityMachine.layer(OrderEntity, orderMachine, {
26
- * initializeState: (entityId) => OrderState.Pending({ orderId: entityId }),
27
- * })
28
- * ```
29
- */
30
- readonly initializeState?: (entityId: string) => S;
31
-
32
- /**
33
- * Optional hooks for inspection/tracing.
34
- * Called at specific points during event processing.
35
- *
36
- * @example
37
- * ```ts
38
- * EntityMachine.layer(OrderEntity, orderMachine, {
39
- * hooks: {
40
- * onTransition: (from, to, event) =>
41
- * Effect.log(`Transition: ${from._tag} -> ${to._tag}`),
42
- * onSpawnEffect: (state) =>
43
- * Effect.log(`Running spawn effects for ${state._tag}`),
44
- * onError: ({ phase, state }) =>
45
- * Effect.log(`Defect in ${phase} at ${state._tag}`),
46
- * },
47
- * })
48
- * ```
49
- */
50
- readonly hooks?: ProcessEventHooks<S, E>;
51
- }
52
-
53
- /**
54
- * Process a single event through the machine using shared core.
55
- * Returns the new state after processing.
56
- */
57
- const processEvent = Effect.fn("effect-machine.cluster.processEvent")(function* <
58
- S extends { readonly _tag: string },
59
- E extends { readonly _tag: string },
60
- R,
61
- GD extends GuardsDef = Record<string, never>,
62
- EFD extends EffectsDef = Record<string, never>,
63
- >(
64
- machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>,
65
- stateRef: Ref.Ref<S>,
66
- event: E,
67
- self: MachineRef<E>,
68
- stateScopeRef: { current: Scope.CloseableScope },
69
- hooks?: ProcessEventHooks<S, E>,
70
- ) {
71
- const currentState = yield* Ref.get(stateRef);
72
-
73
- // Process event using shared core
74
- const result = yield* processEventCore(machine, currentState, event, self, stateScopeRef, hooks);
75
-
76
- // Update state ref if transition occurred
77
- if (result.transitioned) {
78
- yield* Ref.set(stateRef, result.newState);
79
- }
80
-
81
- return result.newState;
82
- });
83
-
84
- /**
85
- * Create an Entity layer that wires a machine to handle RPC calls.
86
- *
87
- * The layer:
88
- * - Maintains state via Ref per entity instance
89
- * - Resolves transitions using the indexed lookup
90
- * - Evaluates guards in registration order
91
- * - Runs lifecycle effects (onEnter/spawn)
92
- * - Processes internal events from spawn effects
93
- *
94
- * @example
95
- * ```ts
96
- * const OrderEntity = toEntity(orderMachine, {
97
- * type: "Order",
98
- * stateSchema: OrderState,
99
- * eventSchema: OrderEvent,
100
- * })
101
- *
102
- * const OrderEntityLayer = EntityMachine.layer(OrderEntity, orderMachine, {
103
- * initializeState: (entityId) => OrderState.Pending({ orderId: entityId }),
104
- * })
105
- *
106
- * // Use in cluster
107
- * const program = Effect.gen(function* () {
108
- * const client = yield* ShardingClient.client(OrderEntity)
109
- * yield* client.Send("order-123", { event: OrderEvent.Ship({ trackingId: "abc" }) })
110
- * })
111
- * ```
112
- */
113
- export const EntityMachine = {
114
- /**
115
- * Create a layer that wires a machine to an Entity.
116
- *
117
- * @param entity - Entity created via toEntity()
118
- * @param machine - Machine with all effects provided
119
- * @param options - Optional configuration (state initializer, inspection hooks)
120
- */
121
- layer: <
122
- S extends { readonly _tag: string },
123
- E extends { readonly _tag: string },
124
- R,
125
- GD extends GuardsDef,
126
- EFD extends EffectsDef,
127
- EntityType extends string,
128
- Rpcs extends Rpc.Any,
129
- >(
130
- entity: Entity.Entity<EntityType, Rpcs>,
131
- machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>,
132
- options?: EntityMachineOptions<S, E>,
133
- ): Layer.Layer<never, never, R> => {
134
- const layer = Effect.fn("effect-machine.cluster.layer")(function* () {
135
- // Get entity ID from context if available
136
- const entityId = yield* Effect.serviceOption(Entity.CurrentAddress).pipe(
137
- Effect.map((opt) => (opt._tag === "Some" ? opt.value.entityId : "")),
138
- );
139
-
140
- // Initialize state - use provided initializer or machine's initial state
141
- const initialState =
142
- options?.initializeState !== undefined
143
- ? options.initializeState(entityId)
144
- : machine.initial;
145
-
146
- // Create self reference for sending events back to machine
147
- const internalQueue = yield* Queue.unbounded<E>();
148
- const self: MachineRef<E> = {
149
- send: Effect.fn("effect-machine.cluster.self.send")(function* (event: E) {
150
- yield* Queue.offer(internalQueue, event);
151
- }),
152
- };
153
-
154
- // Create state ref
155
- const stateRef = yield* Ref.make<S>(initialState);
156
-
157
- // Create state scope for spawn effects
158
- const stateScopeRef: { current: Scope.CloseableScope } = {
159
- current: yield* Scope.make(),
160
- };
161
-
162
- // Use $init event for initial lifecycle
163
- const initEvent = { _tag: "$init" } as E;
164
-
165
- // Run initial spawn effects
166
- yield* runSpawnEffects(
167
- machine,
168
- initialState,
169
- initEvent,
170
- self,
171
- stateScopeRef.current,
172
- options?.hooks?.onError,
173
- );
174
-
175
- // Process internal events in background
176
- const runInternalEvent = Effect.fn("effect-machine.cluster.internalEvent")(function* () {
177
- const event = yield* Queue.take(internalQueue);
178
- yield* processEvent(machine, stateRef, event, self, stateScopeRef, options?.hooks);
179
- });
180
- yield* Effect.forkScoped(Effect.forever(runInternalEvent()));
181
-
182
- // Return handlers matching the Entity's RPC protocol
183
- // The actual types are inferred from the entity definition
184
- return entity.of({
185
- Send: (envelope: { payload: { event: E } }) =>
186
- processEvent(
187
- machine,
188
- stateRef,
189
- envelope.payload.event,
190
- self,
191
- stateScopeRef,
192
- options?.hooks,
193
- ),
194
-
195
- GetState: () => Ref.get(stateRef),
196
- // Entity.of expects handlers matching Rpcs type param - dynamic construction requires cast
197
- } as unknown as Parameters<typeof entity.of>[0]);
198
- });
199
- return entity.toLayer(layer()) as unknown as Layer.Layer<never, never, R>;
200
- },
201
- };
@@ -1,43 +0,0 @@
1
- /**
2
- * Cluster integration for effect-machine.
3
- *
4
- * Provides bridges between effect-machine state machines and @effect/cluster:
5
- * - `toEntity` - Generate Entity definition from machine
6
- * - `EntityMachine` - Wire machine to cluster Entity layer
7
- *
8
- * @example
9
- * ```ts
10
- * import { Machine, MachineSchema } from "effect-machine"
11
- * import { toEntity, EntityMachine } from "effect-machine/cluster"
12
- *
13
- * // Schema-first definitions
14
- * const OrderState = MachineSchema.State({
15
- * Pending: { orderId: Schema.String },
16
- * Shipped: { trackingId: Schema.String },
17
- * })
18
- *
19
- * const OrderEvent = MachineSchema.Event({
20
- * Ship: { trackingId: Schema.String },
21
- * })
22
- *
23
- * // Define machine
24
- * const orderMachine = Machine.make(OrderState.Pending({ orderId: "" })).pipe(
25
- * Machine.on(OrderState.Pending, OrderEvent.Ship, ...)
26
- * )
27
- *
28
- * // Generate Entity
29
- * const OrderEntity = toEntity(orderMachine, {
30
- * type: "Order",
31
- * stateSchema: OrderState,
32
- * eventSchema: OrderEvent,
33
- * })
34
- *
35
- * // Create layer
36
- * const OrderEntityLayer = EntityMachine.layer(OrderEntity, orderMachine)
37
- * ```
38
- *
39
- * @module
40
- */
41
-
42
- export { toEntity, type ToEntityOptions } from "./to-entity.js";
43
- export { EntityMachine, type EntityMachineOptions } from "./entity-machine.js";
@@ -1,99 +0,0 @@
1
- /**
2
- * Generate Entity definition from a machine.
3
- *
4
- * @module
5
- */
6
- import { Entity } from "@effect/cluster";
7
- import { Rpc } from "@effect/rpc";
8
- import type { Schema } from "effect";
9
-
10
- import type { Machine } from "../machine.js";
11
- import { MissingSchemaError } from "../errors.js";
12
-
13
- /**
14
- * Options for toEntity.
15
- */
16
- export interface ToEntityOptions {
17
- /**
18
- * Entity type name (e.g., "Order", "User")
19
- */
20
- readonly type: string;
21
- }
22
-
23
- /**
24
- * Default RPC protocol for entity machines.
25
- *
26
- * - `Send` - Send event to machine, returns new state
27
- * - `GetState` - Get current state
28
- */
29
- export type EntityRpcs<
30
- StateSchema extends Schema.Schema.Any,
31
- EventSchema extends Schema.Schema.Any,
32
- > = readonly [
33
- Rpc.Rpc<
34
- "Send",
35
- Schema.Struct<{ readonly event: EventSchema }>,
36
- StateSchema,
37
- typeof Schema.Never,
38
- never
39
- >,
40
- Rpc.Rpc<"GetState", typeof Schema.Void, StateSchema, typeof Schema.Never, never>,
41
- ];
42
-
43
- /**
44
- * Generate an Entity definition from a machine.
45
- *
46
- * Creates an Entity with a standard RPC protocol:
47
- * - `Send(event)` - Process event through machine, returns new state
48
- * - `GetState()` - Returns current state
49
- *
50
- * Schemas are read from the machine - must use `Machine.make({ state, event, initial })`.
51
- *
52
- * @example
53
- * ```ts
54
- * const OrderState = State({
55
- * Pending: { orderId: Schema.String },
56
- * Shipped: { trackingId: Schema.String },
57
- * })
58
- *
59
- * const OrderEvent = Event({
60
- * Ship: { trackingId: Schema.String },
61
- * })
62
- *
63
- * const orderMachine = Machine.make({
64
- * state: OrderState,
65
- * event: OrderEvent,
66
- * initial: OrderState.Pending({ orderId: "" }),
67
- * }).pipe(
68
- * Machine.on(OrderState.Pending, OrderEvent.Ship, ...),
69
- * )
70
- *
71
- * const OrderEntity = toEntity(orderMachine, { type: "Order" })
72
- * ```
73
- */
74
- export const toEntity = <
75
- S extends { readonly _tag: string },
76
- E extends { readonly _tag: string },
77
- R,
78
- >(
79
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Schema fields need wide acceptance
80
- machine: Machine<S, E, R, any, any, any, any>,
81
- options: ToEntityOptions,
82
- ) => {
83
- const stateSchema = machine.stateSchema;
84
- const eventSchema = machine.eventSchema;
85
-
86
- if (stateSchema === undefined || eventSchema === undefined) {
87
- throw new MissingSchemaError({ operation: "toEntity" });
88
- }
89
-
90
- return Entity.make(options.type, [
91
- Rpc.make("Send", {
92
- payload: { event: eventSchema },
93
- success: stateSchema,
94
- }),
95
- Rpc.make("GetState", {
96
- success: stateSchema,
97
- }),
98
- ]);
99
- };
package/src/errors.ts DELETED
@@ -1,64 +0,0 @@
1
- /**
2
- * Typed error classes for effect-machine.
3
- *
4
- * All errors extend Schema.TaggedError for:
5
- * - Type-safe catching via Effect.catchTag
6
- * - Serialization support
7
- * - Composable error handling
8
- *
9
- * @module
10
- */
11
- import { Schema } from "effect";
12
-
13
- /** Attempted to spawn/restore actor with ID already in use */
14
- export class DuplicateActorError extends Schema.TaggedError<DuplicateActorError>()(
15
- "DuplicateActorError",
16
- { actorId: Schema.String },
17
- ) {}
18
-
19
- /** Machine has unprovided effect slots */
20
- export class UnprovidedSlotsError extends Schema.TaggedError<UnprovidedSlotsError>()(
21
- "UnprovidedSlotsError",
22
- { slots: Schema.Array(Schema.String) },
23
- ) {}
24
-
25
- /** Operation requires schemas attached to machine */
26
- export class MissingSchemaError extends Schema.TaggedError<MissingSchemaError>()(
27
- "MissingSchemaError",
28
- { operation: Schema.String },
29
- ) {}
30
-
31
- /** State/Event schema has no variants */
32
- export class InvalidSchemaError extends Schema.TaggedError<InvalidSchemaError>()(
33
- "InvalidSchemaError",
34
- {},
35
- ) {}
36
-
37
- /** $match called with missing handler for tag */
38
- export class MissingMatchHandlerError extends Schema.TaggedError<MissingMatchHandlerError>()(
39
- "MissingMatchHandlerError",
40
- { tag: Schema.String },
41
- ) {}
42
-
43
- /** Slot handler not found at runtime (internal error) */
44
- export class SlotProvisionError extends Schema.TaggedError<SlotProvisionError>()(
45
- "SlotProvisionError",
46
- {
47
- slotName: Schema.String,
48
- slotType: Schema.Literal("guard", "effect"),
49
- },
50
- ) {}
51
-
52
- /** Machine.build() validation failed - missing or extra handlers */
53
- export class ProvisionValidationError extends Schema.TaggedError<ProvisionValidationError>()(
54
- "ProvisionValidationError",
55
- {
56
- missing: Schema.Array(Schema.String),
57
- extra: Schema.Array(Schema.String),
58
- },
59
- ) {}
60
-
61
- /** Assertion failed in testing utilities */
62
- export class AssertionError extends Schema.TaggedError<AssertionError>()("AssertionError", {
63
- message: Schema.String,
64
- }) {}
package/src/index.ts DELETED
@@ -1,105 +0,0 @@
1
- // Machine namespace (Effect-style)
2
- export * as Machine from "./machine.js";
3
-
4
- // Slot module
5
- export { Slot } from "./slot.js";
6
- export type {
7
- GuardsSchema,
8
- EffectsSchema,
9
- GuardsDef,
10
- EffectsDef,
11
- GuardSlots,
12
- EffectSlots,
13
- GuardSlot,
14
- EffectSlot as SlotEffectSlot,
15
- GuardHandlers,
16
- EffectHandlers as SlotEffectHandlers,
17
- MachineContext,
18
- } from "./slot.js";
19
-
20
- // Errors
21
- export {
22
- AssertionError,
23
- DuplicateActorError,
24
- InvalidSchemaError,
25
- MissingMatchHandlerError,
26
- MissingSchemaError,
27
- ProvisionValidationError,
28
- SlotProvisionError,
29
- UnprovidedSlotsError,
30
- } from "./errors.js";
31
-
32
- // Schema-first State/Event definitions
33
- export { State, Event } from "./schema.js";
34
- export type { MachineStateSchema, MachineEventSchema } from "./schema.js";
35
-
36
- // Core machine types (for advanced use)
37
- export type {
38
- Machine as MachineType,
39
- BuiltMachine,
40
- MachineRef,
41
- MakeConfig,
42
- Transition,
43
- SpawnEffect,
44
- BackgroundEffect,
45
- PersistOptions,
46
- HandlerContext,
47
- StateHandlerContext,
48
- ProvideHandlers,
49
- } from "./machine.js";
50
-
51
- // Actor types and system
52
- export type { ActorRef, ActorSystem } from "./actor.js";
53
- export { ActorSystem as ActorSystemService, Default as ActorSystemDefault } from "./actor.js";
54
-
55
- // Testing utilities
56
- export {
57
- assertNeverReaches,
58
- assertPath,
59
- assertReaches,
60
- createTestHarness,
61
- simulate,
62
- } from "./testing.js";
63
- export type { SimulationResult, TestHarness, TestHarnessOptions } from "./testing.js";
64
-
65
- // Inspection
66
- export type {
67
- AnyInspectionEvent,
68
- EffectEvent,
69
- ErrorEvent,
70
- EventReceivedEvent,
71
- InspectionEvent,
72
- Inspector,
73
- SpawnEvent,
74
- StopEvent,
75
- TransitionEvent,
76
- } from "./inspection.js";
77
- export {
78
- collectingInspector,
79
- consoleInspector,
80
- Inspector as InspectorService,
81
- makeInspector,
82
- } from "./inspection.js";
83
-
84
- // Persistence
85
- export type {
86
- ActorMetadata,
87
- PersistedEvent,
88
- PersistenceAdapter,
89
- PersistenceConfig,
90
- PersistentActorRef,
91
- PersistentMachine,
92
- RestoreFailure,
93
- RestoreResult,
94
- Snapshot,
95
- } from "./persistence/index.js";
96
- export {
97
- createPersistentActor,
98
- InMemoryPersistenceAdapter,
99
- isPersistentMachine,
100
- makeInMemoryPersistenceAdapter,
101
- PersistenceAdapterTag,
102
- PersistenceError,
103
- restorePersistentActor,
104
- VersionConflictError,
105
- } from "./persistence/index.js";
package/src/inspection.ts DELETED
@@ -1,178 +0,0 @@
1
- import { Context, type Schema } from "effect";
2
-
3
- // ============================================================================
4
- // Type-level helpers
5
- // ============================================================================
6
-
7
- /**
8
- * Resolve a type param: if it's a Schema, extract `.Type`; otherwise use as-is.
9
- */
10
- type ResolveType<T> = T extends Schema.Schema<infer A, infer _I, infer _R> ? A : T;
11
-
12
- // ============================================================================
13
- // Inspection Events
14
- // ============================================================================
15
-
16
- /**
17
- * Event emitted when an actor is spawned
18
- */
19
- export interface SpawnEvent<S> {
20
- readonly type: "@machine.spawn";
21
- readonly actorId: string;
22
- readonly initialState: S;
23
- readonly timestamp: number;
24
- }
25
-
26
- /**
27
- * Event emitted when an actor receives an event
28
- */
29
- export interface EventReceivedEvent<S, E> {
30
- readonly type: "@machine.event";
31
- readonly actorId: string;
32
- readonly state: S;
33
- readonly event: E;
34
- readonly timestamp: number;
35
- }
36
-
37
- /**
38
- * Event emitted when a transition occurs
39
- */
40
- export interface TransitionEvent<S, E> {
41
- readonly type: "@machine.transition";
42
- readonly actorId: string;
43
- readonly fromState: S;
44
- readonly toState: S;
45
- readonly event: E;
46
- readonly timestamp: number;
47
- }
48
-
49
- /**
50
- * Event emitted when a spawn effect runs
51
- */
52
- export interface EffectEvent<S> {
53
- readonly type: "@machine.effect";
54
- readonly actorId: string;
55
- readonly effectType: "spawn";
56
- readonly state: S;
57
- readonly timestamp: number;
58
- }
59
-
60
- /**
61
- * Event emitted when a transition handler or spawn effect fails with a defect
62
- */
63
- export interface ErrorEvent<S, E> {
64
- readonly type: "@machine.error";
65
- readonly actorId: string;
66
- readonly phase: "transition" | "spawn";
67
- readonly state: S;
68
- readonly event: E;
69
- readonly error: string;
70
- readonly timestamp: number;
71
- }
72
-
73
- /**
74
- * Event emitted when an actor stops
75
- */
76
- export interface StopEvent<S> {
77
- readonly type: "@machine.stop";
78
- readonly actorId: string;
79
- readonly finalState: S;
80
- readonly timestamp: number;
81
- }
82
-
83
- /**
84
- * Union of all inspection events
85
- */
86
- export type InspectionEvent<S, E> =
87
- | SpawnEvent<S>
88
- | EventReceivedEvent<S, E>
89
- | TransitionEvent<S, E>
90
- | EffectEvent<S>
91
- | ErrorEvent<S, E>
92
- | StopEvent<S>;
93
-
94
- /**
95
- * Convenience alias for untyped inspection events.
96
- * Useful for general-purpose inspectors that don't need specific state/event types.
97
- * State and event fields are typed as `{ readonly _tag: string }` so discriminated
98
- * access to `_tag` works without casting.
99
- */
100
- export type AnyInspectionEvent = InspectionEvent<
101
- { readonly _tag: string },
102
- { readonly _tag: string }
103
- >;
104
-
105
- // ============================================================================
106
- // Inspector Service
107
- // ============================================================================
108
-
109
- /**
110
- * Inspector interface for observing machine behavior
111
- */
112
- export interface Inspector<S, E> {
113
- readonly onInspect: (event: InspectionEvent<S, E>) => void;
114
- }
115
-
116
- /**
117
- * Inspector service tag - optional service for machine introspection
118
- * Uses `any` types to allow variance flexibility when providing the service
119
- */
120
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
121
- export const Inspector = Context.GenericTag<Inspector<any, any>>("@effect/machine/Inspector");
122
-
123
- /**
124
- * Create an inspector from a callback function.
125
- *
126
- * Type params accept either raw tagged types or Schema constructors:
127
- * - `makeInspector(cb)` — defaults to `AnyInspectionEvent`
128
- * - `makeInspector<MyState, MyEvent>(cb)` — explicit tagged types
129
- * - `makeInspector<typeof MyState, typeof MyEvent>(cb)` — schema constructors (auto-extracts `.Type`)
130
- */
131
- export const makeInspector = <S = { readonly _tag: string }, E = { readonly _tag: string }>(
132
- onInspect: (event: InspectionEvent<ResolveType<S>, ResolveType<E>>) => void,
133
- ): Inspector<ResolveType<S>, ResolveType<E>> => ({ onInspect });
134
-
135
- // ============================================================================
136
- // Built-in Inspectors
137
- // ============================================================================
138
-
139
- /**
140
- * Console inspector that logs events in a readable format
141
- */
142
- export const consoleInspector = (): Inspector<
143
- { readonly _tag: string },
144
- { readonly _tag: string }
145
- > =>
146
- makeInspector((event) => {
147
- const prefix = `[${event.actorId}]`;
148
- switch (event.type) {
149
- case "@machine.spawn":
150
- console.log(prefix, "spawned →", event.initialState._tag);
151
- break;
152
- case "@machine.event":
153
- console.log(prefix, "received", event.event._tag, "in", event.state._tag);
154
- break;
155
- case "@machine.transition":
156
- console.log(prefix, event.fromState._tag, "→", event.toState._tag);
157
- break;
158
- case "@machine.effect":
159
- console.log(prefix, event.effectType, "effect in", event.state._tag);
160
- break;
161
- case "@machine.error":
162
- console.log(prefix, "error in", event.phase, event.state._tag, "-", event.error);
163
- break;
164
- case "@machine.stop":
165
- console.log(prefix, "stopped in", event.finalState._tag);
166
- break;
167
- }
168
- });
169
-
170
- /**
171
- * Collecting inspector that stores events in an array for testing
172
- */
173
- export const collectingInspector = <
174
- S extends { readonly _tag: string },
175
- E extends { readonly _tag: string },
176
- >(
177
- events: InspectionEvent<S, E>[],
178
- ): Inspector<S, E> => ({ onInspect: (event) => events.push(event) });