effect-machine 0.13.0 → 0.15.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 (49) hide show
  1. package/README.md +19 -11
  2. package/dist/actor.d.ts +18 -6
  3. package/dist/actor.js +151 -61
  4. package/dist/cluster/entity-machine.d.ts +1 -1
  5. package/dist/cluster/entity-machine.js +2 -1
  6. package/dist/cluster/to-entity.d.ts +1 -1
  7. package/dist/errors.d.ts +9 -2
  8. package/dist/errors.js +8 -2
  9. package/dist/index.d.ts +4 -4
  10. package/dist/index.js +4 -4
  11. package/dist/internal/runtime.d.ts +21 -2
  12. package/dist/internal/runtime.js +64 -56
  13. package/dist/internal/transition.d.ts +9 -9
  14. package/dist/internal/transition.js +3 -5
  15. package/dist/machine.d.ts +129 -135
  16. package/dist/machine.js +97 -112
  17. package/dist/schema.d.ts +17 -1
  18. package/dist/schema.js +10 -0
  19. package/dist/slot.d.ts +112 -86
  20. package/dist/slot.js +92 -59
  21. package/dist/testing.d.ts +16 -16
  22. package/dist/testing.js +3 -3
  23. package/package.json +3 -3
  24. package/v3/dist/actor.d.ts +25 -12
  25. package/v3/dist/actor.js +174 -89
  26. package/v3/dist/cluster/entity-machine.d.ts +1 -1
  27. package/v3/dist/cluster/entity-machine.js +1 -0
  28. package/v3/dist/cluster/to-entity.d.ts +1 -1
  29. package/v3/dist/errors.d.ts +12 -3
  30. package/v3/dist/errors.js +10 -4
  31. package/v3/dist/index.d.ts +6 -6
  32. package/v3/dist/index.js +2 -2
  33. package/v3/dist/inspection.d.ts +3 -22
  34. package/v3/dist/inspection.js +1 -15
  35. package/v3/dist/internal/brands.d.ts +4 -8
  36. package/v3/dist/internal/inspection.js +1 -1
  37. package/v3/dist/internal/runtime.d.ts +27 -8
  38. package/v3/dist/internal/runtime.js +91 -61
  39. package/v3/dist/internal/transition.d.ts +10 -10
  40. package/v3/dist/internal/transition.js +8 -10
  41. package/v3/dist/internal/utils.js +5 -1
  42. package/v3/dist/machine.d.ts +160 -120
  43. package/v3/dist/machine.js +118 -115
  44. package/v3/dist/schema.d.ts +25 -11
  45. package/v3/dist/schema.js +18 -5
  46. package/v3/dist/slot.d.ts +112 -86
  47. package/v3/dist/slot.js +92 -59
  48. package/v3/dist/testing.d.ts +16 -16
  49. package/v3/dist/testing.js +7 -7
@@ -1,15 +1,16 @@
1
1
  import { __exportAll } from "./_virtual/_rolldown/runtime.js";
2
- import { getTag, makeReply, stubSystem } from "./internal/utils.js";
3
- 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
4
  import { findTransitions, invalidateIndex, resolveTransition, runTransitionHandler, shouldPostpone } from "./internal/transition.js";
5
5
  import { emitWithTimestamp } from "./internal/inspection.js";
6
6
  import { Inspector } from "./inspection.js";
7
7
  import { MachineContextTag } from "./slot.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
12
  Machine: () => Machine,
13
+ deferReply: () => deferReply,
13
14
  findTransitions: () => findTransitions,
14
15
  make: () => make,
15
16
  materializeMachine: () => materializeMachine,
@@ -17,7 +18,7 @@ var machine_exports = /* @__PURE__ */ __exportAll({
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,
@@ -35,22 +36,14 @@ const emitTaskInspection = (input) => Effect.flatMap(Effect.serviceOptional(Insp
35
36
  */
36
37
  const materializeMachine = (machine, handlers) => {
37
38
  if (handlers === void 0) {
38
- const hasGuards = machine._guardsSchema !== void 0 && Object.keys(machine._guardsSchema.definitions).length > 0;
39
- const hasEffects = machine._effectsSchema !== void 0 && Object.keys(machine._effectsSchema.definitions).length > 0;
40
- if (hasGuards || hasEffects) {
41
- const missing = [];
42
- if (machine._guardsSchema !== void 0) missing.push(...Object.keys(machine._guardsSchema.definitions));
43
- if (machine._effectsSchema !== void 0) missing.push(...Object.keys(machine._effectsSchema.definitions));
44
- throw new ProvisionValidationError({
45
- missing,
46
- extra: []
47
- });
48
- }
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
+ });
49
43
  return machine;
50
44
  }
51
45
  const requiredSlots = /* @__PURE__ */ new Set();
52
- if (machine._guardsSchema !== void 0) for (const name of Object.keys(machine._guardsSchema.definitions)) requiredSlots.add(name);
53
- if (machine._effectsSchema !== void 0) for (const name of Object.keys(machine._effectsSchema.definitions)) requiredSlots.add(name);
46
+ if (machine._slotsSchema !== void 0) for (const name of Object.keys(machine._slotsSchema.definitions)) requiredSlots.add(name);
54
47
  const providedSlots = new Set(Object.keys(handlers));
55
48
  const missing = [];
56
49
  const extra = [];
@@ -60,15 +53,14 @@ const materializeMachine = (machine, handlers) => {
60
53
  missing,
61
54
  extra
62
55
  });
63
- const result = new Machine(machine.initial, machine.stateSchema, machine.eventSchema, machine._guardsSchema, machine._effectsSchema);
56
+ const result = new Machine(machine.initial, machine.stateSchema, machine.eventSchema, machine._slotsSchema, machine._slotValidation);
64
57
  result._transitions = [...machine._transitions];
65
58
  result._finalStates = new Set(machine._finalStates);
66
59
  result._spawnEffects = [...machine._spawnEffects];
67
60
  result._backgroundEffects = [...machine._backgroundEffects];
68
61
  result._postponeRules = [...machine._postponeRules];
69
62
  result._replySchemas = machine._replySchemas;
70
- if (machine._guardsSchema !== void 0) for (const name of Object.keys(machine._guardsSchema.definitions)) result._guardHandlers.set(name, handlers[name]);
71
- if (machine._effectsSchema !== void 0) for (const name of Object.keys(machine._effectsSchema.definitions)) result._effectHandlers.set(name, handlers[name]);
63
+ if (machine._slotsSchema !== void 0) for (const name of Object.keys(machine._slotsSchema.definitions)) result._slotHandlers.set(name, handlers[name]);
72
64
  return result;
73
65
  };
74
66
  /**
@@ -80,8 +72,7 @@ const materializeMachine = (machine, handlers) => {
80
72
  * - `R`: Effect requirements
81
73
  * - `_SD`: State schema definition (for compile-time validation)
82
74
  * - `_ED`: Event schema definition (for compile-time validation)
83
- * - `GD`: Guard definitions
84
- * - `EFD`: Effect definitions
75
+ * - `SD`: Slot definitions
85
76
  */
86
77
  var Machine = class Machine {
87
78
  initial;
@@ -90,11 +81,10 @@ var Machine = class Machine {
90
81
  /** @internal */ _backgroundEffects;
91
82
  /** @internal */ _finalStates;
92
83
  /** @internal */ _postponeRules;
93
- /** @internal */ _guardsSchema;
94
- /** @internal */ _effectsSchema;
95
- /** @internal */ _guardHandlers;
96
- /** @internal */ _effectHandlers;
84
+ /** @internal */ _slotsSchema;
85
+ /** @internal */ _slotHandlers;
97
86
  /** @internal */ _slots;
87
+ /** @internal */ _slotValidation;
98
88
  stateSchema;
99
89
  eventSchema;
100
90
  /** @internal */ _replySchemas;
@@ -118,49 +108,73 @@ var Machine = class Machine {
118
108
  get postponeRules() {
119
109
  return this._postponeRules;
120
110
  }
121
- get guardsSchema() {
122
- return this._guardsSchema;
123
- }
124
- get effectsSchema() {
125
- return this._effectsSchema;
111
+ get slotsSchema() {
112
+ return this._slotsSchema;
126
113
  }
127
114
  get replySchemas() {
128
115
  return this._replySchemas;
129
116
  }
130
117
  /** @internal */
131
- constructor(initial, stateSchema, eventSchema, guardsSchema, effectsSchema) {
118
+ constructor(initial, stateSchema, eventSchema, slotsSchema, slotValidation = true) {
132
119
  this.initial = initial;
133
120
  this._transitions = [];
134
121
  this._spawnEffects = [];
135
122
  this._backgroundEffects = [];
136
123
  this._finalStates = /* @__PURE__ */ new Set();
137
124
  this._postponeRules = [];
138
- this._guardsSchema = guardsSchema;
139
- this._effectsSchema = effectsSchema;
140
- this._guardHandlers = /* @__PURE__ */ new Map();
141
- 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;
142
129
  this.stateSchema = stateSchema;
143
130
  this.eventSchema = eventSchema;
144
- this._replySchemas = eventSchema?._replySchemas ?? /* @__PURE__ */ new Map();
145
- this._slots = {
146
- guards: this._guardsSchema !== void 0 ? this._guardsSchema._createSlots((name, params) => Effect.flatMap(Effect.serviceOptional(this.Context).pipe(Effect.orDie), (ctx) => {
147
- const handler = this._guardHandlers.get(name);
148
- if (handler === void 0) return Effect.die(new SlotProvisionError({
149
- slotName: name,
150
- slotType: "guard"
151
- }));
152
- const result = handler(params, ctx);
153
- return typeof result === "boolean" ? Effect.succeed(result) : result;
154
- })) : {},
155
- effects: this._effectsSchema !== void 0 ? this._effectsSchema._createSlots((name, params) => Effect.flatMap(Effect.serviceOptional(this.Context).pipe(Effect.orDie), (ctx) => {
156
- const handler = this._effectHandlers.get(name);
157
- if (handler === void 0) return Effect.die(new SlotProvisionError({
158
- slotName: name,
159
- slotType: "effect"
160
- }));
161
- return handler(params, ctx);
162
- })) : {}
163
- };
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) : {};
164
178
  }
165
179
  from(stateOrStates, build) {
166
180
  build(new TransitionScope(this, Array.isArray(stateOrStates) ? stateOrStates : [stateOrStates]));
@@ -208,42 +222,19 @@ var Machine = class Machine {
208
222
  invalidateIndex(this);
209
223
  return this;
210
224
  }
211
- /**
212
- * State-scoped effect that is forked on state entry and automatically cancelled on state exit.
213
- * Use effect slots defined via `Slot.Effects` for the actual work.
214
- *
215
- * @example
216
- * ```ts
217
- * const MyEffects = Slot.Effects({
218
- * fetchData: { url: Schema.String },
219
- * });
220
- *
221
- * machine
222
- * .spawn(State.Loading, ({ effects, state }) => effects.fetchData({ url: state.url }))
223
- * .build({
224
- * fetchData: ({ url }, { self }) =>
225
- * Effect.gen(function* () {
226
- * yield* Effect.addFinalizer(() => Effect.log("Leaving Loading"));
227
- * const data = yield* Http.get(url);
228
- * yield* self.send(Event.Loaded({ data }));
229
- * }),
230
- * });
231
- * ```
232
- */
233
- spawn(state, handler) {
234
- const stateTag = getTag(state);
235
- this._spawnEffects.push({
236
- stateTag,
237
- handler
238
- });
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
+ }
239
234
  invalidateIndex(this);
240
235
  return this;
241
236
  }
242
- /**
243
- * State-scoped task that runs on entry and sends success/failure events.
244
- * Interrupts do not emit failure events.
245
- */
246
- task(state, run, options) {
237
+ task(stateOrStates, run, options) {
247
238
  const handler = Effect.fn("effect-machine.task")(function* (ctx) {
248
239
  yield* emitTaskInspection({
249
240
  actorId: ctx.actorId,
@@ -259,7 +250,8 @@ var Machine = class Machine {
259
250
  taskName: options.name,
260
251
  phase: "success"
261
252
  });
262
- 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);
263
255
  yield* Effect.yieldNow();
264
256
  return;
265
257
  }
@@ -287,7 +279,7 @@ var Machine = class Machine {
287
279
  }
288
280
  return yield* Effect.failCause(cause).pipe(Effect.orDie);
289
281
  });
290
- return this.spawn(state, handler);
282
+ return this.spawn(stateOrStates, handler);
291
283
  }
292
284
  /**
293
285
  * State timeout — gen_statem's `state_timeout`.
@@ -321,22 +313,14 @@ var Machine = class Machine {
321
313
  }
322
314
  /**
323
315
  * Machine-lifetime effect that is forked on actor spawn and runs until the actor stops.
324
- * Use effect slots defined via `Slot.Effects` for the actual work.
325
316
  *
326
317
  * @example
327
318
  * ```ts
328
- * const MyEffects = Slot.Effects({
329
- * heartbeat: {},
330
- * });
331
- *
332
- * machine
333
- * .background(({ effects }) => effects.heartbeat())
334
- * .build({
335
- * heartbeat: (_, { self }) =>
336
- * Effect.forever(
337
- * Effect.sleep("30 seconds").pipe(Effect.andThen(self.send(Event.Ping)))
338
- * ),
339
- * });
319
+ * machine.background(({ self }) =>
320
+ * Effect.forever(
321
+ * Effect.sleep("30 seconds").pipe(Effect.andThen(self.send(Event.Ping))),
322
+ * ),
323
+ * );
340
324
  * ```
341
325
  */
342
326
  background(handler) {
@@ -378,7 +362,7 @@ var Machine = class Machine {
378
362
  return this;
379
363
  }
380
364
  static make(config) {
381
- 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);
382
366
  }
383
367
  };
384
368
  var TransitionScope = class {
@@ -399,16 +383,33 @@ const make = Machine.make;
399
383
  /**
400
384
  * Spawn an actor from a machine.
401
385
  *
402
- * Options:
403
- * - `id` — custom actor ID (default: random)
404
- * - `hydrate` — restore from a previously-saved state snapshot.
405
- * - `slots` — slot handler implementations for slotful machines.
386
+ * For machines with slots, pass implementations via `{ slots: { ... } }`.
387
+ *
388
+ * @example
389
+ * ```ts
390
+ * // No slots
391
+ * const actor = yield* Machine.spawn(machine);
392
+ *
393
+ * // With slots
394
+ * const actor = yield* Machine.spawn(machine, {
395
+ * slots: { canRetry: ({ max }) => attempts < max },
396
+ * });
397
+ *
398
+ * // With lifecycle (recovery + durability)
399
+ * const actor = yield* Machine.spawn(machine, {
400
+ * lifecycle: {
401
+ * recovery: { resolve: ({ machineInitial }) => storage.get("actor-state") },
402
+ * durability: { save: ({ nextState }) => storage.set("actor-state", nextState) },
403
+ * },
404
+ * });
405
+ * ```
406
406
  */
407
- const spawn = Effect.fn("effect-machine.spawn")(function* (machine, options) {
408
- const opts = typeof options === "string" ? { id: options } : options;
409
- const actor = yield* createActor(opts?.id ?? `actor-${Math.random().toString(36).slice(2)}`, materializeMachine(machine, opts?.slots), {
407
+ const spawn = Effect.fn("effect-machine.spawn")(function* (machine, idOrOptions) {
408
+ const opts = typeof idOrOptions === "string" ? { id: idOrOptions } : idOrOptions;
409
+ const actor = yield* createActor(opts?.id ?? `actor-${(yield* Random.next).toString(36).slice(2)}`, materializeMachine(machine, opts?.slots), {
410
410
  initialState: opts?.hydrate,
411
- supervision: opts?.supervision
411
+ supervision: opts?.supervision,
412
+ lifecycle: opts?.lifecycle
412
413
  });
413
414
  const maybeScope = yield* Effect.serviceOption(Scope.Scope);
414
415
  if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, actor.stop);
@@ -423,7 +424,8 @@ const replay = Effect.fn("effect-machine.replay")(function* (input, events, opti
423
424
  const self = {
424
425
  send: dummySend,
425
426
  cast: dummySend,
426
- spawn: () => Effect.die("spawn not supported in replay")
427
+ spawn: () => Effect.die("spawn not supported in replay"),
428
+ reply: () => Effect.succeed(false)
427
429
  };
428
430
  for (const event of events) {
429
431
  if (machine.finalStates.has(state._tag)) break;
@@ -458,5 +460,6 @@ const replay = Effect.fn("effect-machine.replay")(function* (input, events, opti
458
460
  return state;
459
461
  });
460
462
  const reply = makeReply;
463
+ const deferReply = makeDeferReply;
461
464
  //#endregion
462
- export { Machine, findTransitions, machine_exports, make, materializeMachine, 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);