effect-machine 0.3.0 → 0.3.2

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 (61) hide show
  1. package/dist/_virtual/_rolldown/runtime.js +18 -0
  2. package/dist/actor.d.ts +251 -0
  3. package/dist/actor.js +385 -0
  4. package/dist/cluster/entity-machine.d.ts +90 -0
  5. package/dist/cluster/entity-machine.js +74 -0
  6. package/dist/cluster/index.d.ts +3 -0
  7. package/dist/cluster/index.js +4 -0
  8. package/dist/cluster/to-entity.d.ts +64 -0
  9. package/dist/cluster/to-entity.js +53 -0
  10. package/dist/errors.d.ts +61 -0
  11. package/dist/errors.js +38 -0
  12. package/dist/index.d.ts +13 -0
  13. package/dist/index.js +14 -0
  14. package/dist/inspection.d.ts +125 -0
  15. package/dist/inspection.js +50 -0
  16. package/dist/internal/brands.d.ts +40 -0
  17. package/dist/internal/brands.js +0 -0
  18. package/dist/internal/inspection.d.ts +11 -0
  19. package/dist/internal/inspection.js +15 -0
  20. package/dist/internal/transition.d.ts +159 -0
  21. package/dist/internal/transition.js +235 -0
  22. package/dist/internal/utils.d.ts +52 -0
  23. package/dist/internal/utils.js +31 -0
  24. package/dist/machine.d.ts +271 -0
  25. package/dist/machine.js +317 -0
  26. package/{src/persistence/adapter.ts → dist/persistence/adapter.d.ts} +40 -72
  27. package/dist/persistence/adapter.js +27 -0
  28. package/dist/persistence/adapters/in-memory.d.ts +32 -0
  29. package/dist/persistence/adapters/in-memory.js +176 -0
  30. package/dist/persistence/index.d.ts +5 -0
  31. package/dist/persistence/index.js +6 -0
  32. package/dist/persistence/persistent-actor.d.ts +50 -0
  33. package/dist/persistence/persistent-actor.js +348 -0
  34. package/{src/persistence/persistent-machine.ts → dist/persistence/persistent-machine.d.ts} +28 -54
  35. package/dist/persistence/persistent-machine.js +24 -0
  36. package/dist/schema.d.ts +141 -0
  37. package/dist/schema.js +165 -0
  38. package/dist/slot.d.ts +128 -0
  39. package/dist/slot.js +99 -0
  40. package/dist/testing.d.ts +142 -0
  41. package/dist/testing.js +131 -0
  42. package/package.json +18 -7
  43. package/src/actor.ts +0 -1050
  44. package/src/cluster/entity-machine.ts +0 -201
  45. package/src/cluster/index.ts +0 -43
  46. package/src/cluster/to-entity.ts +0 -99
  47. package/src/errors.ts +0 -64
  48. package/src/index.ts +0 -105
  49. package/src/inspection.ts +0 -178
  50. package/src/internal/brands.ts +0 -51
  51. package/src/internal/inspection.ts +0 -18
  52. package/src/internal/transition.ts +0 -489
  53. package/src/internal/utils.ts +0 -80
  54. package/src/machine.ts +0 -836
  55. package/src/persistence/adapters/in-memory.ts +0 -294
  56. package/src/persistence/index.ts +0 -24
  57. package/src/persistence/persistent-actor.ts +0 -791
  58. package/src/schema.ts +0 -362
  59. package/src/slot.ts +0 -281
  60. package/src/testing.ts +0 -284
  61. package/tsconfig.json +0 -65
@@ -0,0 +1,15 @@
1
+ import { Clock, Effect } from "effect";
2
+
3
+ //#region src/internal/inspection.ts
4
+ /**
5
+ * Emit an inspection event with timestamp from Clock.
6
+ * @internal
7
+ */
8
+ const emitWithTimestamp = Effect.fn("effect-machine.emitWithTimestamp")(function* (inspector, makeEvent) {
9
+ if (inspector === void 0) return;
10
+ const timestamp = yield* Clock.currentTimeMillis;
11
+ yield* Effect.try(() => inspector.onInspect(makeEvent(timestamp))).pipe(Effect.ignore);
12
+ });
13
+
14
+ //#endregion
15
+ export { emitWithTimestamp };
@@ -0,0 +1,159 @@
1
+ import { EffectsDef, GuardsDef, MachineContext } from "../slot.js";
2
+ import { BuiltMachine, Machine, MachineRef, SpawnEffect, Transition } from "../machine.js";
3
+ import { Cause, Effect, Scope } from "effect";
4
+
5
+ //#region src/internal/transition.d.ts
6
+ /**
7
+ * Result of executing a transition.
8
+ */
9
+ interface TransitionExecutionResult<S> {
10
+ /** New state after transition (or current state if no transition matched) */
11
+ readonly newState: S;
12
+ /** Whether a transition was executed */
13
+ readonly transitioned: boolean;
14
+ /** Whether reenter was specified on the transition */
15
+ readonly reenter: boolean;
16
+ }
17
+ /**
18
+ * Run a transition handler and return the new state.
19
+ * Shared logic for executing handlers with proper context.
20
+ *
21
+ * Used by:
22
+ * - executeTransition (actor event loop, testing)
23
+ * - persistent-actor replay (restore, replayTo)
24
+ *
25
+ * @internal
26
+ */
27
+ declare const runTransitionHandler: <S extends {
28
+ readonly _tag: string;
29
+ }, E extends {
30
+ readonly _tag: string;
31
+ }, R, GD extends GuardsDef, EFD extends EffectsDef>(machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>, transition: Transition<S, E, GD, EFD, R>, state: S, event: E, self: MachineRef<E>) => Effect.Effect<S, never, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
32
+ /**
33
+ * Execute a transition for a given state and event.
34
+ * Handles transition resolution, handler invocation, and guard/effect slot creation.
35
+ *
36
+ * Used by:
37
+ * - processEvent in actor.ts (actual actor event loop)
38
+ * - simulate in testing.ts (pure transition simulation)
39
+ * - createTestHarness.send in testing.ts (step-by-step testing)
40
+ *
41
+ * @internal
42
+ */
43
+ declare const executeTransition: <S extends {
44
+ readonly _tag: string;
45
+ }, E extends {
46
+ readonly _tag: string;
47
+ }, R, GD extends GuardsDef, EFD extends EffectsDef>(machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>, currentState: S, event: E, self: MachineRef<E>) => Effect.Effect<{
48
+ newState: S;
49
+ transitioned: boolean;
50
+ reenter: boolean;
51
+ }, never, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
52
+ /**
53
+ * Optional hooks for event processing inspection/tracing.
54
+ */
55
+ interface ProcessEventHooks<S, E> {
56
+ /** Called before running spawn effects */
57
+ readonly onSpawnEffect?: (state: S) => Effect.Effect<void>;
58
+ /** Called after transition completes */
59
+ readonly onTransition?: (from: S, to: S, event: E) => Effect.Effect<void>;
60
+ /** Called when a transition handler or spawn effect fails with a defect */
61
+ readonly onError?: (info: ProcessEventError<S, E>) => Effect.Effect<void>;
62
+ }
63
+ /**
64
+ * Error info for inspection hooks.
65
+ */
66
+ interface ProcessEventError<S, E> {
67
+ readonly phase: "transition" | "spawn";
68
+ readonly state: S;
69
+ readonly event: E;
70
+ readonly cause: Cause.Cause<unknown>;
71
+ }
72
+ /**
73
+ * Result of processing an event through the machine.
74
+ */
75
+ interface ProcessEventResult<S> {
76
+ /** New state after processing */
77
+ readonly newState: S;
78
+ /** Previous state before processing */
79
+ readonly previousState: S;
80
+ /** Whether a transition occurred */
81
+ readonly transitioned: boolean;
82
+ /** Whether lifecycle effects ran (state change or reenter) */
83
+ readonly lifecycleRan: boolean;
84
+ /** Whether new state is final */
85
+ readonly isFinal: boolean;
86
+ }
87
+ /**
88
+ * Process a single event through the machine.
89
+ *
90
+ * Handles:
91
+ * - Transition execution
92
+ * - State scope lifecycle (close old, create new)
93
+ * - Running spawn effects
94
+ *
95
+ * Optional hooks allow inspection/tracing without coupling to specific impl.
96
+ *
97
+ * @internal
98
+ */
99
+ declare const processEventCore: <S extends {
100
+ readonly _tag: string;
101
+ }, E extends {
102
+ readonly _tag: string;
103
+ }, R, GD extends GuardsDef, EFD extends EffectsDef>(machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>, currentState: S, event: E, self: MachineRef<E>, stateScopeRef: {
104
+ current: Scope.CloseableScope;
105
+ }, hooks?: ProcessEventHooks<S, E> | undefined) => Effect.Effect<{
106
+ newState: S;
107
+ previousState: S;
108
+ transitioned: boolean;
109
+ lifecycleRan: boolean;
110
+ isFinal: boolean;
111
+ }, never, Exclude<R, MachineContext<S, E, MachineRef<E>>> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>>;
112
+ /**
113
+ * Run spawn effects for a state (forked into state scope, auto-cancelled on state exit).
114
+ *
115
+ * @internal
116
+ */
117
+ declare const runSpawnEffects: <S extends {
118
+ readonly _tag: string;
119
+ }, E extends {
120
+ readonly _tag: string;
121
+ }, R, GD extends GuardsDef, EFD extends EffectsDef>(machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>, state: S, event: E, self: MachineRef<E>, stateScope: Scope.CloseableScope, onError?: ((info: ProcessEventError<S, E>) => Effect.Effect<void>) | undefined) => Effect.Effect<void, never, Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>>;
122
+ /**
123
+ * Resolve which transition should fire for a given state and event.
124
+ * Uses indexed O(1) lookup. First matching transition wins.
125
+ */
126
+ declare const resolveTransition: <S extends {
127
+ readonly _tag: string;
128
+ }, E extends {
129
+ readonly _tag: string;
130
+ }, R>(machine: Machine<S, E, R, any, any, any, any>, currentState: S, event: E) => (typeof machine.transitions)[number] | undefined;
131
+ /**
132
+ * Invalidate cached index for a machine (call after mutation).
133
+ */
134
+ declare const invalidateIndex: (machine: object) => void;
135
+ /**
136
+ * Find all transitions matching a state/event pair.
137
+ * Returns empty array if no matches.
138
+ *
139
+ * Accepts both `Machine` and `BuiltMachine`.
140
+ * O(1) lookup after first access (index is lazily built).
141
+ */
142
+ declare const findTransitions: <S extends {
143
+ readonly _tag: string;
144
+ }, E extends {
145
+ readonly _tag: string;
146
+ }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: Machine<S, E, R, any, any, GD, EFD> | BuiltMachine<S, E, R>, stateTag: string, eventTag: string) => ReadonlyArray<Transition<S, E, GD, EFD, R>>;
147
+ /**
148
+ * Find all spawn effects for a state.
149
+ * Returns empty array if no matches.
150
+ *
151
+ * O(1) lookup after first access (index is lazily built).
152
+ */
153
+ declare const findSpawnEffects: <S extends {
154
+ readonly _tag: string;
155
+ }, E extends {
156
+ readonly _tag: string;
157
+ }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(machine: Machine<S, E, R, any, any, GD, EFD>, stateTag: string) => ReadonlyArray<SpawnEffect<S, E, EFD, R>>;
158
+ //#endregion
159
+ export { ProcessEventError, ProcessEventHooks, ProcessEventResult, TransitionExecutionResult, executeTransition, findSpawnEffects, findTransitions, invalidateIndex, processEventCore, resolveTransition, runSpawnEffects, runTransitionHandler };
@@ -0,0 +1,235 @@
1
+ import { INTERNAL_ENTER_EVENT, isEffect } from "./utils.js";
2
+ import { BuiltMachine } from "../machine.js";
3
+ import { Cause, Effect, Exit, Scope } from "effect";
4
+
5
+ //#region src/internal/transition.ts
6
+ /**
7
+ * Transition execution and indexing.
8
+ *
9
+ * Combines:
10
+ * - Transition execution logic (for event processing, simulation, test harness)
11
+ * - Event processing core (shared between actor and cluster entity)
12
+ * - O(1) indexed lookup by state/event tag
13
+ *
14
+ * @internal
15
+ */
16
+ /**
17
+ * Run a transition handler and return the new state.
18
+ * Shared logic for executing handlers with proper context.
19
+ *
20
+ * Used by:
21
+ * - executeTransition (actor event loop, testing)
22
+ * - persistent-actor replay (restore, replayTo)
23
+ *
24
+ * @internal
25
+ */
26
+ const runTransitionHandler = Effect.fn("effect-machine.runTransitionHandler")(function* (machine, transition, state, event, self) {
27
+ const ctx = {
28
+ state,
29
+ event,
30
+ self
31
+ };
32
+ const { guards, effects } = machine._slots;
33
+ const handlerCtx = {
34
+ state,
35
+ event,
36
+ guards,
37
+ effects
38
+ };
39
+ const result = transition.handler(handlerCtx);
40
+ return isEffect(result) ? yield* result.pipe(Effect.provideService(machine.Context, ctx)) : result;
41
+ });
42
+ /**
43
+ * Execute a transition for a given state and event.
44
+ * Handles transition resolution, handler invocation, and guard/effect slot creation.
45
+ *
46
+ * Used by:
47
+ * - processEvent in actor.ts (actual actor event loop)
48
+ * - simulate in testing.ts (pure transition simulation)
49
+ * - createTestHarness.send in testing.ts (step-by-step testing)
50
+ *
51
+ * @internal
52
+ */
53
+ const executeTransition = Effect.fn("effect-machine.executeTransition")(function* (machine, currentState, event, self) {
54
+ const transition = resolveTransition(machine, currentState, event);
55
+ if (transition === void 0) return {
56
+ newState: currentState,
57
+ transitioned: false,
58
+ reenter: false
59
+ };
60
+ return {
61
+ newState: yield* runTransitionHandler(machine, transition, currentState, event, self),
62
+ transitioned: true,
63
+ reenter: transition.reenter === true
64
+ };
65
+ });
66
+ /**
67
+ * Process a single event through the machine.
68
+ *
69
+ * Handles:
70
+ * - Transition execution
71
+ * - State scope lifecycle (close old, create new)
72
+ * - Running spawn effects
73
+ *
74
+ * Optional hooks allow inspection/tracing without coupling to specific impl.
75
+ *
76
+ * @internal
77
+ */
78
+ const processEventCore = Effect.fn("effect-machine.processEventCore")(function* (machine, currentState, event, self, stateScopeRef, hooks) {
79
+ const result = yield* executeTransition(machine, currentState, event, self).pipe(Effect.catchAllCause((cause) => {
80
+ if (Cause.isInterruptedOnly(cause)) return Effect.interrupt;
81
+ const onError = hooks?.onError;
82
+ if (onError === void 0) return Effect.failCause(cause).pipe(Effect.orDie);
83
+ return onError({
84
+ phase: "transition",
85
+ state: currentState,
86
+ event,
87
+ cause
88
+ }).pipe(Effect.zipRight(Effect.failCause(cause).pipe(Effect.orDie)));
89
+ }));
90
+ if (!result.transitioned) return {
91
+ newState: currentState,
92
+ previousState: currentState,
93
+ transitioned: false,
94
+ lifecycleRan: false,
95
+ isFinal: false
96
+ };
97
+ const newState = result.newState;
98
+ const runLifecycle = newState._tag !== currentState._tag || result.reenter;
99
+ if (runLifecycle) {
100
+ yield* Scope.close(stateScopeRef.current, Exit.void);
101
+ stateScopeRef.current = yield* Scope.make();
102
+ if (hooks?.onTransition !== void 0) yield* hooks.onTransition(currentState, newState, event);
103
+ if (hooks?.onSpawnEffect !== void 0) yield* hooks.onSpawnEffect(newState);
104
+ yield* runSpawnEffects(machine, newState, { _tag: INTERNAL_ENTER_EVENT }, self, stateScopeRef.current, hooks?.onError);
105
+ }
106
+ return {
107
+ newState,
108
+ previousState: currentState,
109
+ transitioned: true,
110
+ lifecycleRan: runLifecycle,
111
+ isFinal: machine.finalStates.has(newState._tag)
112
+ };
113
+ });
114
+ /**
115
+ * Run spawn effects for a state (forked into state scope, auto-cancelled on state exit).
116
+ *
117
+ * @internal
118
+ */
119
+ const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (machine, state, event, self, stateScope, onError) {
120
+ const spawnEffects = findSpawnEffects(machine, state._tag);
121
+ const ctx = {
122
+ state,
123
+ event,
124
+ self
125
+ };
126
+ const { effects: effectSlots } = machine._slots;
127
+ const reportError = onError;
128
+ for (const spawnEffect of spawnEffects) {
129
+ const effect = spawnEffect.handler({
130
+ state,
131
+ event,
132
+ self,
133
+ effects: effectSlots
134
+ }).pipe(Effect.provideService(machine.Context, ctx), Effect.catchAllCause((cause) => {
135
+ if (Cause.isInterruptedOnly(cause)) return Effect.interrupt;
136
+ if (reportError === void 0) return Effect.failCause(cause).pipe(Effect.orDie);
137
+ return reportError({
138
+ phase: "spawn",
139
+ state,
140
+ event,
141
+ cause
142
+ }).pipe(Effect.zipRight(Effect.failCause(cause).pipe(Effect.orDie)));
143
+ }));
144
+ yield* Effect.forkScoped(effect).pipe(Effect.provideService(Scope.Scope, stateScope));
145
+ }
146
+ });
147
+ /**
148
+ * Resolve which transition should fire for a given state and event.
149
+ * Uses indexed O(1) lookup. First matching transition wins.
150
+ */
151
+ const resolveTransition = (machine, currentState, event) => {
152
+ return findTransitions(machine, currentState._tag, event._tag)[0];
153
+ };
154
+ const indexCache = /* @__PURE__ */ new WeakMap();
155
+ /**
156
+ * Invalidate cached index for a machine (call after mutation).
157
+ */
158
+ const invalidateIndex = (machine) => {
159
+ indexCache.delete(machine);
160
+ };
161
+ /**
162
+ * Build transition index from machine definition.
163
+ * O(n) where n = number of transitions.
164
+ */
165
+ const buildTransitionIndex = (transitions) => {
166
+ const index = /* @__PURE__ */ new Map();
167
+ for (const t of transitions) {
168
+ let stateMap = index.get(t.stateTag);
169
+ if (stateMap === void 0) {
170
+ stateMap = /* @__PURE__ */ new Map();
171
+ index.set(t.stateTag, stateMap);
172
+ }
173
+ let eventList = stateMap.get(t.eventTag);
174
+ if (eventList === void 0) {
175
+ eventList = [];
176
+ stateMap.set(t.eventTag, eventList);
177
+ }
178
+ eventList.push(t);
179
+ }
180
+ return index;
181
+ };
182
+ /**
183
+ * Build spawn index from machine definition.
184
+ */
185
+ const buildSpawnIndex = (effects) => {
186
+ const index = /* @__PURE__ */ new Map();
187
+ for (const e of effects) {
188
+ let stateList = index.get(e.stateTag);
189
+ if (stateList === void 0) {
190
+ stateList = [];
191
+ index.set(e.stateTag, stateList);
192
+ }
193
+ stateList.push(e);
194
+ }
195
+ return index;
196
+ };
197
+ /**
198
+ * Get or build index for a machine.
199
+ */
200
+ const getIndex = (machine) => {
201
+ let index = indexCache.get(machine);
202
+ if (index === void 0) {
203
+ index = {
204
+ transitions: buildTransitionIndex(machine.transitions),
205
+ spawn: buildSpawnIndex(machine.spawnEffects)
206
+ };
207
+ indexCache.set(machine, index);
208
+ }
209
+ return index;
210
+ };
211
+ /**
212
+ * Find all transitions matching a state/event pair.
213
+ * Returns empty array if no matches.
214
+ *
215
+ * Accepts both `Machine` and `BuiltMachine`.
216
+ * O(1) lookup after first access (index is lazily built).
217
+ */
218
+ const findTransitions = (input, stateTag, eventTag) => {
219
+ const index = getIndex(input instanceof BuiltMachine ? input._inner : input);
220
+ const specific = index.transitions.get(stateTag)?.get(eventTag) ?? [];
221
+ if (specific.length > 0) return specific;
222
+ return index.transitions.get("*")?.get(eventTag) ?? [];
223
+ };
224
+ /**
225
+ * Find all spawn effects for a state.
226
+ * Returns empty array if no matches.
227
+ *
228
+ * O(1) lookup after first access (index is lazily built).
229
+ */
230
+ const findSpawnEffects = (machine, stateTag) => {
231
+ return getIndex(machine).spawn.get(stateTag) ?? [];
232
+ };
233
+
234
+ //#endregion
235
+ export { executeTransition, findSpawnEffects, findTransitions, invalidateIndex, processEventCore, resolveTransition, runSpawnEffects, runTransitionHandler };
@@ -0,0 +1,52 @@
1
+ import { Effect } from "effect";
2
+
3
+ //#region src/internal/utils.d.ts
4
+ /**
5
+ * Extracts _tag from a tagged union member
6
+ */
7
+ type TagOf<T> = T extends {
8
+ readonly _tag: infer Tag;
9
+ } ? Tag : never;
10
+ /**
11
+ * Extracts args type from a Data.taggedEnum constructor
12
+ */
13
+ type ArgsOf<C> = C extends ((args: infer A) => unknown) ? A : never;
14
+ /**
15
+ * Extracts return type from a Data.taggedEnum constructor
16
+ * @internal
17
+ */
18
+ type InstanceOf<C> = C extends ((...args: unknown[]) => infer R) ? R : never;
19
+ /**
20
+ * A tagged union constructor (from Data.taggedEnum)
21
+ */
22
+ type TaggedConstructor<T extends {
23
+ readonly _tag: string;
24
+ }> = (args: Omit<T, "_tag">) => T;
25
+ /**
26
+ * Transition handler result - either a new state or Effect producing one
27
+ */
28
+ type TransitionResult<State, R> = State | Effect.Effect<State, never, R>;
29
+ /**
30
+ * Internal event tags used for lifecycle effect contexts.
31
+ * Prefixed with $ to distinguish from user events.
32
+ * @internal
33
+ */
34
+ declare const INTERNAL_INIT_EVENT: "$init";
35
+ declare const INTERNAL_ENTER_EVENT: "$enter";
36
+ /**
37
+ * Extract _tag from a tagged value or constructor.
38
+ *
39
+ * Supports:
40
+ * - Plain values with `_tag` (MachineSchema empty structs)
41
+ * - Constructors with static `_tag` (MachineSchema non-empty structs)
42
+ * - Data.taggedEnum constructors (fallback via instantiation)
43
+ */
44
+ declare const getTag: (constructorOrValue: {
45
+ _tag: string;
46
+ } | ((...args: never[]) => {
47
+ _tag: string;
48
+ })) => string;
49
+ /** Check if a value is an Effect */
50
+ declare const isEffect: (value: unknown) => value is Effect.Effect<unknown, unknown, unknown>;
51
+ //#endregion
52
+ export { ArgsOf, INTERNAL_ENTER_EVENT, INTERNAL_INIT_EVENT, InstanceOf, TagOf, TaggedConstructor, TransitionResult, getTag, isEffect };
@@ -0,0 +1,31 @@
1
+ import { Effect } from "effect";
2
+
3
+ //#region src/internal/utils.ts
4
+ /**
5
+ * Internal event tags used for lifecycle effect contexts.
6
+ * Prefixed with $ to distinguish from user events.
7
+ * @internal
8
+ */
9
+ const INTERNAL_INIT_EVENT = "$init";
10
+ const INTERNAL_ENTER_EVENT = "$enter";
11
+ /**
12
+ * Extract _tag from a tagged value or constructor.
13
+ *
14
+ * Supports:
15
+ * - Plain values with `_tag` (MachineSchema empty structs)
16
+ * - Constructors with static `_tag` (MachineSchema non-empty structs)
17
+ * - Data.taggedEnum constructors (fallback via instantiation)
18
+ */
19
+ const getTag = (constructorOrValue) => {
20
+ if ("_tag" in constructorOrValue && typeof constructorOrValue._tag === "string") return constructorOrValue._tag;
21
+ try {
22
+ return constructorOrValue()._tag;
23
+ } catch {
24
+ return constructorOrValue({})._tag;
25
+ }
26
+ };
27
+ /** Check if a value is an Effect */
28
+ const isEffect = (value) => typeof value === "object" && value !== null && Effect.EffectTypeId in value;
29
+
30
+ //#endregion
31
+ export { INTERNAL_ENTER_EVENT, INTERNAL_INIT_EVENT, getTag, isEffect };