effect-machine 0.1.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.
@@ -0,0 +1,202 @@
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
+ * },
45
+ * })
46
+ * ```
47
+ */
48
+ readonly hooks?: ProcessEventHooks<S, E>;
49
+ }
50
+
51
+ /**
52
+ * Process a single event through the machine using shared core.
53
+ * Returns the new state after processing.
54
+ */
55
+ const processEvent = <
56
+ S extends { readonly _tag: string },
57
+ E extends { readonly _tag: string },
58
+ R,
59
+ GD extends GuardsDef = Record<string, never>,
60
+ EFD extends EffectsDef = Record<string, never>,
61
+ >(
62
+ machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>,
63
+ stateRef: Ref.Ref<S>,
64
+ event: E,
65
+ self: MachineRef<E>,
66
+ stateScopeRef: { current: Scope.CloseableScope },
67
+ hooks?: ProcessEventHooks<S, E>,
68
+ ): Effect.Effect<S, never, R> =>
69
+ Effect.gen(function* () {
70
+ const currentState = yield* Ref.get(stateRef);
71
+
72
+ // Process event using shared core
73
+ const result = yield* processEventCore(
74
+ machine,
75
+ currentState,
76
+ event,
77
+ self,
78
+ stateScopeRef,
79
+ hooks,
80
+ );
81
+
82
+ // Update state ref if transition occurred
83
+ if (result.transitioned) {
84
+ yield* Ref.set(stateRef, result.newState);
85
+ }
86
+
87
+ return result.newState;
88
+ });
89
+
90
+ /**
91
+ * Create an Entity layer that wires a machine to handle RPC calls.
92
+ *
93
+ * The layer:
94
+ * - Maintains state via Ref per entity instance
95
+ * - Resolves transitions using the indexed lookup
96
+ * - Evaluates guards in registration order
97
+ * - Runs lifecycle effects (onEnter/spawn)
98
+ * - Processes internal events from spawn effects
99
+ *
100
+ * @example
101
+ * ```ts
102
+ * const OrderEntity = toEntity(orderMachine, {
103
+ * type: "Order",
104
+ * stateSchema: OrderState,
105
+ * eventSchema: OrderEvent,
106
+ * })
107
+ *
108
+ * const OrderEntityLayer = EntityMachine.layer(OrderEntity, orderMachine, {
109
+ * initializeState: (entityId) => OrderState.Pending({ orderId: entityId }),
110
+ * })
111
+ *
112
+ * // Use in cluster
113
+ * const program = Effect.gen(function* () {
114
+ * const client = yield* ShardingClient.client(OrderEntity)
115
+ * yield* client.Send("order-123", { event: OrderEvent.Ship({ trackingId: "abc" }) })
116
+ * })
117
+ * ```
118
+ */
119
+ export const EntityMachine = {
120
+ /**
121
+ * Create a layer that wires a machine to an Entity.
122
+ *
123
+ * @param entity - Entity created via toEntity()
124
+ * @param machine - Machine with all effects provided
125
+ * @param options - Optional configuration (state initializer, inspection hooks)
126
+ */
127
+ layer: <
128
+ S extends { readonly _tag: string },
129
+ E extends { readonly _tag: string },
130
+ R,
131
+ GD extends GuardsDef,
132
+ EFD extends EffectsDef,
133
+ EntityType extends string,
134
+ Rpcs extends Rpc.Any,
135
+ >(
136
+ entity: Entity.Entity<EntityType, Rpcs>,
137
+ machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>,
138
+ options?: EntityMachineOptions<S, E>,
139
+ ): Layer.Layer<never, never, R> => {
140
+ return entity.toLayer(
141
+ Effect.gen(function* () {
142
+ // Get entity ID from context if available
143
+ const entityId = yield* Effect.serviceOption(Entity.CurrentAddress).pipe(
144
+ Effect.map((opt) => (opt._tag === "Some" ? opt.value.entityId : "")),
145
+ );
146
+
147
+ // Initialize state - use provided initializer or machine's initial state
148
+ const initialState =
149
+ options?.initializeState !== undefined
150
+ ? options.initializeState(entityId)
151
+ : machine.initial;
152
+
153
+ // Create self reference for sending events back to machine
154
+ const internalQueue = yield* Queue.unbounded<E>();
155
+ const self: MachineRef<E> = {
156
+ send: (event) => Queue.offer(internalQueue, event),
157
+ };
158
+
159
+ // Create state ref
160
+ const stateRef = yield* Ref.make<S>(initialState);
161
+
162
+ // Create state scope for spawn effects
163
+ const stateScopeRef: { current: Scope.CloseableScope } = {
164
+ current: yield* Scope.make(),
165
+ };
166
+
167
+ // Use $init event for initial lifecycle
168
+ const initEvent = { _tag: "$init" } as E;
169
+
170
+ // Run initial spawn effects
171
+ yield* runSpawnEffects(machine, initialState, initEvent, self, stateScopeRef.current);
172
+
173
+ // Process internal events in background
174
+ yield* Effect.forkScoped(
175
+ Effect.forever(
176
+ Effect.gen(function* () {
177
+ const event = yield* Queue.take(internalQueue);
178
+ yield* processEvent(machine, stateRef, event, self, stateScopeRef, options?.hooks);
179
+ }),
180
+ ),
181
+ );
182
+
183
+ // Return handlers matching the Entity's RPC protocol
184
+ // The actual types are inferred from the entity definition
185
+ return entity.of({
186
+ Send: (envelope: { payload: { event: E } }) =>
187
+ processEvent(
188
+ machine,
189
+ stateRef,
190
+ envelope.payload.event,
191
+ self,
192
+ stateScopeRef,
193
+ options?.hooks,
194
+ ),
195
+
196
+ GetState: () => Ref.get(stateRef),
197
+ // Entity.of expects handlers matching Rpcs type param - dynamic construction requires cast
198
+ } as unknown as Parameters<typeof entity.of>[0]);
199
+ }),
200
+ ) as unknown as Layer.Layer<never, never, R>;
201
+ },
202
+ };
@@ -0,0 +1,43 @@
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";
@@ -0,0 +1,99 @@
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 ADDED
@@ -0,0 +1,64 @@
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.provide() 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 ADDED
@@ -0,0 +1,102 @@
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
+ MachineRef,
40
+ MakeConfig,
41
+ Transition,
42
+ SpawnEffect,
43
+ BackgroundEffect,
44
+ PersistOptions,
45
+ HandlerContext,
46
+ StateHandlerContext,
47
+ ProvideHandlers,
48
+ } from "./machine.js";
49
+
50
+ // Actor types and system
51
+ export type { ActorRef, ActorSystem } from "./actor.js";
52
+ export { ActorSystem as ActorSystemService, Default as ActorSystemDefault } from "./actor.js";
53
+
54
+ // Testing utilities
55
+ export {
56
+ assertNeverReaches,
57
+ assertPath,
58
+ assertReaches,
59
+ createTestHarness,
60
+ simulate,
61
+ } from "./testing.js";
62
+ export type { SimulationResult, TestHarness, TestHarnessOptions } from "./testing.js";
63
+
64
+ // Inspection
65
+ export type {
66
+ EffectEvent,
67
+ EventReceivedEvent,
68
+ InspectionEvent,
69
+ Inspector,
70
+ SpawnEvent,
71
+ StopEvent,
72
+ TransitionEvent,
73
+ } from "./inspection.js";
74
+ export {
75
+ collectingInspector,
76
+ consoleInspector,
77
+ Inspector as InspectorService,
78
+ makeInspector,
79
+ } from "./inspection.js";
80
+
81
+ // Persistence
82
+ export type {
83
+ ActorMetadata,
84
+ PersistedEvent,
85
+ PersistenceAdapter,
86
+ PersistenceConfig,
87
+ PersistentActorRef,
88
+ PersistentMachine,
89
+ RestoreFailure,
90
+ RestoreResult,
91
+ Snapshot,
92
+ } from "./persistence/index.js";
93
+ export {
94
+ createPersistentActor,
95
+ InMemoryPersistenceAdapter,
96
+ isPersistentMachine,
97
+ makeInMemoryPersistenceAdapter,
98
+ PersistenceAdapterTag,
99
+ PersistenceError,
100
+ restorePersistentActor,
101
+ VersionConflictError,
102
+ } from "./persistence/index.js";
@@ -0,0 +1,132 @@
1
+ import { Context } from "effect";
2
+
3
+ // ============================================================================
4
+ // Inspection Events
5
+ // ============================================================================
6
+
7
+ /**
8
+ * Event emitted when an actor is spawned
9
+ */
10
+ export interface SpawnEvent<S> {
11
+ readonly type: "@machine.spawn";
12
+ readonly actorId: string;
13
+ readonly initialState: S;
14
+ readonly timestamp: number;
15
+ }
16
+
17
+ /**
18
+ * Event emitted when an actor receives an event
19
+ */
20
+ export interface EventReceivedEvent<S, E> {
21
+ readonly type: "@machine.event";
22
+ readonly actorId: string;
23
+ readonly state: S;
24
+ readonly event: E;
25
+ readonly timestamp: number;
26
+ }
27
+
28
+ /**
29
+ * Event emitted when a transition occurs
30
+ */
31
+ export interface TransitionEvent<S, E> {
32
+ readonly type: "@machine.transition";
33
+ readonly actorId: string;
34
+ readonly fromState: S;
35
+ readonly toState: S;
36
+ readonly event: E;
37
+ readonly timestamp: number;
38
+ }
39
+
40
+ /**
41
+ * Event emitted when a spawn effect runs
42
+ */
43
+ export interface EffectEvent<S> {
44
+ readonly type: "@machine.effect";
45
+ readonly actorId: string;
46
+ readonly effectType: "spawn";
47
+ readonly state: S;
48
+ readonly timestamp: number;
49
+ }
50
+
51
+ /**
52
+ * Event emitted when an actor stops
53
+ */
54
+ export interface StopEvent<S> {
55
+ readonly type: "@machine.stop";
56
+ readonly actorId: string;
57
+ readonly finalState: S;
58
+ readonly timestamp: number;
59
+ }
60
+
61
+ /**
62
+ * Union of all inspection events
63
+ */
64
+ export type InspectionEvent<S, E> =
65
+ | SpawnEvent<S>
66
+ | EventReceivedEvent<S, E>
67
+ | TransitionEvent<S, E>
68
+ | EffectEvent<S>
69
+ | StopEvent<S>;
70
+
71
+ // ============================================================================
72
+ // Inspector Service
73
+ // ============================================================================
74
+
75
+ /**
76
+ * Inspector interface for observing machine behavior
77
+ */
78
+ export interface Inspector<S, E> {
79
+ readonly onInspect: (event: InspectionEvent<S, E>) => void;
80
+ }
81
+
82
+ /**
83
+ * Inspector service tag - optional service for machine introspection
84
+ * Uses `any` types to allow variance flexibility when providing the service
85
+ */
86
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
87
+ export const Inspector = Context.GenericTag<Inspector<any, any>>("@effect/machine/Inspector");
88
+
89
+ /**
90
+ * Create an inspector from a callback function
91
+ */
92
+ export const makeInspector = <S, E>(
93
+ onInspect: (event: InspectionEvent<S, E>) => void,
94
+ ): Inspector<S, E> => ({ onInspect });
95
+
96
+ // ============================================================================
97
+ // Built-in Inspectors
98
+ // ============================================================================
99
+
100
+ /**
101
+ * Console inspector that logs events in a readable format
102
+ */
103
+ export const consoleInspector = <
104
+ S extends { readonly _tag: string },
105
+ E extends { readonly _tag: string },
106
+ >(): Inspector<S, E> =>
107
+ makeInspector((event) => {
108
+ const prefix = `[${event.actorId}]`;
109
+ switch (event.type) {
110
+ case "@machine.spawn":
111
+ console.log(prefix, "spawned →", event.initialState._tag);
112
+ break;
113
+ case "@machine.event":
114
+ console.log(prefix, "received", event.event._tag, "in", event.state._tag);
115
+ break;
116
+ case "@machine.transition":
117
+ console.log(prefix, event.fromState._tag, "→", event.toState._tag);
118
+ break;
119
+ case "@machine.effect":
120
+ console.log(prefix, event.effectType, "effect in", event.state._tag);
121
+ break;
122
+ case "@machine.stop":
123
+ console.log(prefix, "stopped in", event.finalState._tag);
124
+ break;
125
+ }
126
+ });
127
+
128
+ /**
129
+ * Collecting inspector that stores events in an array for testing
130
+ */
131
+ export const collectingInspector = <S, E>(events: InspectionEvent<S, E>[]): Inspector<S, E> =>
132
+ makeInspector((event) => events.push(event));
@@ -0,0 +1,51 @@
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);