effect-machine 0.2.4 → 0.3.1

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/src/actor.ts CHANGED
@@ -6,9 +6,11 @@
6
6
  * - ActorSystem service (spawn/stop/get actors)
7
7
  * - Actor creation and event loop
8
8
  */
9
+ import type { Stream } from "effect";
9
10
  import {
10
11
  Cause,
11
12
  Context,
13
+ Deferred,
12
14
  Effect,
13
15
  Exit,
14
16
  Fiber,
@@ -17,12 +19,12 @@ import {
17
19
  Option,
18
20
  Queue,
19
21
  Ref,
22
+ Runtime,
20
23
  Scope,
21
- Stream,
22
24
  SubscriptionRef,
23
25
  } from "effect";
24
26
 
25
- import type { Machine, MachineRef } from "./machine.js";
27
+ import type { Machine, MachineRef, BuiltMachine } from "./machine.js";
26
28
  import type { Inspector } from "./inspection.js";
27
29
  import { Inspector as InspectorTag } from "./inspection.js";
28
30
  import { processEventCore, runSpawnEffects, resolveTransition } from "./internal/transition.js";
@@ -37,7 +39,7 @@ export type {
37
39
  ProcessEventResult,
38
40
  } from "./internal/transition.js";
39
41
  import type { GuardsDef, EffectsDef } from "./slot.js";
40
- import { DuplicateActorError, UnprovidedSlotsError } from "./errors.js";
42
+ import { DuplicateActorError } from "./errors.js";
41
43
  import { INTERNAL_INIT_EVENT } from "./internal/utils.js";
42
44
  import type {
43
45
  ActorMetadata,
@@ -82,6 +84,13 @@ export interface ActorRef<State extends { readonly _tag: string }, Event> {
82
84
  */
83
85
  readonly stop: Effect.Effect<void>;
84
86
 
87
+ /**
88
+ * Stop the actor (fire-and-forget).
89
+ * Signals graceful shutdown without waiting for completion.
90
+ * Use when stopping from sync contexts (e.g. framework cleanup hooks).
91
+ */
92
+ readonly stopSync: () => void;
93
+
85
94
  /**
86
95
  * Get current state snapshot (Effect)
87
96
  */
@@ -118,9 +127,13 @@ export interface ActorRef<State extends { readonly _tag: string }, Event> {
118
127
  readonly changes: Stream.Stream<State>;
119
128
 
120
129
  /**
121
- * Wait for a state that matches predicate (includes current snapshot)
130
+ * Wait for a state that matches predicate or state variant (includes current snapshot).
131
+ * Accepts a predicate function or a state constructor/value (e.g. `State.Active`).
122
132
  */
123
- readonly waitFor: (predicate: (state: State) => boolean) => Effect.Effect<State>;
133
+ readonly waitFor: {
134
+ (predicate: (state: State) => boolean): Effect.Effect<State>;
135
+ (state: { readonly _tag: State["_tag"] }): Effect.Effect<State>;
136
+ };
124
137
 
125
138
  /**
126
139
  * Wait for a final state (includes current snapshot)
@@ -128,12 +141,21 @@ export interface ActorRef<State extends { readonly _tag: string }, Event> {
128
141
  readonly awaitFinal: Effect.Effect<State>;
129
142
 
130
143
  /**
131
- * Send event and wait for predicate or final state
144
+ * Send event and wait for predicate, state variant, or final state.
145
+ * Accepts a predicate function or a state constructor/value (e.g. `State.Active`).
146
+ */
147
+ readonly sendAndWait: {
148
+ (event: Event, predicate: (state: State) => boolean): Effect.Effect<State>;
149
+ (event: Event, state: { readonly _tag: State["_tag"] }): Effect.Effect<State>;
150
+ (event: Event): Effect.Effect<State>;
151
+ };
152
+
153
+ /**
154
+ * Send event synchronously (fire-and-forget).
155
+ * No-op on stopped actors. Use when you need to send from sync contexts
156
+ * (e.g. framework hooks, event handlers).
132
157
  */
133
- readonly sendAndWait: (
134
- event: Event,
135
- predicate?: (state: State) => boolean,
136
- ) => Effect.Effect<State>;
158
+ readonly sendSync: (event: Event) => void;
137
159
 
138
160
  /**
139
161
  * Subscribe to state changes (sync callback)
@@ -159,14 +181,13 @@ export interface ActorSystem {
159
181
  * For regular machines, returns ActorRef.
160
182
  * For persistent machines (created with Machine.persist), returns PersistentActorRef.
161
183
  *
162
- * Note: All effect slots must be provided via `Machine.provide` before spawning.
163
- * Attempting to spawn a machine with unprovided effect slots will fail.
184
+ * All effect slots must be provided via `.build()` before spawning.
164
185
  *
165
186
  * @example
166
187
  * ```ts
167
- * // Regular machine (effects provided)
168
- * const machine = Machine.provide(baseMachine, { fetchData: ... })
169
- * const actor = yield* system.spawn("my-actor", machine);
188
+ * // Regular machine (built)
189
+ * const built = machine.build({ fetchData: ... })
190
+ * const actor = yield* system.spawn("my-actor", built);
170
191
  *
171
192
  * // Persistent machine (auto-detected)
172
193
  * const persistentActor = yield* system.spawn("my-actor", persistentMachine);
@@ -175,18 +196,11 @@ export interface ActorSystem {
175
196
  * ```
176
197
  */
177
198
  readonly spawn: {
178
- // Regular machine overload
179
- <
180
- S extends { readonly _tag: string },
181
- E extends { readonly _tag: string },
182
- R,
183
- GD extends GuardsDef = Record<string, never>,
184
- EFD extends EffectsDef = Record<string, never>,
185
- >(
199
+ // Regular machine overload (BuiltMachine)
200
+ <S extends { readonly _tag: string }, E extends { readonly _tag: string }, R>(
186
201
  id: string,
187
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Schema fields need wide acceptance
188
- machine: Machine<S, E, R, any, any, GD, EFD>,
189
- ): Effect.Effect<ActorRef<S, E>, DuplicateActorError | UnprovidedSlotsError, R | Scope.Scope>;
202
+ machine: BuiltMachine<S, E, R>,
203
+ ): Effect.Effect<ActorRef<S, E>, DuplicateActorError, R>;
190
204
 
191
205
  // Persistent machine overload
192
206
  <S extends { readonly _tag: string }, E extends { readonly _tag: string }, R>(
@@ -194,8 +208,8 @@ export interface ActorSystem {
194
208
  machine: PersistentMachine<S, E, R>,
195
209
  ): Effect.Effect<
196
210
  PersistentActorRef<S, E, R>,
197
- PersistenceError | VersionConflictError | DuplicateActorError | UnprovidedSlotsError,
198
- R | Scope.Scope | PersistenceAdapterTag
211
+ PersistenceError | VersionConflictError | DuplicateActorError,
212
+ R | PersistenceAdapterTag
199
213
  >;
200
214
  };
201
215
 
@@ -218,8 +232,8 @@ export interface ActorSystem {
218
232
  machine: PersistentMachine<S, E, R>,
219
233
  ) => Effect.Effect<
220
234
  Option.Option<PersistentActorRef<S, E, R>>,
221
- PersistenceError | DuplicateActorError | UnprovidedSlotsError,
222
- R | Scope.Scope | PersistenceAdapterTag
235
+ PersistenceError | DuplicateActorError,
236
+ R | PersistenceAdapterTag
223
237
  >;
224
238
 
225
239
  /**
@@ -267,7 +281,7 @@ export interface ActorSystem {
267
281
  >(
268
282
  ids: ReadonlyArray<string>,
269
283
  machine: PersistentMachine<S, E, R>,
270
- ) => Effect.Effect<RestoreResult<S, E, R>, never, R | Scope.Scope | PersistenceAdapterTag>;
284
+ ) => Effect.Effect<RestoreResult<S, E, R>, never, R | PersistenceAdapterTag>;
271
285
 
272
286
  /**
273
287
  * Restore all persisted actors for a machine type.
@@ -288,11 +302,7 @@ export interface ActorSystem {
288
302
  >(
289
303
  machine: PersistentMachine<S, E, R>,
290
304
  options?: { filter?: (meta: ActorMetadata) => boolean },
291
- ) => Effect.Effect<
292
- RestoreResult<S, E, R>,
293
- PersistenceError,
294
- R | Scope.Scope | PersistenceAdapterTag
295
- >;
305
+ ) => Effect.Effect<RestoreResult<S, E, R>, PersistenceError, R | PersistenceAdapterTag>;
296
306
  }
297
307
 
298
308
  /**
@@ -362,16 +372,41 @@ export const buildActorRefCore = <
362
372
  });
363
373
 
364
374
  const waitFor = Effect.fn("effect-machine.actor.waitFor")(function* (
365
- predicate: (state: S) => boolean,
375
+ predicateOrState: ((state: S) => boolean) | { readonly _tag: S["_tag"] },
366
376
  ) {
367
- // Use stateRef.changes directly — it emits the current value as its first
368
- // element inside the SubscriptionRef semaphore, so this is atomic and
369
- // race-free (no gap between "check current" and "subscribe").
370
- const result = yield* stateRef.changes.pipe(Stream.filter(predicate), Stream.runHead);
371
- if (Option.isSome(result)) return result.value;
372
- // Unreachable: changes always emits at least the current value.
373
- // Fallback to snapshot for type completeness.
374
- return yield* SubscriptionRef.get(stateRef);
377
+ const predicate =
378
+ typeof predicateOrState === "function" && !("_tag" in predicateOrState)
379
+ ? predicateOrState
380
+ : (s: S) => s._tag === (predicateOrState as { readonly _tag: string })._tag;
381
+
382
+ // Check current state first SubscriptionRef.get acquires/releases
383
+ // the semaphore quickly (read-only), no deadlock risk.
384
+ const current = yield* SubscriptionRef.get(stateRef);
385
+ if (predicate(current)) return current;
386
+
387
+ // Use sync listener + Deferred to avoid holding the SubscriptionRef
388
+ // semaphore for the duration of a stream (which causes deadlock when
389
+ // send triggers SubscriptionRef.set concurrently).
390
+ const done = yield* Deferred.make<S>();
391
+ const rt = yield* Effect.runtime<never>();
392
+ const runFork = Runtime.runFork(rt);
393
+ const listener = (state: S) => {
394
+ if (predicate(state)) {
395
+ runFork(Deferred.succeed(done, state));
396
+ }
397
+ };
398
+ listeners.add(listener);
399
+
400
+ // Re-check after subscribing to close the race window
401
+ const afterSubscribe = yield* SubscriptionRef.get(stateRef);
402
+ if (predicate(afterSubscribe)) {
403
+ listeners.delete(listener);
404
+ return afterSubscribe;
405
+ }
406
+
407
+ const result = yield* Deferred.await(done);
408
+ listeners.delete(listener);
409
+ return result;
375
410
  });
376
411
 
377
412
  const awaitFinal = waitFor((state) => machine.finalStates.has(state._tag)).pipe(
@@ -380,11 +415,11 @@ export const buildActorRefCore = <
380
415
 
381
416
  const sendAndWait = Effect.fn("effect-machine.actor.sendAndWait")(function* (
382
417
  event: E,
383
- predicate?: (state: S) => boolean,
418
+ predicateOrState?: ((state: S) => boolean) | { readonly _tag: S["_tag"] },
384
419
  ) {
385
420
  yield* send(event);
386
- if (predicate !== undefined) {
387
- return yield* waitFor(predicate);
421
+ if (predicateOrState !== undefined) {
422
+ return yield* waitFor(predicateOrState);
388
423
  }
389
424
  return yield* awaitFinal;
390
425
  });
@@ -394,6 +429,7 @@ export const buildActorRefCore = <
394
429
  send,
395
430
  state: stateRef,
396
431
  stop,
432
+ stopSync: () => Effect.runFork(stop),
397
433
  snapshot,
398
434
  snapshotSync: () => Effect.runSync(SubscriptionRef.get(stateRef)),
399
435
  matches,
@@ -407,6 +443,12 @@ export const buildActorRefCore = <
407
443
  waitFor,
408
444
  awaitFinal,
409
445
  sendAndWait,
446
+ sendSync: (event) => {
447
+ const stopped = Effect.runSync(Ref.get(stoppedRef));
448
+ if (!stopped) {
449
+ Effect.runSync(Queue.offer(eventQueue, event));
450
+ }
451
+ },
410
452
  subscribe: (fn) => {
411
453
  listeners.add(fn);
412
454
  return () => {
@@ -432,11 +474,6 @@ export const createActor = Effect.fn("effect-machine.actor.spawn")(function* <
432
474
  >(id: string, machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>) {
433
475
  yield* Effect.annotateCurrentSpan("effect_machine.actor.id", id);
434
476
 
435
- const missing = machine._missingSlots();
436
- if (missing.length > 0) {
437
- return yield* new UnprovidedSlotsError({ slots: missing });
438
- }
439
-
440
477
  // Get optional inspector from context
441
478
  const inspectorValue = Option.getOrUndefined(yield* Effect.serviceOption(InspectorTag)) as
442
479
  | Inspector<S, E>
@@ -477,7 +514,7 @@ export const createActor = Effect.fn("effect-machine.actor.spawn")(function* <
477
514
  const { effects: effectSlots } = machine._slots;
478
515
 
479
516
  for (const bg of machine.backgroundEffects) {
480
- const fiber = yield* Effect.fork(
517
+ const fiber = yield* Effect.forkDaemon(
481
518
  bg
482
519
  .handler({ state: machine.initial, event: initEvent, self, effects: effectSlots })
483
520
  .pipe(Effect.provideService(machine.Context, ctx)),
@@ -520,10 +557,9 @@ export const createActor = Effect.fn("effect-machine.actor.spawn")(function* <
520
557
  return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, stop);
521
558
  }
522
559
 
523
- // Start the event loop — use forkScoped so the event loop fiber's lifetime
524
- // is tied to the provided Scope, not the calling fiber. This prevents the
525
- // event loop from being interrupted when a transient caller completes.
526
- const loopFiber = yield* Effect.forkScoped(
560
+ // Start the event loop — use forkDaemon so the event loop fiber's lifetime
561
+ // is detached from any parent scope/fiber. actor.stop handles cleanup.
562
+ const loopFiber = yield* Effect.forkDaemon(
527
563
  eventLoop(
528
564
  machine,
529
565
  stateRef,
@@ -774,7 +810,16 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
774
810
  const spawnGate = yield* Effect.makeSemaphore(1);
775
811
  const withSpawnGate = spawnGate.withPermits(1);
776
812
 
777
- /** Check for duplicate ID, register actor, add cleanup finalizer */
813
+ // Stop all actors on system teardown
814
+ yield* Effect.addFinalizer(() => {
815
+ const stops: Effect.Effect<void>[] = [];
816
+ MutableHashMap.forEach(actors, (actor) => {
817
+ stops.push(actor.stop);
818
+ });
819
+ return Effect.all(stops, { concurrency: "unbounded" }).pipe(Effect.asVoid);
820
+ });
821
+
822
+ /** Check for duplicate ID, register actor, attach scope cleanup if available */
778
823
  const registerActor = Effect.fn("effect-machine.actorSystem.register")(function* <
779
824
  T extends { stop: Effect.Effect<void> },
780
825
  >(id: string, actor: T) {
@@ -788,13 +833,17 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
788
833
  // Register it - O(1)
789
834
  MutableHashMap.set(actors, id, actor as unknown as ActorRef<AnyState, unknown>);
790
835
 
791
- // Register cleanup on scope finalization
792
- yield* Effect.addFinalizer(
793
- Effect.fn("effect-machine.actorSystem.register.finalizer")(function* () {
794
- yield* actor.stop;
795
- MutableHashMap.remove(actors, id);
796
- }),
797
- );
836
+ // If scope available, attach per-actor cleanup
837
+ const maybeScope = yield* Effect.serviceOption(Scope.Scope);
838
+ if (Option.isSome(maybeScope)) {
839
+ yield* Scope.addFinalizer(
840
+ maybeScope.value,
841
+ Effect.gen(function* () {
842
+ yield* actor.stop;
843
+ MutableHashMap.remove(actors, id);
844
+ }),
845
+ );
846
+ }
798
847
 
799
848
  return actor;
800
849
  });
@@ -803,14 +852,12 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
803
852
  S extends { readonly _tag: string },
804
853
  E extends { readonly _tag: string },
805
854
  R,
806
- GD extends GuardsDef = Record<string, never>,
807
- EFD extends EffectsDef = Record<string, never>,
808
- >(id: string, machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>) {
855
+ >(id: string, built: BuiltMachine<S, E, R>) {
809
856
  if (MutableHashMap.has(actors, id)) {
810
857
  return yield* new DuplicateActorError({ actorId: id });
811
858
  }
812
859
  // Create and register the actor
813
- const actor = yield* createActor(id, machine);
860
+ const actor = yield* createActor(id, built._inner);
814
861
  return yield* registerActor(id, actor);
815
862
  });
816
863
 
@@ -846,68 +893,44 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
846
893
  S extends { readonly _tag: string },
847
894
  E extends { readonly _tag: string },
848
895
  R,
849
- GD extends GuardsDef = Record<string, never>,
850
- EFD extends EffectsDef = Record<string, never>,
851
- >(
852
- id: string,
853
- machine:
854
- | Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>
855
- | PersistentMachine<S, E, R>,
856
- ) {
896
+ >(id: string, machine: BuiltMachine<S, E, R> | PersistentMachine<S, E, R>) {
857
897
  if (isPersistentMachine(machine)) {
858
898
  // TypeScript can't narrow union with invariant generic params
859
899
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
860
900
  return yield* spawnPersistent(id, machine as PersistentMachine<S, E, R>);
861
901
  }
862
- return yield* spawnRegular(
863
- id,
864
- machine as Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>,
865
- );
902
+ return yield* spawnRegular(id, machine as BuiltMachine<S, E, R>);
866
903
  });
867
904
 
868
905
  // Type-safe overloaded spawn implementation
869
- function spawn<
870
- S extends { readonly _tag: string },
871
- E extends { readonly _tag: string },
872
- R,
873
- GD extends GuardsDef = Record<string, never>,
874
- EFD extends EffectsDef = Record<string, never>,
875
- >(
906
+ function spawn<S extends { readonly _tag: string }, E extends { readonly _tag: string }, R>(
876
907
  id: string,
877
- machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>,
878
- ): Effect.Effect<ActorRef<S, E>, DuplicateActorError | UnprovidedSlotsError, R | Scope.Scope>;
908
+ machine: BuiltMachine<S, E, R>,
909
+ ): Effect.Effect<ActorRef<S, E>, DuplicateActorError, R>;
879
910
  function spawn<S extends { readonly _tag: string }, E extends { readonly _tag: string }, R>(
880
911
  id: string,
881
912
  machine: PersistentMachine<S, E, R>,
882
913
  ): Effect.Effect<
883
914
  PersistentActorRef<S, E, R>,
884
- PersistenceError | VersionConflictError | DuplicateActorError | UnprovidedSlotsError,
885
- R | Scope.Scope | PersistenceAdapterTag
915
+ PersistenceError | VersionConflictError | DuplicateActorError,
916
+ R | PersistenceAdapterTag
886
917
  >;
887
- function spawn<
888
- S extends { readonly _tag: string },
889
- E extends { readonly _tag: string },
890
- R,
891
- GD extends GuardsDef = Record<string, never>,
892
- EFD extends EffectsDef = Record<string, never>,
893
- >(
918
+ function spawn<S extends { readonly _tag: string }, E extends { readonly _tag: string }, R>(
894
919
  id: string,
895
- machine:
896
- | Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>
897
- | PersistentMachine<S, E, R>,
920
+ machine: BuiltMachine<S, E, R> | PersistentMachine<S, E, R>,
898
921
  ):
899
- | Effect.Effect<ActorRef<S, E>, DuplicateActorError | UnprovidedSlotsError, R | Scope.Scope>
922
+ | Effect.Effect<ActorRef<S, E>, DuplicateActorError, R>
900
923
  | Effect.Effect<
901
924
  PersistentActorRef<S, E, R>,
902
- PersistenceError | VersionConflictError | DuplicateActorError | UnprovidedSlotsError,
903
- R | Scope.Scope | PersistenceAdapterTag
925
+ PersistenceError | VersionConflictError | DuplicateActorError,
926
+ R | PersistenceAdapterTag
904
927
  > {
905
928
  return withSpawnGate(spawnImpl(id, machine)) as
906
- | Effect.Effect<ActorRef<S, E>, DuplicateActorError | UnprovidedSlotsError, R | Scope.Scope>
929
+ | Effect.Effect<ActorRef<S, E>, DuplicateActorError, R>
907
930
  | Effect.Effect<
908
931
  PersistentActorRef<S, E, R>,
909
- PersistenceError | VersionConflictError | DuplicateActorError | UnprovidedSlotsError,
910
- R | Scope.Scope | PersistenceAdapterTag
932
+ PersistenceError | VersionConflictError | DuplicateActorError,
933
+ R | PersistenceAdapterTag
911
934
  >;
912
935
  }
913
936
 
@@ -961,7 +984,7 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
961
984
  const restored: PersistentActorRef<S, E, R>[] = [];
962
985
  const failed: {
963
986
  id: string;
964
- error: PersistenceError | DuplicateActorError | UnprovidedSlotsError;
987
+ error: PersistenceError | DuplicateActorError;
965
988
  }[] = [];
966
989
 
967
990
  for (const id of ids) {
@@ -1032,4 +1055,4 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
1032
1055
  /**
1033
1056
  * Default ActorSystem layer
1034
1057
  */
1035
- export const Default = Layer.effect(ActorSystem, make());
1058
+ export const Default = Layer.scoped(ActorSystem, make());
package/src/errors.ts CHANGED
@@ -49,7 +49,7 @@ export class SlotProvisionError extends Schema.TaggedError<SlotProvisionError>()
49
49
  },
50
50
  ) {}
51
51
 
52
- /** Machine.provide() validation failed - missing or extra handlers */
52
+ /** Machine.build() validation failed - missing or extra handlers */
53
53
  export class ProvisionValidationError extends Schema.TaggedError<ProvisionValidationError>()(
54
54
  "ProvisionValidationError",
55
55
  {
package/src/index.ts CHANGED
@@ -36,6 +36,7 @@ export type { MachineStateSchema, MachineEventSchema } from "./schema.js";
36
36
  // Core machine types (for advanced use)
37
37
  export type {
38
38
  Machine as MachineType,
39
+ BuiltMachine,
39
40
  MachineRef,
40
41
  MakeConfig,
41
42
  Transition,
@@ -11,6 +11,7 @@
11
11
  import { Cause, Effect, Exit, Scope } from "effect";
12
12
 
13
13
  import type { Machine, MachineRef, Transition, SpawnEffect, HandlerContext } from "../machine.js";
14
+ import { BuiltMachine } from "../machine.js";
14
15
  import type { GuardsDef, EffectsDef, MachineContext } from "../slot.js";
15
16
  import { isEffect, INTERNAL_ENTER_EVENT } from "./utils.js";
16
17
 
@@ -443,6 +444,7 @@ const getIndex = <
443
444
  * Find all transitions matching a state/event pair.
444
445
  * Returns empty array if no matches.
445
446
  *
447
+ * Accepts both `Machine` and `BuiltMachine`.
446
448
  * O(1) lookup after first access (index is lazily built).
447
449
  */
448
450
  export const findTransitions = <
@@ -453,12 +455,16 @@ export const findTransitions = <
453
455
  EFD extends EffectsDef = Record<string, never>,
454
456
  >(
455
457
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Schema fields need wide acceptance
456
- machine: Machine<S, E, R, any, any, GD, EFD>,
458
+ input: Machine<S, E, R, any, any, GD, EFD> | BuiltMachine<S, E, R>,
457
459
  stateTag: string,
458
460
  eventTag: string,
459
461
  ): ReadonlyArray<Transition<S, E, GD, EFD, R>> => {
462
+ const machine = input instanceof BuiltMachine ? input._inner : input;
460
463
  const index = getIndex(machine);
461
- return index.transitions.get(stateTag)?.get(eventTag) ?? [];
464
+ const specific = index.transitions.get(stateTag)?.get(eventTag) ?? [];
465
+ if (specific.length > 0) return specific;
466
+ // Fallback to wildcard transitions
467
+ return index.transitions.get("*")?.get(eventTag) ?? [];
462
468
  };
463
469
 
464
470
  /**