effect-machine 0.17.1 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -54
  3. package/dist/actor.d.ts +8 -45
  4. package/dist/actor.js +157 -231
  5. package/dist/atom.d.ts +31 -0
  6. package/dist/atom.js +29 -0
  7. package/dist/cluster/adapters/in-memory.d.ts +0 -1
  8. package/dist/cluster/adapters/in-memory.js +10 -5
  9. package/dist/cluster/entity-actor-ref.d.ts +2 -3
  10. package/dist/cluster/entity-actor-ref.js +14 -17
  11. package/dist/cluster/entity-machine.d.ts +2 -8
  12. package/dist/cluster/entity-machine.js +57 -34
  13. package/dist/cluster/index.js +1 -1
  14. package/dist/cluster/persistence.d.ts +0 -1
  15. package/dist/cluster/to-entity.d.ts +19 -21
  16. package/dist/cluster/to-entity.js +18 -19
  17. package/dist/errors.d.ts +11 -37
  18. package/dist/errors.js +12 -30
  19. package/dist/index.d.ts +4 -5
  20. package/dist/index.js +4 -5
  21. package/dist/inspection.d.ts +0 -1
  22. package/dist/inspection.js +24 -13
  23. package/dist/internal/brands.d.ts +0 -1
  24. package/dist/internal/event-advancement.d.ts +50 -0
  25. package/dist/internal/event-advancement.js +79 -0
  26. package/dist/internal/inspection.d.ts +5 -9
  27. package/dist/internal/inspection.js +31 -10
  28. package/dist/internal/machine-definition.d.ts +16 -0
  29. package/dist/internal/runtime.d.ts +1 -161
  30. package/dist/internal/runtime.js +191 -150
  31. package/dist/internal/transition.d.ts +4 -135
  32. package/dist/internal/transition.js +82 -157
  33. package/dist/internal/utils.d.ts +3 -40
  34. package/dist/internal/utils.js +2 -2
  35. package/dist/machine.d.ts +47 -137
  36. package/dist/machine.js +147 -215
  37. package/dist/schema.d.ts +42 -21
  38. package/dist/schema.js +28 -18
  39. package/dist/supervision.d.ts +1 -24
  40. package/dist/supervision.js +2 -3
  41. package/dist/testing.d.ts +15 -29
  42. package/dist/testing.js +76 -95
  43. package/package.json +41 -35
  44. package/dist/slot.d.ts +0 -159
  45. package/dist/slot.js +0 -165
  46. package/v3/dist/_virtual/_rolldown/runtime.js +0 -13
  47. package/v3/dist/actor.d.ts +0 -250
  48. package/v3/dist/actor.js +0 -577
  49. package/v3/dist/cluster/adapters/in-memory.d.ts +0 -15
  50. package/v3/dist/cluster/adapters/in-memory.js +0 -62
  51. package/v3/dist/cluster/entity-actor-ref.d.ts +0 -49
  52. package/v3/dist/cluster/entity-actor-ref.js +0 -19
  53. package/v3/dist/cluster/entity-machine.d.ts +0 -74
  54. package/v3/dist/cluster/entity-machine.js +0 -166
  55. package/v3/dist/cluster/index.d.ts +0 -6
  56. package/v3/dist/cluster/index.js +0 -6
  57. package/v3/dist/cluster/persistence.d.ts +0 -48
  58. package/v3/dist/cluster/persistence.js +0 -14
  59. package/v3/dist/cluster/to-entity.d.ts +0 -69
  60. package/v3/dist/cluster/to-entity.js +0 -59
  61. package/v3/dist/errors.d.ts +0 -95
  62. package/v3/dist/errors.js +0 -54
  63. package/v3/dist/index.d.ts +0 -11
  64. package/v3/dist/index.js +0 -9
  65. package/v3/dist/inspection.d.ts +0 -151
  66. package/v3/dist/inspection.js +0 -128
  67. package/v3/dist/internal/brands.d.ts +0 -50
  68. package/v3/dist/internal/inspection.d.ts +0 -11
  69. package/v3/dist/internal/inspection.js +0 -20
  70. package/v3/dist/internal/runtime.d.ts +0 -161
  71. package/v3/dist/internal/runtime.js +0 -360
  72. package/v3/dist/internal/transition.d.ts +0 -190
  73. package/v3/dist/internal/transition.js +0 -278
  74. package/v3/dist/internal/utils.d.ts +0 -101
  75. package/v3/dist/internal/utils.js +0 -75
  76. package/v3/dist/machine.d.ts +0 -398
  77. package/v3/dist/machine.js +0 -487
  78. package/v3/dist/schema.d.ts +0 -174
  79. package/v3/dist/schema.js +0 -206
  80. package/v3/dist/slot.d.ts +0 -158
  81. package/v3/dist/slot.js +0 -165
  82. package/v3/dist/supervision.d.ts +0 -97
  83. package/v3/dist/supervision.js +0 -42
  84. package/v3/dist/testing.d.ts +0 -151
  85. package/v3/dist/testing.js +0 -189
  86. /package/{v3/dist/internal/brands.js → dist/internal/machine-definition.js} +0 -0
@@ -1,487 +0,0 @@
1
- import { __exportAll } from "./_virtual/_rolldown/runtime.js";
2
- import { getTag, makeDeferReply, makeReply, stubSystem } from "./internal/utils.js";
3
- import { ProvisionValidationError, SlotCodecError, SlotProvisionError } from "./errors.js";
4
- import { findTransitions, invalidateIndex, resolveTransition, runTransitionHandler, shouldPostpone } from "./internal/transition.js";
5
- import { emitWithTimestamp } from "./internal/inspection.js";
6
- import { Inspector } from "./inspection.js";
7
- import { MachineContextTag } from "./slot.js";
8
- import { ActorScope, createActor } from "./actor.js";
9
- import { Cause, Effect, Exit, Option, Random, Schema, Scope } from "effect";
10
- //#region src/machine.ts
11
- var machine_exports = /* @__PURE__ */ __exportAll({
12
- Machine: () => Machine,
13
- deferReply: () => deferReply,
14
- findTransitions: () => findTransitions,
15
- make: () => make,
16
- materializeMachine: () => materializeMachine,
17
- replay: () => replay,
18
- reply: () => reply,
19
- scoped: () => scoped,
20
- spawn: () => spawn
21
- });
22
- const emitTaskInspection = (input) => Effect.flatMap(Effect.serviceOption(Inspector), (inspector) => Option.isNone(inspector) ? Effect.void : emitWithTimestamp(inspector.value, (timestamp) => ({
23
- type: "@machine.task",
24
- actorId: input.actorId,
25
- state: input.state,
26
- taskName: input.taskName,
27
- phase: input.phase,
28
- error: input.error,
29
- timestamp
30
- })));
31
- /**
32
- * Bind slot handlers to a machine, returning a fresh copy with handlers installed.
33
- * If no handlers provided and machine has no slots, returns the machine as-is.
34
- * Validates that all required slots are provided and no extra slots are given.
35
- *
36
- * @internal — used by spawn, replay, simulate, test harness, entity-machine
37
- */
38
- const materializeMachine = (machine, handlers) => {
39
- if (handlers === void 0) {
40
- if (machine._slotsSchema !== void 0 && Object.keys(machine._slotsSchema.definitions).length > 0) throw new ProvisionValidationError({
41
- missing: Object.keys(machine._slotsSchema.definitions),
42
- extra: []
43
- });
44
- return machine;
45
- }
46
- const requiredSlots = /* @__PURE__ */ new Set();
47
- if (machine._slotsSchema !== void 0) for (const name of Object.keys(machine._slotsSchema.definitions)) requiredSlots.add(name);
48
- const providedSlots = new Set(Object.keys(handlers));
49
- const missing = [];
50
- const extra = [];
51
- for (const name of requiredSlots) if (!providedSlots.has(name)) missing.push(name);
52
- for (const name of providedSlots) if (!requiredSlots.has(name)) extra.push(name);
53
- if (missing.length > 0 || extra.length > 0) throw new ProvisionValidationError({
54
- missing,
55
- extra
56
- });
57
- const result = new Machine(machine.initial, machine.stateSchema, machine.eventSchema, machine._slotsSchema, machine._slotValidation);
58
- result._transitions = [...machine._transitions];
59
- result._finalStates = new Set(machine._finalStates);
60
- result._spawnEffects = [...machine._spawnEffects];
61
- result._backgroundEffects = [...machine._backgroundEffects];
62
- result._postponeRules = [...machine._postponeRules];
63
- result._replySchemas = machine._replySchemas;
64
- if (machine._slotsSchema !== void 0) for (const name of Object.keys(machine._slotsSchema.definitions)) result._slotHandlers.set(name, handlers[name]);
65
- return result;
66
- };
67
- /**
68
- * Machine definition with fluent builder API.
69
- *
70
- * Type parameters:
71
- * - `State`: The state union type
72
- * - `Event`: The event union type
73
- * - `R`: Effect requirements
74
- * - `_SD`: State schema definition (for compile-time validation)
75
- * - `_ED`: Event schema definition (for compile-time validation)
76
- * - `SD`: Slot definitions
77
- */
78
- var Machine = class Machine {
79
- initial;
80
- /** @internal */ _transitions;
81
- /** @internal */ _spawnEffects;
82
- /** @internal */ _backgroundEffects;
83
- /** @internal */ _finalStates;
84
- /** @internal */ _postponeRules;
85
- /** @internal */ _slotsSchema;
86
- /** @internal */ _slotHandlers;
87
- /** @internal */ _slots;
88
- /** @internal */ _slotValidation;
89
- stateSchema;
90
- eventSchema;
91
- /** @internal */ _replySchemas;
92
- /**
93
- * Context tag for accessing machine state/event/self in slot handlers.
94
- * Uses shared module-level tag for all machines.
95
- */
96
- Context = MachineContextTag;
97
- get transitions() {
98
- return this._transitions;
99
- }
100
- get spawnEffects() {
101
- return this._spawnEffects;
102
- }
103
- get backgroundEffects() {
104
- return this._backgroundEffects;
105
- }
106
- get finalStates() {
107
- return this._finalStates;
108
- }
109
- get postponeRules() {
110
- return this._postponeRules;
111
- }
112
- get slotsSchema() {
113
- return this._slotsSchema;
114
- }
115
- get replySchemas() {
116
- return this._replySchemas;
117
- }
118
- /** @internal */
119
- constructor(initial, stateSchema, eventSchema, slotsSchema, slotValidation = true) {
120
- this.initial = initial;
121
- this._transitions = [];
122
- this._spawnEffects = [];
123
- this._backgroundEffects = [];
124
- this._finalStates = /* @__PURE__ */ new Set();
125
- this._postponeRules = [];
126
- this._slotsSchema = slotsSchema;
127
- this._replySchemas = eventSchema?._replySchemas ?? /* @__PURE__ */ new Map();
128
- this._slotHandlers = /* @__PURE__ */ new Map();
129
- this._slotValidation = slotValidation;
130
- this.stateSchema = stateSchema;
131
- this.eventSchema = eventSchema;
132
- const validators = slotValidation && slotsSchema !== void 0 ? new Map(Object.entries(slotsSchema.definitions).map(([name, def]) => [name, {
133
- decodeInput: Schema.decodeUnknownSync(def.inputSchema),
134
- decodeOutput: Schema.decodeUnknownSync(def.outputSchema)
135
- }])) : void 0;
136
- const resolve = (name, params) => Effect.flatMap(Effect.serviceOption(this.Context), (maybeCtx) => {
137
- if (Option.isNone(maybeCtx)) return Effect.die("MachineContext not available");
138
- const handler = this._slotHandlers.get(name);
139
- if (handler === void 0) return Effect.die(new SlotProvisionError({
140
- slotName: name,
141
- slotType: "slot"
142
- }));
143
- const validatedParams = validators !== void 0 ? (() => {
144
- try {
145
- const v = validators.get(name);
146
- return v !== void 0 ? v.decodeInput(params) : params;
147
- } catch (e) {
148
- return Effect.die(new SlotCodecError({
149
- slotName: name,
150
- phase: "input",
151
- message: e instanceof Error ? e.message : String(e)
152
- }));
153
- }
154
- })() : params;
155
- if (Effect.isEffect(validatedParams)) return validatedParams;
156
- const result = handler(validatedParams);
157
- let resultEffect;
158
- if (result === void 0 || result === null) resultEffect = Effect.void;
159
- else if (Effect.isEffect(result)) resultEffect = result;
160
- else resultEffect = Effect.succeed(result);
161
- if (validators !== void 0) {
162
- const v = validators.get(name);
163
- if (v !== void 0) return Effect.flatMap(resultEffect, (value) => {
164
- try {
165
- const decoded = v.decodeOutput(value);
166
- return Effect.succeed(decoded);
167
- } catch (e) {
168
- return Effect.die(new SlotCodecError({
169
- slotName: name,
170
- phase: "output",
171
- message: e instanceof Error ? e.message : String(e)
172
- }));
173
- }
174
- });
175
- }
176
- return resultEffect;
177
- });
178
- this._slots = this._slotsSchema !== void 0 ? this._slotsSchema._createSlots(resolve) : {};
179
- }
180
- from(stateOrStates, build) {
181
- const states = Array.isArray(stateOrStates) ? stateOrStates : [stateOrStates];
182
- build(new TransitionScope(this, states));
183
- return this;
184
- }
185
- /** @internal */
186
- scopeTransition(states, event, handler, reenter) {
187
- for (const state of states) this.addTransition(state, event, handler, reenter);
188
- return this;
189
- }
190
- on(stateOrStates, event, handler) {
191
- const states = Array.isArray(stateOrStates) ? stateOrStates : [stateOrStates];
192
- for (const s of states) this.addTransition(s, event, handler, false);
193
- return this;
194
- }
195
- reenter(stateOrStates, event, handler) {
196
- const states = Array.isArray(stateOrStates) ? stateOrStates : [stateOrStates];
197
- for (const s of states) this.addTransition(s, event, handler, true);
198
- return this;
199
- }
200
- /**
201
- * Register a wildcard transition that fires from any state when no specific transition matches.
202
- * Specific `.on()` transitions always take priority over `.onAny()`.
203
- */
204
- onAny(event, handler) {
205
- const transition = {
206
- stateTag: "*",
207
- eventTag: getTag(event),
208
- handler,
209
- reenter: false
210
- };
211
- this._transitions.push(transition);
212
- invalidateIndex(this);
213
- return this;
214
- }
215
- /** @internal */
216
- addTransition(state, event, handler, reenter) {
217
- const transition = {
218
- stateTag: getTag(state),
219
- eventTag: getTag(event),
220
- handler,
221
- reenter
222
- };
223
- this._transitions.push(transition);
224
- invalidateIndex(this);
225
- return this;
226
- }
227
- spawn(stateOrStates, handler) {
228
- const states = Array.isArray(stateOrStates) ? stateOrStates : [stateOrStates];
229
- for (const s of states) {
230
- const stateTag = getTag(s);
231
- this._spawnEffects.push({
232
- stateTag,
233
- handler
234
- });
235
- }
236
- invalidateIndex(this);
237
- return this;
238
- }
239
- task(stateOrStates, run, options) {
240
- const handler = Effect.fn("effect-machine.task")(function* (ctx) {
241
- yield* emitTaskInspection({
242
- actorId: ctx.actorId,
243
- state: ctx.state,
244
- taskName: options.name,
245
- phase: "start"
246
- });
247
- const exit = yield* Effect.exit(run(ctx));
248
- if (Exit.isSuccess(exit)) {
249
- yield* emitTaskInspection({
250
- actorId: ctx.actorId,
251
- state: ctx.state,
252
- taskName: options.name,
253
- phase: "success"
254
- });
255
- const successEvent = options.onSuccess !== void 0 ? options.onSuccess(exit.value, ctx) : exit.value;
256
- yield* ctx.self.send(successEvent);
257
- yield* Effect.yieldNow();
258
- return;
259
- }
260
- const cause = exit.cause;
261
- if (Cause.isInterruptedOnly(cause)) {
262
- yield* emitTaskInspection({
263
- actorId: ctx.actorId,
264
- state: ctx.state,
265
- taskName: options.name,
266
- phase: "interrupt"
267
- });
268
- return;
269
- }
270
- yield* emitTaskInspection({
271
- actorId: ctx.actorId,
272
- state: ctx.state,
273
- taskName: options.name,
274
- phase: "failure",
275
- error: Cause.pretty(cause)
276
- });
277
- if (options.onFailure !== void 0) {
278
- yield* ctx.self.send(options.onFailure(cause, ctx));
279
- yield* Effect.yieldNow();
280
- return;
281
- }
282
- return yield* Effect.failCause(cause).pipe(Effect.orDie);
283
- });
284
- return this.spawn(stateOrStates, handler);
285
- }
286
- /**
287
- * State timeout — gen_statem's `state_timeout`.
288
- *
289
- * Entering the state starts a timer. Leaving cancels it (via state scope).
290
- * `.reenter()` restarts the timer with fresh state values.
291
- * Compiles to `.task()` internally — preserves `@machine.task` inspection events.
292
- *
293
- * @example
294
- * ```ts
295
- * machine
296
- * .timeout(State.Loading, {
297
- * duration: Duration.seconds(30),
298
- * event: Event.Timeout,
299
- * })
300
- * // Dynamic duration from state
301
- * .timeout(State.Retrying, {
302
- * duration: (state) => Duration.seconds(state.backoff),
303
- * event: Event.GiveUp,
304
- * })
305
- * ```
306
- */
307
- timeout(state, config) {
308
- const stateTag = getTag(state);
309
- const resolveDuration = typeof config.duration === "function" ? config.duration : () => config.duration;
310
- const resolveEvent = typeof config.event === "function" ? config.event : () => config.event;
311
- return this.task(state, (ctx) => Effect.sleep(resolveDuration(ctx.state)), {
312
- onSuccess: (_, ctx) => resolveEvent(ctx.state),
313
- name: `$timeout:${stateTag}`
314
- });
315
- }
316
- /**
317
- * Machine-lifetime effect that is forked on actor spawn and runs until the actor stops.
318
- *
319
- * @example
320
- * ```ts
321
- * machine.background(({ self }) =>
322
- * Effect.forever(
323
- * Effect.sleep("30 seconds").pipe(Effect.andThen(self.send(Event.Ping))),
324
- * ),
325
- * );
326
- * ```
327
- */
328
- background(handler) {
329
- this._backgroundEffects.push({ handler });
330
- return this;
331
- }
332
- /**
333
- * Postpone events — gen_statem's event postpone.
334
- *
335
- * When a matching event arrives in the given state, it is buffered instead of
336
- * processed. After the next state transition (tag change), all buffered events
337
- * are drained through the loop in FIFO order.
338
- *
339
- * Reply-bearing events (from `call`/`ask`) in the postpone buffer are settled
340
- * with `ActorStoppedError` on stop/interrupt/final-state.
341
- *
342
- * @example
343
- * ```ts
344
- * machine
345
- * .postpone(State.Connecting, Event.Data) // single event
346
- * .postpone(State.Connecting, [Event.Data, Event.Cmd]) // multiple events
347
- * ```
348
- */
349
- postpone(state, events) {
350
- const stateTag = getTag(state);
351
- const eventList = Array.isArray(events) ? events : [events];
352
- for (const ev of eventList) {
353
- const eventTag = getTag(ev);
354
- this._postponeRules.push({
355
- stateTag,
356
- eventTag
357
- });
358
- }
359
- return this;
360
- }
361
- final(state) {
362
- const stateTag = getTag(state);
363
- this._finalStates.add(stateTag);
364
- return this;
365
- }
366
- static make(config) {
367
- return new Machine(config.initial, config.state, config.event, config.slots, config.slotValidation ?? true);
368
- }
369
- };
370
- var TransitionScope = class {
371
- constructor(machine, states) {
372
- this.machine = machine;
373
- this.states = states;
374
- }
375
- on(event, handler) {
376
- this.machine.scopeTransition(this.states, event, handler, false);
377
- return this;
378
- }
379
- reenter(event, handler) {
380
- this.machine.scopeTransition(this.states, event, handler, true);
381
- return this;
382
- }
383
- };
384
- const make = Machine.make;
385
- /**
386
- * Spawn an actor from a machine.
387
- *
388
- * For machines with slots, pass implementations via `{ slots: { ... } }`.
389
- *
390
- * @example
391
- * ```ts
392
- * // No slots
393
- * const actor = yield* Machine.spawn(machine);
394
- *
395
- * // With slots
396
- * const actor = yield* Machine.spawn(machine, {
397
- * slots: { canRetry: ({ max }) => attempts < max },
398
- * });
399
- *
400
- * // With lifecycle (recovery + durability)
401
- * const actor = yield* Machine.spawn(machine, {
402
- * lifecycle: {
403
- * recovery: { resolve: ({ machineInitial }) => storage.get("actor-state") },
404
- * durability: { save: ({ nextState }) => storage.set("actor-state", nextState) },
405
- * },
406
- * });
407
- * ```
408
- */
409
- const spawn = Effect.fn("effect-machine.spawn")(function* (machine, idOrOptions) {
410
- const opts = typeof idOrOptions === "string" ? { id: idOrOptions } : idOrOptions;
411
- const actor = yield* createActor(opts?.id ?? `actor-${(yield* Random.next).toString(36).slice(2)}`, materializeMachine(machine, opts?.slots), {
412
- initialState: opts?.hydrate,
413
- supervision: opts?.supervision,
414
- lifecycle: opts?.lifecycle
415
- });
416
- const maybeScope = yield* Effect.serviceOption(ActorScope);
417
- if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, actor.stop);
418
- return actor;
419
- });
420
- /**
421
- * Wrap an effect to provide an `ActorScope` from the current `Scope`.
422
- *
423
- * Actors spawned inside will attach cleanup finalizers to this scope,
424
- * so they are automatically stopped when the scope closes.
425
- *
426
- * @example
427
- * ```ts
428
- * yield* Effect.scoped(
429
- * Machine.scoped(
430
- * Effect.gen(function* () {
431
- * const actor = yield* Machine.spawn(machine);
432
- * yield* actor.start;
433
- * // actor auto-stopped when scope closes
434
- * }),
435
- * ),
436
- * );
437
- * ```
438
- */
439
- const scoped = (effect) => Effect.flatMap(Scope.Scope, (scope) => Effect.provideService(effect, ActorScope, scope));
440
- const replay = Effect.fn("effect-machine.replay")(function* (input, events, options) {
441
- const machine = materializeMachine(input, options?.slots);
442
- let state = options?.from ?? machine.initial;
443
- const hasPostponeRules = machine.postponeRules.length > 0;
444
- const postponed = [];
445
- const dummySend = Effect.fn("effect-machine.replay.send")((_event) => Effect.void);
446
- const self = {
447
- send: dummySend,
448
- cast: dummySend,
449
- spawn: () => Effect.die("spawn not supported in replay"),
450
- reply: () => Effect.succeed(false)
451
- };
452
- for (const event of events) {
453
- if (machine.finalStates.has(state._tag)) break;
454
- if (hasPostponeRules && shouldPostpone(machine, state._tag, event._tag)) {
455
- postponed.push(event);
456
- continue;
457
- }
458
- const transition = resolveTransition(machine, state, event);
459
- if (transition !== void 0) {
460
- const result = yield* runTransitionHandler(machine, transition, state, event, self, stubSystem, "replay");
461
- const previousTag = state._tag;
462
- state = result.newState;
463
- if ((state._tag !== previousTag || transition.reenter === true) && postponed.length > 0) {
464
- let drainTag = previousTag;
465
- while (state._tag !== drainTag && postponed.length > 0) {
466
- if (machine.finalStates.has(state._tag)) break;
467
- drainTag = state._tag;
468
- const drained = postponed.splice(0);
469
- for (const postponedEvent of drained) {
470
- if (machine.finalStates.has(state._tag)) break;
471
- if (shouldPostpone(machine, state._tag, postponedEvent._tag)) {
472
- postponed.push(postponedEvent);
473
- continue;
474
- }
475
- const pTransition = resolveTransition(machine, state, postponedEvent);
476
- if (pTransition !== void 0) state = (yield* runTransitionHandler(machine, pTransition, state, postponedEvent, self, stubSystem, "replay")).newState;
477
- }
478
- }
479
- }
480
- }
481
- }
482
- return state;
483
- });
484
- const reply = makeReply;
485
- const deferReply = makeDeferReply;
486
- //#endregion
487
- export { Machine, deferReply, findTransitions, machine_exports, make, materializeMachine, replay, reply, scoped, spawn };
@@ -1,174 +0,0 @@
1
- import { FullEventBrand, FullStateBrand, ReplyTypeBrand } from "./internal/brands.js";
2
- import { Schema } from "effect";
3
-
4
- //#region src/schema.d.ts
5
- declare const ReplySchemaSymbol: unique symbol;
6
- type ReplySchemaSymbol = typeof ReplySchemaSymbol;
7
- /**
8
- * Fields annotated with a reply schema.
9
- * Structurally identical to Schema.Struct.Fields at runtime,
10
- * but carries the reply schema type at compile time.
11
- */
12
- type ReplyFields<F extends Schema.Struct.Fields, RS extends Schema.Schema.Any> = F & {
13
- readonly [ReplySchemaSymbol]: RS;
14
- };
15
- /**
16
- * Payload fields that actually flow through constructors and runtime values.
17
- * Reply schema metadata is type-only and must not leak into payload shapes.
18
- */
19
- type PayloadFields<F extends Schema.Struct.Fields> = { readonly [K in keyof F as K extends ReplySchemaSymbol ? never : K]: F[K] };
20
- /**
21
- * Extract the TypeScript type from a TaggedStruct schema
22
- */
23
- type TaggedStructType<Tag extends string, Fields extends Schema.Struct.Fields> = Schema.Schema.Type<Schema.TaggedStruct<Tag, PayloadFields<Fields>>>;
24
- /**
25
- * Build variant schemas type from definition
26
- */
27
- type VariantSchemas<D extends Record<string, Schema.Struct.Fields>> = { readonly [K in keyof D & string]: Schema.TaggedStruct<K, PayloadFields<D[K]>> };
28
- /**
29
- * Build union type from variant schemas.
30
- * Reply-bearing variants carry ReplyTypeBrand<R> for ask() inference.
31
- */
32
- type VariantsUnion<D extends Record<string, Schema.Struct.Fields>> = { [K in keyof D & string]: TaggedStructType<K, D[K]> & (D[K] extends {
33
- readonly [ReplySchemaSymbol]: Schema.Schema<infer R>;
34
- } ? ReplyTypeBrand<R> : unknown) }[keyof D & string];
35
- /**
36
- * Check if fields are empty (no required string properties).
37
- * Symbol keys (like ReplySchemaSymbol) are metadata, not payload fields.
38
- */
39
- type IsEmptyFields<Fields extends Schema.Struct.Fields> = string & keyof Fields extends never ? true : false;
40
- /**
41
- * Resolve the reply brand for a variant's fields.
42
- * If fields carry ReplySchemaSymbol, adds ReplyTypeBrand<R>.
43
- */
44
- type VariantReplyBrand<Fields extends Schema.Struct.Fields> = Fields extends {
45
- readonly [ReplySchemaSymbol]: Schema.Schema<infer R>;
46
- } ? ReplyTypeBrand<R> : unknown;
47
- /**
48
- * Constructor functions for each variant.
49
- * Empty structs: plain values with `_tag`: `State.Idle`
50
- * Non-empty structs require args: `State.Loading({ url })`
51
- *
52
- * Each variant also has a `with` method for constructing from a source object,
53
- * copying matching fields and overriding with a partial.
54
- * The source type uses `object` to accept branded state types without index signature issues.
55
- * Reply-bearing variants carry ReplyTypeBrand<R> for ask() type inference.
56
- */
57
- type VariantConstructors<D extends Record<string, Schema.Struct.Fields>, Brand> = { readonly [K in keyof D & string]: IsEmptyFields<D[K]> extends true ? TaggedStructType<K, D[K]> & Brand & VariantReplyBrand<D[K]> & {
58
- readonly with: (source: object) => TaggedStructType<K, D[K]> & Brand;
59
- } : ((args: Schema.Struct.Type<PayloadFields<D[K]>>) => TaggedStructType<K, D[K]> & Brand & VariantReplyBrand<D[K]>) & {
60
- readonly with: (source: object, partial?: Partial<Schema.Struct.Type<PayloadFields<D[K]>>>) => TaggedStructType<K, D[K]> & Brand;
61
- readonly _tag: K;
62
- } };
63
- /**
64
- * Keys present in ALL variants (intersection of field names).
65
- * Used by union-level `with` to accept only fields safe to update
66
- * regardless of which variant the source is.
67
- */
68
- type SharedKeys<D extends Record<string, Schema.Struct.Fields>> = keyof D[keyof D & string] & string;
69
- /**
70
- * Pattern matching cases type
71
- */
72
- type MatchCases<D extends Record<string, Schema.Struct.Fields>, R> = { readonly [K in keyof D & string]: (value: TaggedStructType<K, D[K]>) => R };
73
- /**
74
- * Base schema interface with pattern matching helpers
75
- */
76
- interface MachineSchemaBase<D extends Record<string, Schema.Struct.Fields>, Brand> {
77
- /**
78
- * Raw definition record for introspection
79
- */
80
- readonly _definition: D;
81
- /**
82
- * Per-variant schemas for fine-grained operations
83
- */
84
- readonly variants: VariantSchemas<D>;
85
- /**
86
- * Type guard: `OrderState.$is("Pending")(value)`
87
- */
88
- readonly $is: <Tag extends keyof D & string>(tag: Tag) => (u: unknown) => u is TaggedStructType<Tag, D[Tag]> & Brand;
89
- /**
90
- * Pattern matching (curried and uncurried)
91
- */
92
- readonly $match: {
93
- <R>(cases: MatchCases<D, R>): (value: VariantsUnion<D> & Brand) => R;
94
- <R>(value: VariantsUnion<D> & Brand, cases: MatchCases<D, R>): R;
95
- };
96
- /**
97
- * Copy fields from `source` into the same variant, overriding with `partial`.
98
- * Preserves the specific variant subtype in the return.
99
- *
100
- * The partial accepts fields common to all variants, so it works safely
101
- * when `S` is a generic type parameter (e.g., `<S extends MyState>`).
102
- *
103
- * @example
104
- * ```ts
105
- * // Per-variant field update
106
- * const next = MyState.Streaming.with(state, { draft: newDraft })
107
- *
108
- * // Cross-variant shared field — works with generic state
109
- * const updateQueue = <S extends MyState>(state: S, queue: Queue): S =>
110
- * MyState.with(state, { queue })
111
- * ```
112
- */
113
- readonly with: <S extends VariantsUnion<D> & Brand>(source: S, partial?: Partial<Record<SharedKeys<D>, unknown>>) => S;
114
- /**
115
- * Reply schemas per variant tag. Only populated for event schemas
116
- * with variants defined via `Event.reply()`.
117
- */
118
- readonly _replySchemas: ReadonlyMap<string, Schema.Schema.Any>;
119
- }
120
- /**
121
- * Schema-first state definition that provides:
122
- * - Schema for encode/decode/validate
123
- * - Variant constructors: `OrderState.Pending({ orderId: "x" })`
124
- * - Pattern matching: `$is`, `$match`
125
- * - Type inference: `typeof OrderState.Type`
126
- *
127
- * The D type parameter captures the definition, creating a unique brand
128
- * per distinct schema definition shape.
129
- */
130
- type MachineStateSchema<D extends Record<string, Schema.Struct.Fields>> = Schema.Schema<VariantsUnion<D> & FullStateBrand<D>, unknown> & MachineSchemaBase<D, FullStateBrand<D>> & VariantConstructors<D, FullStateBrand<D>> & {
131
- /** Schema for persistence, config, and registration. */readonly schema: Schema.Schema<VariantsUnion<D> & FullStateBrand<D>>;
132
- };
133
- /**
134
- * Schema-first event definition (same structure as state, different brand)
135
- *
136
- * The D type parameter captures the definition, creating a unique brand
137
- * per distinct schema definition shape.
138
- */
139
- type MachineEventSchema<D extends Record<string, Schema.Struct.Fields>> = Schema.Schema<VariantsUnion<D> & FullEventBrand<D>, unknown> & MachineSchemaBase<D, FullEventBrand<D>> & VariantConstructors<D, FullEventBrand<D>>;
140
- /**
141
- * Create a schema-first State definition.
142
- *
143
- * The schema's definition type D creates a unique brand, preventing
144
- * accidental use of constructors from different state schemas
145
- * (unless they have identical definitions).
146
- *
147
- * @example
148
- * ```ts
149
- * const OrderState = MachineSchema.State({
150
- * Pending: { orderId: Schema.String },
151
- * Shipped: { trackingId: Schema.String },
152
- * })
153
- *
154
- * type OrderState = typeof OrderState.Type
155
- *
156
- * // Construct
157
- * const s = OrderState.Pending({ orderId: "123" })
158
- *
159
- * // Pattern match
160
- * OrderState.$match(s, {
161
- * Pending: (v) => v.orderId,
162
- * Shipped: (v) => v.trackingId,
163
- * })
164
- *
165
- * // Validate
166
- * Schema.decodeUnknownSync(OrderState)(rawJson)
167
- * ```
168
- */
169
- declare const State: <const D extends Record<string, Schema.Struct.Fields>>(definition: D) => MachineStateSchema<D>;
170
- declare const Event: (<const D extends Record<string, Schema.Struct.Fields>>(definition: D) => MachineEventSchema<D>) & {
171
- reply: <F extends Schema.Struct.Fields, RS extends Schema.Schema.Any>(fields: F, replySchema: RS) => ReplyFields<F, RS>;
172
- };
173
- //#endregion
174
- export { Event, MachineEventSchema, MachineStateSchema, ReplyFields, ReplySchemaSymbol, State, VariantsUnion };