effect-machine 0.12.0 → 0.14.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 (53) hide show
  1. package/README.md +133 -324
  2. package/dist/actor.d.ts +46 -28
  3. package/dist/actor.js +276 -315
  4. package/dist/cluster/entity-machine.d.ts +1 -1
  5. package/dist/cluster/entity-machine.js +20 -9
  6. package/dist/cluster/to-entity.d.ts +3 -3
  7. package/dist/errors.d.ts +24 -20
  8. package/dist/errors.js +10 -6
  9. package/dist/index.d.ts +5 -4
  10. package/dist/index.js +3 -2
  11. package/dist/internal/runtime.d.ts +82 -7
  12. package/dist/internal/runtime.js +162 -58
  13. package/dist/internal/transition.d.ts +12 -11
  14. package/dist/internal/transition.js +12 -14
  15. package/dist/machine.d.ts +148 -140
  16. package/dist/machine.js +141 -155
  17. package/dist/schema.d.ts +14 -0
  18. package/dist/schema.js +10 -1
  19. package/dist/slot.d.ts +112 -86
  20. package/dist/slot.js +92 -59
  21. package/dist/supervision.d.ts +97 -0
  22. package/dist/supervision.js +42 -0
  23. package/dist/testing.d.ts +21 -12
  24. package/dist/testing.js +23 -26
  25. package/package.json +7 -7
  26. package/v3/dist/actor.d.ts +53 -30
  27. package/v3/dist/actor.js +286 -311
  28. package/v3/dist/cluster/entity-machine.d.ts +1 -1
  29. package/v3/dist/cluster/entity-machine.js +5 -5
  30. package/v3/dist/cluster/to-entity.d.ts +1 -1
  31. package/v3/dist/errors.d.ts +14 -10
  32. package/v3/dist/errors.js +11 -7
  33. package/v3/dist/index.d.ts +6 -5
  34. package/v3/dist/index.js +3 -2
  35. package/v3/dist/inspection.d.ts +3 -22
  36. package/v3/dist/inspection.js +1 -15
  37. package/v3/dist/internal/brands.d.ts +4 -8
  38. package/v3/dist/internal/inspection.js +1 -1
  39. package/v3/dist/internal/runtime.d.ts +87 -10
  40. package/v3/dist/internal/runtime.js +177 -61
  41. package/v3/dist/internal/transition.d.ts +13 -12
  42. package/v3/dist/internal/transition.js +14 -16
  43. package/v3/dist/internal/utils.js +5 -1
  44. package/v3/dist/machine.d.ts +158 -143
  45. package/v3/dist/machine.js +148 -155
  46. package/v3/dist/schema.d.ts +25 -11
  47. package/v3/dist/schema.js +18 -5
  48. package/v3/dist/slot.d.ts +112 -86
  49. package/v3/dist/slot.js +92 -59
  50. package/v3/dist/supervision.d.ts +97 -0
  51. package/v3/dist/supervision.js +42 -0
  52. package/v3/dist/testing.d.ts +21 -12
  53. package/v3/dist/testing.js +23 -24
@@ -1,23 +1,24 @@
1
1
  import { __exportAll } from "./_virtual/_rolldown/runtime.js";
2
- import { Inspector } from "./inspection.js";
3
- import { getTag, makeReply, stubSystem } from "./internal/utils.js";
4
- import { ProvisionValidationError, SlotProvisionError } from "./errors.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
5
  import { emitWithTimestamp } from "./internal/inspection.js";
6
+ import { Inspector } from "./inspection.js";
6
7
  import { MachineContextTag } from "./slot.js";
7
- import { findTransitions, invalidateIndex, resolveTransition, runTransitionHandler, shouldPostpone } from "./internal/transition.js";
8
8
  import { createActor } from "./actor.js";
9
- import { Cause, Effect, Exit, Option, Scope } from "effect";
9
+ import { Cause, Effect, Exit, Option, Random, Schema, Scope } from "effect";
10
10
  //#region src/machine.ts
11
11
  var machine_exports = /* @__PURE__ */ __exportAll({
12
- BuiltMachine: () => BuiltMachine,
13
12
  Machine: () => Machine,
13
+ deferReply: () => deferReply,
14
14
  findTransitions: () => findTransitions,
15
15
  make: () => make,
16
+ materializeMachine: () => materializeMachine,
16
17
  replay: () => replay,
17
18
  reply: () => reply,
18
19
  spawn: () => spawn
19
20
  });
20
- const emitTaskInspection = (input) => Effect.flatMap(Effect.serviceOptional(Inspector).pipe(Effect.option), (inspector) => Option.isNone(inspector) ? Effect.void : emitWithTimestamp(inspector.value, (timestamp) => ({
21
+ const emitTaskInspection = (input) => Effect.flatMap(Effect.serviceOption(Inspector), (inspector) => Option.isNone(inspector) ? Effect.void : emitWithTimestamp(inspector.value, (timestamp) => ({
21
22
  type: "@machine.task",
22
23
  actorId: input.actorId,
23
24
  state: input.state,
@@ -27,22 +28,40 @@ const emitTaskInspection = (input) => Effect.flatMap(Effect.serviceOptional(Insp
27
28
  timestamp
28
29
  })));
29
30
  /**
30
- * A finalized machine ready for spawning.
31
+ * Bind slot handlers to a machine, returning a fresh copy with handlers installed.
32
+ * If no handlers provided and machine has no slots, returns the machine as-is.
33
+ * Validates that all required slots are provided and no extra slots are given.
31
34
  *
32
- * Created by calling `.build()` on a `Machine`. This is the only type
33
- * accepted by `Machine.spawn` and `ActorSystem.spawn` (regular overload).
34
- * Testing utilities (`simulate`, `createTestHarness`, etc.) still accept `Machine`.
35
+ * @internal used by spawn, replay, simulate, test harness, entity-machine
35
36
  */
36
- var BuiltMachine = class {
37
- /** @internal */
38
- _inner;
39
- /** @internal */
40
- constructor(machine) {
41
- this._inner = machine;
42
- }
43
- get initial() {
44
- return this._inner.initial;
37
+ const materializeMachine = (machine, handlers) => {
38
+ if (handlers === void 0) {
39
+ if (machine._slotsSchema !== void 0 && Object.keys(machine._slotsSchema.definitions).length > 0) throw new ProvisionValidationError({
40
+ missing: Object.keys(machine._slotsSchema.definitions),
41
+ extra: []
42
+ });
43
+ return machine;
45
44
  }
45
+ const requiredSlots = /* @__PURE__ */ new Set();
46
+ if (machine._slotsSchema !== void 0) for (const name of Object.keys(machine._slotsSchema.definitions)) requiredSlots.add(name);
47
+ const providedSlots = new Set(Object.keys(handlers));
48
+ const missing = [];
49
+ const extra = [];
50
+ for (const name of requiredSlots) if (!providedSlots.has(name)) missing.push(name);
51
+ for (const name of providedSlots) if (!requiredSlots.has(name)) extra.push(name);
52
+ if (missing.length > 0 || extra.length > 0) throw new ProvisionValidationError({
53
+ missing,
54
+ extra
55
+ });
56
+ const result = new Machine(machine.initial, machine.stateSchema, machine.eventSchema, machine._slotsSchema, machine._slotValidation);
57
+ result._transitions = [...machine._transitions];
58
+ result._finalStates = new Set(machine._finalStates);
59
+ result._spawnEffects = [...machine._spawnEffects];
60
+ result._backgroundEffects = [...machine._backgroundEffects];
61
+ result._postponeRules = [...machine._postponeRules];
62
+ result._replySchemas = machine._replySchemas;
63
+ if (machine._slotsSchema !== void 0) for (const name of Object.keys(machine._slotsSchema.definitions)) result._slotHandlers.set(name, handlers[name]);
64
+ return result;
46
65
  };
47
66
  /**
48
67
  * Machine definition with fluent builder API.
@@ -53,8 +72,7 @@ var BuiltMachine = class {
53
72
  * - `R`: Effect requirements
54
73
  * - `_SD`: State schema definition (for compile-time validation)
55
74
  * - `_ED`: Event schema definition (for compile-time validation)
56
- * - `GD`: Guard definitions
57
- * - `EFD`: Effect definitions
75
+ * - `SD`: Slot definitions
58
76
  */
59
77
  var Machine = class Machine {
60
78
  initial;
@@ -63,11 +81,10 @@ var Machine = class Machine {
63
81
  /** @internal */ _backgroundEffects;
64
82
  /** @internal */ _finalStates;
65
83
  /** @internal */ _postponeRules;
66
- /** @internal */ _guardsSchema;
67
- /** @internal */ _effectsSchema;
68
- /** @internal */ _guardHandlers;
69
- /** @internal */ _effectHandlers;
84
+ /** @internal */ _slotsSchema;
85
+ /** @internal */ _slotHandlers;
70
86
  /** @internal */ _slots;
87
+ /** @internal */ _slotValidation;
71
88
  stateSchema;
72
89
  eventSchema;
73
90
  /** @internal */ _replySchemas;
@@ -91,49 +108,73 @@ var Machine = class Machine {
91
108
  get postponeRules() {
92
109
  return this._postponeRules;
93
110
  }
94
- get guardsSchema() {
95
- return this._guardsSchema;
96
- }
97
- get effectsSchema() {
98
- return this._effectsSchema;
111
+ get slotsSchema() {
112
+ return this._slotsSchema;
99
113
  }
100
114
  get replySchemas() {
101
115
  return this._replySchemas;
102
116
  }
103
117
  /** @internal */
104
- constructor(initial, stateSchema, eventSchema, guardsSchema, effectsSchema) {
118
+ constructor(initial, stateSchema, eventSchema, slotsSchema, slotValidation = true) {
105
119
  this.initial = initial;
106
120
  this._transitions = [];
107
121
  this._spawnEffects = [];
108
122
  this._backgroundEffects = [];
109
123
  this._finalStates = /* @__PURE__ */ new Set();
110
124
  this._postponeRules = [];
111
- this._guardsSchema = guardsSchema;
112
- this._effectsSchema = effectsSchema;
113
- this._guardHandlers = /* @__PURE__ */ new Map();
114
- this._effectHandlers = /* @__PURE__ */ new Map();
125
+ this._slotsSchema = slotsSchema;
126
+ this._replySchemas = eventSchema?._replySchemas ?? /* @__PURE__ */ new Map();
127
+ this._slotHandlers = /* @__PURE__ */ new Map();
128
+ this._slotValidation = slotValidation;
115
129
  this.stateSchema = stateSchema;
116
130
  this.eventSchema = eventSchema;
117
- this._replySchemas = eventSchema?._replySchemas ?? /* @__PURE__ */ new Map();
118
- this._slots = {
119
- guards: this._guardsSchema !== void 0 ? this._guardsSchema._createSlots((name, params) => Effect.flatMap(Effect.serviceOptional(this.Context).pipe(Effect.orDie), (ctx) => {
120
- const handler = this._guardHandlers.get(name);
121
- if (handler === void 0) return Effect.die(new SlotProvisionError({
122
- slotName: name,
123
- slotType: "guard"
124
- }));
125
- const result = handler(params, ctx);
126
- return typeof result === "boolean" ? Effect.succeed(result) : result;
127
- })) : {},
128
- effects: this._effectsSchema !== void 0 ? this._effectsSchema._createSlots((name, params) => Effect.flatMap(Effect.serviceOptional(this.Context).pipe(Effect.orDie), (ctx) => {
129
- const handler = this._effectHandlers.get(name);
130
- if (handler === void 0) return Effect.die(new SlotProvisionError({
131
- slotName: name,
132
- slotType: "effect"
133
- }));
134
- return handler(params, ctx);
135
- })) : {}
136
- };
131
+ const validators = slotValidation && slotsSchema !== void 0 ? new Map(Object.entries(slotsSchema.definitions).map(([name, def]) => [name, {
132
+ decodeInput: Schema.decodeUnknownSync(def.inputSchema),
133
+ decodeOutput: Schema.decodeUnknownSync(def.outputSchema)
134
+ }])) : void 0;
135
+ const resolve = (name, params) => Effect.flatMap(Effect.serviceOption(this.Context), (maybeCtx) => {
136
+ if (Option.isNone(maybeCtx)) return Effect.die("MachineContext not available");
137
+ const handler = this._slotHandlers.get(name);
138
+ if (handler === void 0) return Effect.die(new SlotProvisionError({
139
+ slotName: name,
140
+ slotType: "slot"
141
+ }));
142
+ const validatedParams = validators !== void 0 ? (() => {
143
+ try {
144
+ const v = validators.get(name);
145
+ return v !== void 0 ? v.decodeInput(params) : params;
146
+ } catch (e) {
147
+ return Effect.die(new SlotCodecError({
148
+ slotName: name,
149
+ phase: "input",
150
+ message: e instanceof Error ? e.message : String(e)
151
+ }));
152
+ }
153
+ })() : params;
154
+ if (Effect.isEffect(validatedParams)) return validatedParams;
155
+ const result = handler(validatedParams);
156
+ let resultEffect;
157
+ if (result === void 0 || result === null) resultEffect = Effect.void;
158
+ else if (Effect.isEffect(result)) resultEffect = result;
159
+ else resultEffect = Effect.succeed(result);
160
+ if (validators !== void 0) {
161
+ const v = validators.get(name);
162
+ if (v !== void 0) return Effect.flatMap(resultEffect, (value) => {
163
+ try {
164
+ const decoded = v.decodeOutput(value);
165
+ return Effect.succeed(decoded);
166
+ } catch (e) {
167
+ return Effect.die(new SlotCodecError({
168
+ slotName: name,
169
+ phase: "output",
170
+ message: e instanceof Error ? e.message : String(e)
171
+ }));
172
+ }
173
+ });
174
+ }
175
+ return resultEffect;
176
+ });
177
+ this._slots = this._slotsSchema !== void 0 ? this._slotsSchema._createSlots(resolve) : {};
137
178
  }
138
179
  from(stateOrStates, build) {
139
180
  build(new TransitionScope(this, Array.isArray(stateOrStates) ? stateOrStates : [stateOrStates]));
@@ -181,42 +222,19 @@ var Machine = class Machine {
181
222
  invalidateIndex(this);
182
223
  return this;
183
224
  }
184
- /**
185
- * State-scoped effect that is forked on state entry and automatically cancelled on state exit.
186
- * Use effect slots defined via `Slot.Effects` for the actual work.
187
- *
188
- * @example
189
- * ```ts
190
- * const MyEffects = Slot.Effects({
191
- * fetchData: { url: Schema.String },
192
- * });
193
- *
194
- * machine
195
- * .spawn(State.Loading, ({ effects, state }) => effects.fetchData({ url: state.url }))
196
- * .build({
197
- * fetchData: ({ url }, { self }) =>
198
- * Effect.gen(function* () {
199
- * yield* Effect.addFinalizer(() => Effect.log("Leaving Loading"));
200
- * const data = yield* Http.get(url);
201
- * yield* self.send(Event.Loaded({ data }));
202
- * }),
203
- * });
204
- * ```
205
- */
206
- spawn(state, handler) {
207
- const stateTag = getTag(state);
208
- this._spawnEffects.push({
209
- stateTag,
210
- handler
211
- });
225
+ spawn(stateOrStates, handler) {
226
+ const states = Array.isArray(stateOrStates) ? stateOrStates : [stateOrStates];
227
+ for (const s of states) {
228
+ const stateTag = getTag(s);
229
+ this._spawnEffects.push({
230
+ stateTag,
231
+ handler
232
+ });
233
+ }
212
234
  invalidateIndex(this);
213
235
  return this;
214
236
  }
215
- /**
216
- * State-scoped task that runs on entry and sends success/failure events.
217
- * Interrupts do not emit failure events.
218
- */
219
- task(state, run, options) {
237
+ task(stateOrStates, run, options) {
220
238
  const handler = Effect.fn("effect-machine.task")(function* (ctx) {
221
239
  yield* emitTaskInspection({
222
240
  actorId: ctx.actorId,
@@ -232,7 +250,8 @@ var Machine = class Machine {
232
250
  taskName: options.name,
233
251
  phase: "success"
234
252
  });
235
- yield* ctx.self.send(options.onSuccess(exit.value, ctx));
253
+ const successEvent = options.onSuccess !== void 0 ? options.onSuccess(exit.value, ctx) : exit.value;
254
+ yield* ctx.self.send(successEvent);
236
255
  yield* Effect.yieldNow();
237
256
  return;
238
257
  }
@@ -260,7 +279,7 @@ var Machine = class Machine {
260
279
  }
261
280
  return yield* Effect.failCause(cause).pipe(Effect.orDie);
262
281
  });
263
- return this.spawn(state, handler);
282
+ return this.spawn(stateOrStates, handler);
264
283
  }
265
284
  /**
266
285
  * State timeout — gen_statem's `state_timeout`.
@@ -294,22 +313,14 @@ var Machine = class Machine {
294
313
  }
295
314
  /**
296
315
  * Machine-lifetime effect that is forked on actor spawn and runs until the actor stops.
297
- * Use effect slots defined via `Slot.Effects` for the actual work.
298
316
  *
299
317
  * @example
300
318
  * ```ts
301
- * const MyEffects = Slot.Effects({
302
- * heartbeat: {},
303
- * });
304
- *
305
- * machine
306
- * .background(({ effects }) => effects.heartbeat())
307
- * .build({
308
- * heartbeat: (_, { self }) =>
309
- * Effect.forever(
310
- * Effect.sleep("30 seconds").pipe(Effect.andThen(self.send(Event.Ping)))
311
- * ),
312
- * });
319
+ * machine.background(({ self }) =>
320
+ * Effect.forever(
321
+ * Effect.sleep("30 seconds").pipe(Effect.andThen(self.send(Event.Ping))),
322
+ * ),
323
+ * );
313
324
  * ```
314
325
  */
315
326
  background(handler) {
@@ -350,43 +361,8 @@ var Machine = class Machine {
350
361
  this._finalStates.add(stateTag);
351
362
  return this;
352
363
  }
353
- /**
354
- * Finalize the machine. Returns a `BuiltMachine` — the only type accepted by `Machine.spawn`.
355
- *
356
- * - Machines with slots: pass implementations as the first argument.
357
- * - Machines without slots: call with no arguments.
358
- */
359
- build(...args) {
360
- const handlers = args[0];
361
- if (handlers !== void 0) {
362
- const requiredSlots = /* @__PURE__ */ new Set();
363
- if (this._guardsSchema !== void 0) for (const name of Object.keys(this._guardsSchema.definitions)) requiredSlots.add(name);
364
- if (this._effectsSchema !== void 0) for (const name of Object.keys(this._effectsSchema.definitions)) requiredSlots.add(name);
365
- const providedSlots = new Set(Object.keys(handlers));
366
- const missing = [];
367
- const extra = [];
368
- for (const name of requiredSlots) if (!providedSlots.has(name)) missing.push(name);
369
- for (const name of providedSlots) if (!requiredSlots.has(name)) extra.push(name);
370
- if (missing.length > 0 || extra.length > 0) throw new ProvisionValidationError({
371
- missing,
372
- extra
373
- });
374
- const result = new Machine(this.initial, this.stateSchema, this.eventSchema, this._guardsSchema, this._effectsSchema);
375
- result._transitions = [...this._transitions];
376
- result._finalStates = new Set(this._finalStates);
377
- result._spawnEffects = [...this._spawnEffects];
378
- result._backgroundEffects = [...this._backgroundEffects];
379
- result._postponeRules = [...this._postponeRules];
380
- result._replySchemas = this._replySchemas;
381
- const anyHandlers = handlers;
382
- if (this._guardsSchema !== void 0) for (const name of Object.keys(this._guardsSchema.definitions)) result._guardHandlers.set(name, anyHandlers[name]);
383
- if (this._effectsSchema !== void 0) for (const name of Object.keys(this._effectsSchema.definitions)) result._effectHandlers.set(name, anyHandlers[name]);
384
- return new BuiltMachine(result);
385
- }
386
- return new BuiltMachine(this);
387
- }
388
364
  static make(config) {
389
- return new Machine(config.initial, config.state, config.event, config.guards, config.effects);
365
+ return new Machine(config.initial, config.state, config.event, config.slots, config.slotValidation ?? true);
390
366
  }
391
367
  };
392
368
  var TransitionScope = class {
@@ -405,27 +381,42 @@ var TransitionScope = class {
405
381
  };
406
382
  const make = Machine.make;
407
383
  /**
408
- * Spawn an actor from a built machine.
384
+ * Spawn an actor from a machine.
385
+ *
386
+ * For machines with slots, pass implementations via `{ slots: { ... } }`.
387
+ *
388
+ * @example
389
+ * ```ts
390
+ * // No slots
391
+ * const actor = yield* Machine.spawn(machine);
409
392
  *
410
- * Options:
411
- * - `id` custom actor ID (default: random)
412
- * - `hydrate` restore from a previously-saved state snapshot.
413
- * The actor starts in the hydrated state and re-runs spawn effects
414
- * for that state (timers, scoped resources, etc.). Transition history
415
- * is not replayed — only the current state's entry effects run.
393
+ * // With slots
394
+ * const actor = yield* Machine.spawn(machine, {
395
+ * slots: { canRetry: ({ max }) => attempts < max },
396
+ * });
416
397
  *
417
- * Persistence is composed in userland by observing `actor.changes`
418
- * and saving snapshots to your own storage.
398
+ * // With persistence
399
+ * const actor = yield* Machine.spawn(machine, {
400
+ * persist: {
401
+ * load: () => storage.get("actor-state"),
402
+ * save: (state) => storage.set("actor-state", state),
403
+ * },
404
+ * });
405
+ * ```
419
406
  */
420
- const spawn = Effect.fn("effect-machine.spawn")(function* (built, idOrOptions) {
407
+ const spawn = Effect.fn("effect-machine.spawn")(function* (machine, idOrOptions) {
421
408
  const opts = typeof idOrOptions === "string" ? { id: idOrOptions } : idOrOptions;
422
- const actor = yield* createActor(opts?.id ?? `actor-${Math.random().toString(36).slice(2)}`, built._inner, { initialState: opts?.hydrate });
409
+ const actor = yield* createActor(opts?.id ?? `actor-${(yield* Random.next).toString(36).slice(2)}`, materializeMachine(machine, opts?.slots), {
410
+ initialState: opts?.hydrate,
411
+ supervision: opts?.supervision,
412
+ persist: opts?.persist
413
+ });
423
414
  const maybeScope = yield* Effect.serviceOption(Scope.Scope);
424
415
  if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, actor.stop);
425
416
  return actor;
426
417
  });
427
- const replay = Effect.fn("effect-machine.replay")(function* (built, events, options) {
428
- const machine = built._inner;
418
+ const replay = Effect.fn("effect-machine.replay")(function* (input, events, options) {
419
+ const machine = materializeMachine(input, options?.slots);
429
420
  let state = options?.from ?? machine.initial;
430
421
  const hasPostponeRules = machine.postponeRules.length > 0;
431
422
  const postponed = [];
@@ -433,7 +424,8 @@ const replay = Effect.fn("effect-machine.replay")(function* (built, events, opti
433
424
  const self = {
434
425
  send: dummySend,
435
426
  cast: dummySend,
436
- spawn: () => Effect.die("spawn not supported in replay")
427
+ spawn: () => Effect.die("spawn not supported in replay"),
428
+ reply: () => Effect.succeed(false)
437
429
  };
438
430
  for (const event of events) {
439
431
  if (machine.finalStates.has(state._tag)) break;
@@ -468,5 +460,6 @@ const replay = Effect.fn("effect-machine.replay")(function* (built, events, opti
468
460
  return state;
469
461
  });
470
462
  const reply = makeReply;
463
+ const deferReply = makeDeferReply;
471
464
  //#endregion
472
- export { BuiltMachine, Machine, findTransitions, machine_exports, make, replay, reply, spawn };
465
+ export { Machine, deferReply, findTransitions, machine_exports, make, materializeMachine, replay, reply, spawn };
@@ -25,7 +25,7 @@ type VariantSchemas<D extends Record<string, Schema.Struct.Fields>> = { readonly
25
25
  * Reply-bearing variants carry ReplyTypeBrand<R> for ask() inference.
26
26
  */
27
27
  type VariantsUnion<D extends Record<string, Schema.Struct.Fields>> = { [K in keyof D & string]: TaggedStructType<K, D[K]> & (D[K] extends {
28
- readonly [ReplySchemaSymbol]: Schema.Schema<infer R, infer _I, infer _RR>;
28
+ readonly [ReplySchemaSymbol]: Schema.Schema<infer R>;
29
29
  } ? ReplyTypeBrand<R> : unknown) }[keyof D & string];
30
30
  /**
31
31
  * Check if fields are empty (no required string properties).
@@ -37,7 +37,7 @@ type IsEmptyFields<Fields extends Schema.Struct.Fields> = string & keyof Fields
37
37
  * If fields carry ReplySchemaSymbol, adds ReplyTypeBrand<R>.
38
38
  */
39
39
  type VariantReplyBrand<Fields extends Schema.Struct.Fields> = Fields extends {
40
- readonly [ReplySchemaSymbol]: Schema.Schema<infer R, infer _I, infer _RR>;
40
+ readonly [ReplySchemaSymbol]: Schema.Schema<infer R>;
41
41
  } ? ReplyTypeBrand<R> : unknown;
42
42
  /**
43
43
  * Constructor functions for each variant.
@@ -50,8 +50,8 @@ type VariantReplyBrand<Fields extends Schema.Struct.Fields> = Fields extends {
50
50
  */
51
51
  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]> & {
52
52
  readonly derive: (source: object) => TaggedStructType<K, D[K]> & Brand;
53
- } : ((args: Schema.Struct.Constructor<D[K]>) => TaggedStructType<K, D[K]> & Brand & VariantReplyBrand<D[K]>) & {
54
- readonly derive: (source: object, partial?: Partial<Schema.Struct.Constructor<D[K]>>) => TaggedStructType<K, D[K]> & Brand;
53
+ } : ((args: Schema.Struct.Type<D[K]>) => TaggedStructType<K, D[K]> & Brand & VariantReplyBrand<D[K]>) & {
54
+ readonly derive: (source: object, partial?: Partial<Schema.Struct.Type<D[K]>>) => TaggedStructType<K, D[K]> & Brand;
55
55
  readonly _tag: K;
56
56
  } };
57
57
  /**
@@ -70,11 +70,6 @@ interface MachineSchemaBase<D extends Record<string, Schema.Struct.Fields>, Bran
70
70
  * Per-variant schemas for fine-grained operations
71
71
  */
72
72
  readonly variants: VariantSchemas<D>;
73
- /**
74
- * Reply schemas per variant tag. Only populated for event schemas
75
- * with variants defined via `Event.reply()`.
76
- */
77
- readonly _replySchemas: ReadonlyMap<string, Schema.Schema.Any>;
78
73
  /**
79
74
  * Type guard: `OrderState.$is("Pending")(value)`
80
75
  */
@@ -86,6 +81,25 @@ interface MachineSchemaBase<D extends Record<string, Schema.Struct.Fields>, Bran
86
81
  <R>(cases: MatchCases<D, R>): (value: VariantsUnion<D> & Brand) => R;
87
82
  <R>(value: VariantsUnion<D> & Brand, cases: MatchCases<D, R>): R;
88
83
  };
84
+ /**
85
+ * Union-level derive: copies fields from `source` into the same variant,
86
+ * overriding with `partial`. Preserves the specific variant subtype.
87
+ *
88
+ * Dispatches to the per-variant `derive` based on `source._tag`.
89
+ *
90
+ * @example
91
+ * ```ts
92
+ * // Instead of switching on _tag to call per-variant derive:
93
+ * const updated = AgentLoopState.derive(state, { queue: newQueue })
94
+ * // If state is StreamingState, returns StreamingState (not LoopState)
95
+ * ```
96
+ */
97
+ readonly derive: <S extends VariantsUnion<D> & Brand>(source: S, partial?: Partial<Omit<S, "_tag">>) => S;
98
+ /**
99
+ * Reply schemas per variant tag. Only populated for event schemas
100
+ * with variants defined via `Event.reply()`.
101
+ */
102
+ readonly _replySchemas: ReadonlyMap<string, Schema.Schema.Any>;
89
103
  }
90
104
  /**
91
105
  * Schema-first state definition that provides:
@@ -97,14 +111,14 @@ interface MachineSchemaBase<D extends Record<string, Schema.Struct.Fields>, Bran
97
111
  * The D type parameter captures the definition, creating a unique brand
98
112
  * per distinct schema definition shape.
99
113
  */
100
- type MachineStateSchema<D extends Record<string, Schema.Struct.Fields>> = Schema.Schema<VariantsUnion<D> & FullStateBrand<D>, VariantsUnion<D>, never> & MachineSchemaBase<D, FullStateBrand<D>> & VariantConstructors<D, FullStateBrand<D>>;
114
+ type MachineStateSchema<D extends Record<string, Schema.Struct.Fields>> = Schema.Schema<VariantsUnion<D> & FullStateBrand<D>, unknown, never> & MachineSchemaBase<D, FullStateBrand<D>> & VariantConstructors<D, FullStateBrand<D>>;
101
115
  /**
102
116
  * Schema-first event definition (same structure as state, different brand)
103
117
  *
104
118
  * The D type parameter captures the definition, creating a unique brand
105
119
  * per distinct schema definition shape.
106
120
  */
107
- type MachineEventSchema<D extends Record<string, Schema.Struct.Fields>> = Schema.Schema<VariantsUnion<D> & FullEventBrand<D>, VariantsUnion<D>, never> & MachineSchemaBase<D, FullEventBrand<D>> & VariantConstructors<D, FullEventBrand<D>>;
121
+ type MachineEventSchema<D extends Record<string, Schema.Struct.Fields>> = Schema.Schema<VariantsUnion<D> & FullEventBrand<D>, unknown, never> & MachineSchemaBase<D, FullEventBrand<D>> & VariantConstructors<D, FullEventBrand<D>>;
108
122
  /**
109
123
  * Create a schema-first State definition.
110
124
  *
package/v3/dist/schema.js CHANGED
@@ -42,6 +42,7 @@ const ReplySchemaSymbol = Symbol.for("effect-machine/ReplySchema");
42
42
  /**
43
43
  * Build a schema-first definition from a record of tag -> fields
44
44
  */
45
+ const RESERVED_DERIVE_KEYS = new Set(["_tag"]);
45
46
  const buildMachineSchema = (definition) => {
46
47
  const variants = {};
47
48
  const constructors = {};
@@ -65,7 +66,8 @@ const buildMachineSchema = (definition) => {
65
66
  const result = { _tag: tag };
66
67
  for (const key of fieldNames) if (key in source) result[key] = source[key];
67
68
  if (partial !== void 0) for (const [key, value] of Object.entries(partial)) {
68
- if (key === "_tag") continue;
69
+ if (RESERVED_DERIVE_KEYS.has(key)) continue;
70
+ if (!fieldNames.has(key)) continue;
69
71
  result[key] = value;
70
72
  }
71
73
  return result;
@@ -77,7 +79,7 @@ const buildMachineSchema = (definition) => {
77
79
  };
78
80
  }
79
81
  const variantArray = Object.values(variants);
80
- if (variantArray.length === 0) throw new InvalidSchemaError();
82
+ if (variantArray.length === 0) throw new InvalidSchemaError({ message: "Schema must have at least one variant" });
81
83
  const unionSchema = variantArray.length === 1 ? variantArray[0] : Schema.Union(...variantArray);
82
84
  const $is = (tag) => (u) => typeof u === "object" && u !== null && "_tag" in u && u._tag === tag;
83
85
  const $match = (valueOrCases, maybeCases) => {
@@ -99,7 +101,7 @@ const buildMachineSchema = (definition) => {
99
101
  variants,
100
102
  constructors,
101
103
  _definition: definition,
102
- _replySchemas: replySchemas,
104
+ replySchemas,
103
105
  $is,
104
106
  $match
105
107
  };
@@ -109,13 +111,21 @@ const buildMachineSchema = (definition) => {
109
111
  * Builds the schema object with variants, constructors, $is, and $match.
110
112
  */
111
113
  const createMachineSchema = (definition) => {
112
- const { schema, variants, constructors, _definition, _replySchemas, $is, $match } = buildMachineSchema(definition);
114
+ const { schema, variants, constructors, _definition, replySchemas, $is, $match } = buildMachineSchema(definition);
115
+ const derive = (source, partial) => {
116
+ const ctor = constructors[source._tag];
117
+ if (ctor === void 0) throw new MissingMatchHandlerError({ tag: source._tag });
118
+ const deriveFn = ctor.derive;
119
+ if (deriveFn === void 0) throw new MissingMatchHandlerError({ tag: source._tag });
120
+ return deriveFn(source, partial);
121
+ };
113
122
  return Object.assign(Object.create(schema), {
114
123
  variants,
115
124
  _definition,
116
- _replySchemas,
125
+ _replySchemas: replySchemas,
117
126
  $is,
118
127
  $match,
128
+ derive,
119
129
  ...constructors
120
130
  });
121
131
  };
@@ -171,6 +181,9 @@ const State = (definition) => createMachineSchema(definition);
171
181
  *
172
182
  * // Construct
173
183
  * const e = OrderEvent.Ship({ trackingId: "abc" })
184
+ *
185
+ * // Typed ask
186
+ * const total = yield* actor.ask(OrderEvent.GetTotal) // number
174
187
  * ```
175
188
  */
176
189
  const EventImpl = (definition) => createMachineSchema(definition);