effect-machine 0.3.1 → 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 -1058
  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
@@ -1,51 +0,0 @@
1
- // eslint-disable-next-line eslint-plugin-import/namespace -- false positive: Brand is a type namespace in effect
2
- import type { Brand } from "effect";
3
-
4
- // Unique symbols for type-level branding
5
- declare const StateTypeId: unique symbol;
6
- declare const EventTypeId: unique symbol;
7
-
8
- export type StateTypeId = typeof StateTypeId;
9
- export type EventTypeId = typeof EventTypeId;
10
-
11
- // Brand interfaces - eslint-disable-next-line comments for false positive namespace warnings
12
- // eslint-disable-next-line import/namespace
13
- export interface StateBrand extends Brand.Brand<StateTypeId> {}
14
- // eslint-disable-next-line import/namespace
15
- export interface EventBrand extends Brand.Brand<EventTypeId> {}
16
-
17
- // Shared branded type constraints used across all combinators
18
- export type BrandedState = { readonly _tag: string } & StateBrand;
19
- export type BrandedEvent = { readonly _tag: string } & EventBrand;
20
-
21
- // Unique symbols for schema-level branding (ties brand to specific schema definition)
22
- declare const SchemaIdTypeId: unique symbol;
23
- type SchemaIdTypeId = typeof SchemaIdTypeId;
24
-
25
- /**
26
- * Brand that captures the schema definition type D.
27
- * Two schemas with identical definition shapes will have compatible brands.
28
- * Different definitions = incompatible brands.
29
- */
30
- export interface SchemaIdBrand<
31
- _D extends Record<string, unknown>,
32
- // eslint-disable-next-line import/namespace
33
- > extends Brand.Brand<SchemaIdTypeId> {}
34
-
35
- /**
36
- * Full state brand: combines base state brand with schema-specific brand
37
- */
38
- export type FullStateBrand<D extends Record<string, unknown>> = StateBrand & SchemaIdBrand<D>;
39
-
40
- /**
41
- * Full event brand: combines base event brand with schema-specific brand
42
- */
43
- export type FullEventBrand<D extends Record<string, unknown>> = EventBrand & SchemaIdBrand<D>;
44
-
45
- /**
46
- * Value or constructor for a tagged type.
47
- * Accepts both plain values (empty structs) and constructor functions (non-empty structs).
48
- */
49
- export type TaggedOrConstructor<T extends { readonly _tag: string }> =
50
- | T
51
- | ((...args: never[]) => T);
@@ -1,18 +0,0 @@
1
- import { Clock, Effect } from "effect";
2
-
3
- import type { InspectionEvent, Inspector } from "../inspection.js";
4
-
5
- /**
6
- * Emit an inspection event with timestamp from Clock.
7
- * @internal
8
- */
9
- export const emitWithTimestamp = Effect.fn("effect-machine.emitWithTimestamp")(function* <S, E>(
10
- inspector: Inspector<S, E> | undefined,
11
- makeEvent: (timestamp: number) => InspectionEvent<S, E>,
12
- ) {
13
- if (inspector === undefined) {
14
- return;
15
- }
16
- const timestamp = yield* Clock.currentTimeMillis;
17
- yield* Effect.try(() => inspector.onInspect(makeEvent(timestamp))).pipe(Effect.ignore);
18
- });
@@ -1,489 +0,0 @@
1
- /**
2
- * Transition execution and indexing.
3
- *
4
- * Combines:
5
- * - Transition execution logic (for event processing, simulation, test harness)
6
- * - Event processing core (shared between actor and cluster entity)
7
- * - O(1) indexed lookup by state/event tag
8
- *
9
- * @internal
10
- */
11
- import { Cause, Effect, Exit, Scope } from "effect";
12
-
13
- import type { Machine, MachineRef, Transition, SpawnEffect, HandlerContext } from "../machine.js";
14
- import { BuiltMachine } from "../machine.js";
15
- import type { GuardsDef, EffectsDef, MachineContext } from "../slot.js";
16
- import { isEffect, INTERNAL_ENTER_EVENT } from "./utils.js";
17
-
18
- // ============================================================================
19
- // Transition Execution
20
- // ============================================================================
21
-
22
- /**
23
- * Result of executing a transition.
24
- */
25
- export interface TransitionExecutionResult<S> {
26
- /** New state after transition (or current state if no transition matched) */
27
- readonly newState: S;
28
- /** Whether a transition was executed */
29
- readonly transitioned: boolean;
30
- /** Whether reenter was specified on the transition */
31
- readonly reenter: boolean;
32
- }
33
-
34
- /**
35
- * Run a transition handler and return the new state.
36
- * Shared logic for executing handlers with proper context.
37
- *
38
- * Used by:
39
- * - executeTransition (actor event loop, testing)
40
- * - persistent-actor replay (restore, replayTo)
41
- *
42
- * @internal
43
- */
44
- export const runTransitionHandler = Effect.fn("effect-machine.runTransitionHandler")(function* <
45
- S extends { readonly _tag: string },
46
- E extends { readonly _tag: string },
47
- R,
48
- GD extends GuardsDef,
49
- EFD extends EffectsDef,
50
- >(
51
- machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>,
52
- transition: Transition<S, E, GD, EFD, R>,
53
- state: S,
54
- event: E,
55
- self: MachineRef<E>,
56
- ) {
57
- const ctx: MachineContext<S, E, MachineRef<E>> = { state, event, self };
58
- const { guards, effects } = machine._slots;
59
-
60
- const handlerCtx: HandlerContext<S, E, GD, EFD> = { state, event, guards, effects };
61
- const result = transition.handler(handlerCtx);
62
-
63
- return isEffect(result)
64
- ? yield* (result as Effect.Effect<S, never, R>).pipe(
65
- Effect.provideService(machine.Context, ctx),
66
- )
67
- : result;
68
- });
69
-
70
- /**
71
- * Execute a transition for a given state and event.
72
- * Handles transition resolution, handler invocation, and guard/effect slot creation.
73
- *
74
- * Used by:
75
- * - processEvent in actor.ts (actual actor event loop)
76
- * - simulate in testing.ts (pure transition simulation)
77
- * - createTestHarness.send in testing.ts (step-by-step testing)
78
- *
79
- * @internal
80
- */
81
- export const executeTransition = Effect.fn("effect-machine.executeTransition")(function* <
82
- S extends { readonly _tag: string },
83
- E extends { readonly _tag: string },
84
- R,
85
- GD extends GuardsDef,
86
- EFD extends EffectsDef,
87
- >(
88
- machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>,
89
- currentState: S,
90
- event: E,
91
- self: MachineRef<E>,
92
- ) {
93
- const transition = resolveTransition(machine, currentState, event);
94
-
95
- if (transition === undefined) {
96
- return {
97
- newState: currentState,
98
- transitioned: false,
99
- reenter: false,
100
- };
101
- }
102
-
103
- const newState = yield* runTransitionHandler(machine, transition, currentState, event, self);
104
-
105
- return {
106
- newState,
107
- transitioned: true,
108
- reenter: transition.reenter === true,
109
- };
110
- });
111
-
112
- // ============================================================================
113
- // Event Processing Core (shared by actor and entity-machine)
114
- // ============================================================================
115
-
116
- /**
117
- * Optional hooks for event processing inspection/tracing.
118
- */
119
- export interface ProcessEventHooks<S, E> {
120
- /** Called before running spawn effects */
121
- readonly onSpawnEffect?: (state: S) => Effect.Effect<void>;
122
- /** Called after transition completes */
123
- readonly onTransition?: (from: S, to: S, event: E) => Effect.Effect<void>;
124
- /** Called when a transition handler or spawn effect fails with a defect */
125
- readonly onError?: (info: ProcessEventError<S, E>) => Effect.Effect<void>;
126
- }
127
-
128
- /**
129
- * Error info for inspection hooks.
130
- */
131
- export interface ProcessEventError<S, E> {
132
- readonly phase: "transition" | "spawn";
133
- readonly state: S;
134
- readonly event: E;
135
- readonly cause: Cause.Cause<unknown>;
136
- }
137
-
138
- /**
139
- * Result of processing an event through the machine.
140
- */
141
- export interface ProcessEventResult<S> {
142
- /** New state after processing */
143
- readonly newState: S;
144
- /** Previous state before processing */
145
- readonly previousState: S;
146
- /** Whether a transition occurred */
147
- readonly transitioned: boolean;
148
- /** Whether lifecycle effects ran (state change or reenter) */
149
- readonly lifecycleRan: boolean;
150
- /** Whether new state is final */
151
- readonly isFinal: boolean;
152
- }
153
-
154
- /**
155
- * Process a single event through the machine.
156
- *
157
- * Handles:
158
- * - Transition execution
159
- * - State scope lifecycle (close old, create new)
160
- * - Running spawn effects
161
- *
162
- * Optional hooks allow inspection/tracing without coupling to specific impl.
163
- *
164
- * @internal
165
- */
166
- export const processEventCore = Effect.fn("effect-machine.processEventCore")(function* <
167
- S extends { readonly _tag: string },
168
- E extends { readonly _tag: string },
169
- R,
170
- GD extends GuardsDef,
171
- EFD extends EffectsDef,
172
- >(
173
- machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>,
174
- currentState: S,
175
- event: E,
176
- self: MachineRef<E>,
177
- stateScopeRef: { current: Scope.CloseableScope },
178
- hooks?: ProcessEventHooks<S, E>,
179
- ) {
180
- // Execute transition (defect-aware)
181
- const result = yield* executeTransition(machine, currentState, event, self).pipe(
182
- Effect.catchAllCause((cause) => {
183
- if (Cause.isInterruptedOnly(cause)) {
184
- return Effect.interrupt;
185
- }
186
- const onError = hooks?.onError;
187
- if (onError === undefined) {
188
- return Effect.failCause(cause).pipe(Effect.orDie);
189
- }
190
- return onError({
191
- phase: "transition",
192
- state: currentState,
193
- event,
194
- cause,
195
- }).pipe(Effect.zipRight(Effect.failCause(cause).pipe(Effect.orDie)));
196
- }),
197
- );
198
-
199
- if (!result.transitioned) {
200
- return {
201
- newState: currentState,
202
- previousState: currentState,
203
- transitioned: false,
204
- lifecycleRan: false,
205
- isFinal: false,
206
- };
207
- }
208
-
209
- const newState = result.newState;
210
- const stateTagChanged = newState._tag !== currentState._tag;
211
- const runLifecycle = stateTagChanged || result.reenter;
212
-
213
- if (runLifecycle) {
214
- // Close old state scope (interrupts spawn fibers)
215
- yield* Scope.close(stateScopeRef.current, Exit.void);
216
-
217
- // Create new state scope
218
- stateScopeRef.current = yield* Scope.make();
219
-
220
- // Hook: transition complete (before spawn effects)
221
- if (hooks?.onTransition !== undefined) {
222
- yield* hooks.onTransition(currentState, newState, event);
223
- }
224
-
225
- // Hook: about to run spawn effects
226
- if (hooks?.onSpawnEffect !== undefined) {
227
- yield* hooks.onSpawnEffect(newState);
228
- }
229
-
230
- // Run spawn effects for new state
231
- const enterEvent = { _tag: INTERNAL_ENTER_EVENT } as E;
232
- yield* runSpawnEffects(
233
- machine,
234
- newState,
235
- enterEvent,
236
- self,
237
- stateScopeRef.current,
238
- hooks?.onError,
239
- );
240
- }
241
-
242
- return {
243
- newState,
244
- previousState: currentState,
245
- transitioned: true,
246
- lifecycleRan: runLifecycle,
247
- isFinal: machine.finalStates.has(newState._tag),
248
- };
249
- });
250
-
251
- /**
252
- * Run spawn effects for a state (forked into state scope, auto-cancelled on state exit).
253
- *
254
- * @internal
255
- */
256
- export const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* <
257
- S extends { readonly _tag: string },
258
- E extends { readonly _tag: string },
259
- R,
260
- GD extends GuardsDef,
261
- EFD extends EffectsDef,
262
- >(
263
- machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>,
264
- state: S,
265
- event: E,
266
- self: MachineRef<E>,
267
- stateScope: Scope.CloseableScope,
268
- onError?: (info: ProcessEventError<S, E>) => Effect.Effect<void>,
269
- ) {
270
- const spawnEffects = findSpawnEffects(machine, state._tag);
271
- const ctx: MachineContext<S, E, MachineRef<E>> = { state, event, self };
272
- const { effects: effectSlots } = machine._slots;
273
- const reportError = onError;
274
-
275
- for (const spawnEffect of spawnEffects) {
276
- // Fork the spawn effect into the state scope - interrupted when scope closes
277
- const effect = (
278
- spawnEffect.handler({ state, event, self, effects: effectSlots }) as Effect.Effect<
279
- void,
280
- never,
281
- R
282
- >
283
- ).pipe(
284
- Effect.provideService(machine.Context, ctx),
285
- Effect.catchAllCause((cause) => {
286
- if (Cause.isInterruptedOnly(cause)) {
287
- return Effect.interrupt;
288
- }
289
- if (reportError === undefined) {
290
- return Effect.failCause(cause).pipe(Effect.orDie);
291
- }
292
- return reportError({
293
- phase: "spawn",
294
- state,
295
- event,
296
- cause,
297
- }).pipe(Effect.zipRight(Effect.failCause(cause).pipe(Effect.orDie)));
298
- }),
299
- );
300
-
301
- yield* Effect.forkScoped(effect).pipe(Effect.provideService(Scope.Scope, stateScope));
302
- }
303
- });
304
-
305
- /**
306
- * Resolve which transition should fire for a given state and event.
307
- * Uses indexed O(1) lookup. First matching transition wins.
308
- */
309
- export const resolveTransition = <
310
- S extends { readonly _tag: string },
311
- E extends { readonly _tag: string },
312
- R,
313
- >(
314
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Schema fields need wide acceptance
315
- machine: Machine<S, E, R, any, any, any, any>,
316
- currentState: S,
317
- event: E,
318
- ): (typeof machine.transitions)[number] | undefined => {
319
- const candidates = findTransitions(machine, currentState._tag, event._tag);
320
- return candidates[0];
321
- };
322
-
323
- // ============================================================================
324
- // Transition Index (O(1) Lookup)
325
- // ============================================================================
326
-
327
- /**
328
- * Index structure: stateTag -> eventTag -> transitions[]
329
- * Array preserves registration order for guard cascade evaluation.
330
- */
331
- type TransitionIndex<S, E, GD extends GuardsDef, EFD extends EffectsDef, R> = Map<
332
- string,
333
- Map<string, Array<Transition<S, E, GD, EFD, R>>>
334
- >;
335
-
336
- /**
337
- * Index for spawn effects: stateTag -> effects[]
338
- */
339
- type SpawnIndex<S, E, EFD extends EffectsDef, R> = Map<string, Array<SpawnEffect<S, E, EFD, R>>>;
340
-
341
- /**
342
- * Combined index for a machine
343
- */
344
- interface MachineIndex<S, E, GD extends GuardsDef, EFD extends EffectsDef, R> {
345
- readonly transitions: TransitionIndex<S, E, GD, EFD, R>;
346
- readonly spawn: SpawnIndex<S, E, EFD, R>;
347
- }
348
-
349
- // Module-level cache - WeakMap allows GC of unreferenced machines
350
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
351
- const indexCache = new WeakMap<object, MachineIndex<any, any, any, any, any>>();
352
-
353
- /**
354
- * Invalidate cached index for a machine (call after mutation).
355
- */
356
- export const invalidateIndex = (machine: object): void => {
357
- indexCache.delete(machine);
358
- };
359
-
360
- /**
361
- * Build transition index from machine definition.
362
- * O(n) where n = number of transitions.
363
- */
364
- const buildTransitionIndex = <
365
- S extends { readonly _tag: string },
366
- E extends { readonly _tag: string },
367
- GD extends GuardsDef,
368
- EFD extends EffectsDef,
369
- R,
370
- >(
371
- transitions: ReadonlyArray<Transition<S, E, GD, EFD, R>>,
372
- ): TransitionIndex<S, E, GD, EFD, R> => {
373
- const index: TransitionIndex<S, E, GD, EFD, R> = new Map();
374
-
375
- for (const t of transitions) {
376
- let stateMap = index.get(t.stateTag);
377
- if (stateMap === undefined) {
378
- stateMap = new Map();
379
- index.set(t.stateTag, stateMap);
380
- }
381
-
382
- let eventList = stateMap.get(t.eventTag);
383
- if (eventList === undefined) {
384
- eventList = [];
385
- stateMap.set(t.eventTag, eventList);
386
- }
387
-
388
- eventList.push(t);
389
- }
390
-
391
- return index;
392
- };
393
-
394
- /**
395
- * Build spawn index from machine definition.
396
- */
397
- const buildSpawnIndex = <
398
- S extends { readonly _tag: string },
399
- E extends { readonly _tag: string },
400
- EFD extends EffectsDef,
401
- R,
402
- >(
403
- effects: ReadonlyArray<SpawnEffect<S, E, EFD, R>>,
404
- ): SpawnIndex<S, E, EFD, R> => {
405
- const index: SpawnIndex<S, E, EFD, R> = new Map();
406
-
407
- for (const e of effects) {
408
- let stateList = index.get(e.stateTag);
409
- if (stateList === undefined) {
410
- stateList = [];
411
- index.set(e.stateTag, stateList);
412
- }
413
- stateList.push(e);
414
- }
415
-
416
- return index;
417
- };
418
-
419
- /**
420
- * Get or build index for a machine.
421
- */
422
- const getIndex = <
423
- S extends { readonly _tag: string },
424
- E extends { readonly _tag: string },
425
- R,
426
- GD extends GuardsDef,
427
- EFD extends EffectsDef,
428
- >(
429
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Schema fields need wide acceptance
430
- machine: Machine<S, E, R, any, any, GD, EFD>,
431
- ): MachineIndex<S, E, GD, EFD, R> => {
432
- let index = indexCache.get(machine) as MachineIndex<S, E, GD, EFD, R> | undefined;
433
- if (index === undefined) {
434
- index = {
435
- transitions: buildTransitionIndex(machine.transitions),
436
- spawn: buildSpawnIndex(machine.spawnEffects),
437
- };
438
- indexCache.set(machine, index);
439
- }
440
- return index;
441
- };
442
-
443
- /**
444
- * Find all transitions matching a state/event pair.
445
- * Returns empty array if no matches.
446
- *
447
- * Accepts both `Machine` and `BuiltMachine`.
448
- * O(1) lookup after first access (index is lazily built).
449
- */
450
- export const findTransitions = <
451
- S extends { readonly _tag: string },
452
- E extends { readonly _tag: string },
453
- R,
454
- GD extends GuardsDef = Record<string, never>,
455
- EFD extends EffectsDef = Record<string, never>,
456
- >(
457
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Schema fields need wide acceptance
458
- input: Machine<S, E, R, any, any, GD, EFD> | BuiltMachine<S, E, R>,
459
- stateTag: string,
460
- eventTag: string,
461
- ): ReadonlyArray<Transition<S, E, GD, EFD, R>> => {
462
- const machine = input instanceof BuiltMachine ? input._inner : input;
463
- const index = getIndex(machine);
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) ?? [];
468
- };
469
-
470
- /**
471
- * Find all spawn effects for a state.
472
- * Returns empty array if no matches.
473
- *
474
- * O(1) lookup after first access (index is lazily built).
475
- */
476
- export const findSpawnEffects = <
477
- S extends { readonly _tag: string },
478
- E extends { readonly _tag: string },
479
- R,
480
- GD extends GuardsDef = Record<string, never>,
481
- EFD extends EffectsDef = Record<string, never>,
482
- >(
483
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Schema fields need wide acceptance
484
- machine: Machine<S, E, R, any, any, GD, EFD>,
485
- stateTag: string,
486
- ): ReadonlyArray<SpawnEffect<S, E, EFD, R>> => {
487
- const index = getIndex(machine);
488
- return index.spawn.get(stateTag) ?? [];
489
- };
@@ -1,80 +0,0 @@
1
- /**
2
- * Internal utilities for effect-machine.
3
- * @internal
4
- */
5
- import type { Effect } from "effect";
6
- import { Effect as E } from "effect";
7
-
8
- // ============================================================================
9
- // Type Helpers
10
- // ============================================================================
11
-
12
- /**
13
- * Extracts _tag from a tagged union member
14
- */
15
- export type TagOf<T> = T extends { readonly _tag: infer Tag } ? Tag : never;
16
-
17
- /**
18
- * Extracts args type from a Data.taggedEnum constructor
19
- */
20
- export type ArgsOf<C> = C extends (args: infer A) => unknown ? A : never;
21
-
22
- /**
23
- * Extracts return type from a Data.taggedEnum constructor
24
- * @internal
25
- */
26
- export type InstanceOf<C> = C extends (...args: unknown[]) => infer R ? R : never;
27
-
28
- /**
29
- * A tagged union constructor (from Data.taggedEnum)
30
- */
31
- export type TaggedConstructor<T extends { readonly _tag: string }> = (args: Omit<T, "_tag">) => T;
32
-
33
- /**
34
- * Transition handler result - either a new state or Effect producing one
35
- */
36
- export type TransitionResult<State, R> = State | Effect.Effect<State, never, R>;
37
-
38
- // ============================================================================
39
- // Constants
40
- // ============================================================================
41
-
42
- /**
43
- * Internal event tags used for lifecycle effect contexts.
44
- * Prefixed with $ to distinguish from user events.
45
- * @internal
46
- */
47
- export const INTERNAL_INIT_EVENT = "$init" as const;
48
- export const INTERNAL_ENTER_EVENT = "$enter" as const;
49
-
50
- // ============================================================================
51
- // Runtime Utilities
52
- // ============================================================================
53
-
54
- /**
55
- * Extract _tag from a tagged value or constructor.
56
- *
57
- * Supports:
58
- * - Plain values with `_tag` (MachineSchema empty structs)
59
- * - Constructors with static `_tag` (MachineSchema non-empty structs)
60
- * - Data.taggedEnum constructors (fallback via instantiation)
61
- */
62
- export const getTag = (
63
- constructorOrValue: { _tag: string } | ((...args: never[]) => { _tag: string }),
64
- ): string => {
65
- // Direct _tag property (values or static on constructors)
66
- if ("_tag" in constructorOrValue && typeof constructorOrValue._tag === "string") {
67
- return constructorOrValue._tag;
68
- }
69
- // Fallback: instantiate (Data.taggedEnum compatibility)
70
- // Try zero-arg first, then empty object for record constructors
71
- try {
72
- return (constructorOrValue as () => { _tag: string })()._tag;
73
- } catch {
74
- return (constructorOrValue as (args: object) => { _tag: string })({})._tag;
75
- }
76
- };
77
-
78
- /** Check if a value is an Effect */
79
- export const isEffect = (value: unknown): value is Effect.Effect<unknown, unknown, unknown> =>
80
- typeof value === "object" && value !== null && E.EffectTypeId in value;