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
package/dist/machine.js CHANGED
@@ -1,19 +1,19 @@
1
1
  import { __exportAll } from "./_virtual/_rolldown/runtime.js";
2
- import { Inspector } from "./inspection.js";
3
2
  import { getTag, makeDeferReply, makeReply, stubSystem } from "./internal/utils.js";
4
- import { ProvisionValidationError, SlotProvisionError } from "./errors.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,
14
13
  deferReply: () => deferReply,
15
14
  findTransitions: () => findTransitions,
16
15
  make: () => make,
16
+ materializeMachine: () => materializeMachine,
17
17
  replay: () => replay,
18
18
  reply: () => reply,
19
19
  spawn: () => spawn
@@ -28,22 +28,40 @@ const emitTaskInspection = (input) => Effect.flatMap(Effect.serviceOption(Inspec
28
28
  timestamp
29
29
  })));
30
30
  /**
31
- * 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.
32
34
  *
33
- * Created by calling `.build()` on a `Machine`. This is the only type
34
- * accepted by `Machine.spawn` and `ActorSystem.spawn` (regular overload).
35
- * Testing utilities (`simulate`, `createTestHarness`, etc.) still accept `Machine`.
35
+ * @internal used by spawn, replay, simulate, test harness, entity-machine
36
36
  */
37
- var BuiltMachine = class {
38
- /** @internal */
39
- _inner;
40
- /** @internal */
41
- constructor(machine) {
42
- this._inner = machine;
43
- }
44
- get initial() {
45
- 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;
46
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;
47
65
  };
48
66
  /**
49
67
  * Machine definition with fluent builder API.
@@ -54,8 +72,7 @@ var BuiltMachine = class {
54
72
  * - `R`: Effect requirements
55
73
  * - `_SD`: State schema definition (for compile-time validation)
56
74
  * - `_ED`: Event schema definition (for compile-time validation)
57
- * - `GD`: Guard definitions
58
- * - `EFD`: Effect definitions
75
+ * - `SD`: Slot definitions
59
76
  */
60
77
  var Machine = class Machine {
61
78
  initial;
@@ -64,11 +81,10 @@ var Machine = class Machine {
64
81
  /** @internal */ _backgroundEffects;
65
82
  /** @internal */ _finalStates;
66
83
  /** @internal */ _postponeRules;
67
- /** @internal */ _guardsSchema;
68
- /** @internal */ _effectsSchema;
69
- /** @internal */ _guardHandlers;
70
- /** @internal */ _effectHandlers;
84
+ /** @internal */ _slotsSchema;
85
+ /** @internal */ _slotHandlers;
71
86
  /** @internal */ _slots;
87
+ /** @internal */ _slotValidation;
72
88
  stateSchema;
73
89
  eventSchema;
74
90
  /** @internal */ _replySchemas;
@@ -92,53 +108,73 @@ var Machine = class Machine {
92
108
  get postponeRules() {
93
109
  return this._postponeRules;
94
110
  }
95
- get guardsSchema() {
96
- return this._guardsSchema;
97
- }
98
- get effectsSchema() {
99
- return this._effectsSchema;
111
+ get slotsSchema() {
112
+ return this._slotsSchema;
100
113
  }
101
114
  get replySchemas() {
102
115
  return this._replySchemas;
103
116
  }
104
117
  /** @internal */
105
- constructor(initial, stateSchema, eventSchema, guardsSchema, effectsSchema) {
118
+ constructor(initial, stateSchema, eventSchema, slotsSchema, slotValidation = true) {
106
119
  this.initial = initial;
107
120
  this._transitions = [];
108
121
  this._spawnEffects = [];
109
122
  this._backgroundEffects = [];
110
123
  this._finalStates = /* @__PURE__ */ new Set();
111
124
  this._postponeRules = [];
112
- this._guardsSchema = guardsSchema;
113
- this._effectsSchema = effectsSchema;
125
+ this._slotsSchema = slotsSchema;
114
126
  this._replySchemas = eventSchema?._replySchemas ?? /* @__PURE__ */ new Map();
115
- this._guardHandlers = /* @__PURE__ */ new Map();
116
- this._effectHandlers = /* @__PURE__ */ new Map();
127
+ this._slotHandlers = /* @__PURE__ */ new Map();
128
+ this._slotValidation = slotValidation;
117
129
  this.stateSchema = stateSchema;
118
130
  this.eventSchema = eventSchema;
119
- this._slots = {
120
- guards: this._guardsSchema !== void 0 ? this._guardsSchema._createSlots((name, params) => Effect.flatMap(Effect.serviceOption(this.Context), (maybeCtx) => {
121
- if (Option.isNone(maybeCtx)) return Effect.die("MachineContext not available");
122
- const ctx = maybeCtx.value;
123
- const handler = this._guardHandlers.get(name);
124
- if (handler === void 0) return Effect.die(new SlotProvisionError({
125
- slotName: name,
126
- slotType: "guard"
127
- }));
128
- const result = handler(params, ctx);
129
- return typeof result === "boolean" ? Effect.succeed(result) : result;
130
- })) : {},
131
- effects: this._effectsSchema !== void 0 ? this._effectsSchema._createSlots((name, params) => Effect.flatMap(Effect.serviceOption(this.Context), (maybeCtx) => {
132
- if (Option.isNone(maybeCtx)) return Effect.die("MachineContext not available");
133
- const ctx = maybeCtx.value;
134
- const handler = this._effectHandlers.get(name);
135
- if (handler === void 0) return Effect.die(new SlotProvisionError({
136
- slotName: name,
137
- slotType: "effect"
138
- }));
139
- return handler(params, ctx);
140
- })) : {}
141
- };
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) : {};
142
178
  }
143
179
  from(stateOrStates, build) {
144
180
  build(new TransitionScope(this, Array.isArray(stateOrStates) ? stateOrStates : [stateOrStates]));
@@ -186,42 +222,19 @@ var Machine = class Machine {
186
222
  invalidateIndex(this);
187
223
  return this;
188
224
  }
189
- /**
190
- * State-scoped effect that is forked on state entry and automatically cancelled on state exit.
191
- * Use effect slots defined via `Slot.Effects` for the actual work.
192
- *
193
- * @example
194
- * ```ts
195
- * const MyEffects = Slot.Effects({
196
- * fetchData: { url: Schema.String },
197
- * });
198
- *
199
- * machine
200
- * .spawn(State.Loading, ({ effects, state }) => effects.fetchData({ url: state.url }))
201
- * .build({
202
- * fetchData: ({ url }, { self }) =>
203
- * Effect.gen(function* () {
204
- * yield* Effect.addFinalizer(() => Effect.log("Leaving Loading"));
205
- * const data = yield* Http.get(url);
206
- * yield* self.send(Event.Loaded({ data }));
207
- * }),
208
- * });
209
- * ```
210
- */
211
- spawn(state, handler) {
212
- const stateTag = getTag(state);
213
- this._spawnEffects.push({
214
- stateTag,
215
- handler
216
- });
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
+ }
217
234
  invalidateIndex(this);
218
235
  return this;
219
236
  }
220
- /**
221
- * State-scoped task that runs on entry and sends success/failure events.
222
- * Interrupts do not emit failure events.
223
- */
224
- task(state, run, options) {
237
+ task(stateOrStates, run, options) {
225
238
  const handler = Effect.fn("effect-machine.task")(function* (ctx) {
226
239
  yield* emitTaskInspection({
227
240
  actorId: ctx.actorId,
@@ -237,7 +250,8 @@ var Machine = class Machine {
237
250
  taskName: options.name,
238
251
  phase: "success"
239
252
  });
240
- 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);
241
255
  yield* Effect.yieldNow;
242
256
  return;
243
257
  }
@@ -265,7 +279,7 @@ var Machine = class Machine {
265
279
  }
266
280
  return yield* Effect.failCause(cause).pipe(Effect.orDie);
267
281
  });
268
- return this.spawn(state, handler);
282
+ return this.spawn(stateOrStates, handler);
269
283
  }
270
284
  /**
271
285
  * State timeout — gen_statem's `state_timeout`.
@@ -299,22 +313,14 @@ var Machine = class Machine {
299
313
  }
300
314
  /**
301
315
  * Machine-lifetime effect that is forked on actor spawn and runs until the actor stops.
302
- * Use effect slots defined via `Slot.Effects` for the actual work.
303
316
  *
304
317
  * @example
305
318
  * ```ts
306
- * const MyEffects = Slot.Effects({
307
- * heartbeat: {},
308
- * });
309
- *
310
- * machine
311
- * .background(({ effects }) => effects.heartbeat())
312
- * .build({
313
- * heartbeat: (_, { self }) =>
314
- * Effect.forever(
315
- * Effect.sleep("30 seconds").pipe(Effect.andThen(self.send(Event.Ping)))
316
- * ),
317
- * });
319
+ * machine.background(({ self }) =>
320
+ * Effect.forever(
321
+ * Effect.sleep("30 seconds").pipe(Effect.andThen(self.send(Event.Ping))),
322
+ * ),
323
+ * );
318
324
  * ```
319
325
  */
320
326
  background(handler) {
@@ -355,43 +361,8 @@ var Machine = class Machine {
355
361
  this._finalStates.add(stateTag);
356
362
  return this;
357
363
  }
358
- /**
359
- * Finalize the machine. Returns a `BuiltMachine` — the only type accepted by `Machine.spawn`.
360
- *
361
- * - Machines with slots: pass implementations as the first argument.
362
- * - Machines without slots: call with no arguments.
363
- */
364
- build(...args) {
365
- const handlers = args[0];
366
- if (handlers !== void 0) {
367
- const requiredSlots = /* @__PURE__ */ new Set();
368
- if (this._guardsSchema !== void 0) for (const name of Object.keys(this._guardsSchema.definitions)) requiredSlots.add(name);
369
- if (this._effectsSchema !== void 0) for (const name of Object.keys(this._effectsSchema.definitions)) requiredSlots.add(name);
370
- const providedSlots = new Set(Object.keys(handlers));
371
- const missing = [];
372
- const extra = [];
373
- for (const name of requiredSlots) if (!providedSlots.has(name)) missing.push(name);
374
- for (const name of providedSlots) if (!requiredSlots.has(name)) extra.push(name);
375
- if (missing.length > 0 || extra.length > 0) throw new ProvisionValidationError({
376
- missing,
377
- extra
378
- });
379
- const result = new Machine(this.initial, this.stateSchema, this.eventSchema, this._guardsSchema, this._effectsSchema);
380
- result._transitions = [...this._transitions];
381
- result._finalStates = new Set(this._finalStates);
382
- result._spawnEffects = [...this._spawnEffects];
383
- result._backgroundEffects = [...this._backgroundEffects];
384
- result._postponeRules = [...this._postponeRules];
385
- result._replySchemas = this._replySchemas;
386
- const anyHandlers = handlers;
387
- if (this._guardsSchema !== void 0) for (const name of Object.keys(this._guardsSchema.definitions)) result._guardHandlers.set(name, anyHandlers[name]);
388
- if (this._effectsSchema !== void 0) for (const name of Object.keys(this._effectsSchema.definitions)) result._effectHandlers.set(name, anyHandlers[name]);
389
- return new BuiltMachine(result);
390
- }
391
- return new BuiltMachine(this);
392
- }
393
364
  static make(config) {
394
- 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);
395
366
  }
396
367
  };
397
368
  var TransitionScope = class {
@@ -410,27 +381,42 @@ var TransitionScope = class {
410
381
  };
411
382
  const make = Machine.make;
412
383
  /**
413
- * 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);
414
392
  *
415
- * Options:
416
- * - `id` custom actor ID (default: random)
417
- * - `hydrate` restore from a previously-saved state snapshot.
418
- * The actor starts in the hydrated state and re-runs spawn effects
419
- * for that state (timers, scoped resources, etc.). Transition history
420
- * 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
+ * });
421
397
  *
422
- * Persistence is composed in userland by observing `actor.changes`
423
- * 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
+ * ```
424
406
  */
425
- const spawn = Effect.fn("effect-machine.spawn")(function* (built, idOrOptions) {
407
+ const spawn = Effect.fn("effect-machine.spawn")(function* (machine, idOrOptions) {
426
408
  const opts = typeof idOrOptions === "string" ? { id: idOrOptions } : idOrOptions;
427
- 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
+ });
428
414
  const maybeScope = yield* Effect.serviceOption(Scope.Scope);
429
415
  if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, actor.stop);
430
416
  return actor;
431
417
  });
432
- const replay = Effect.fn("effect-machine.replay")(function* (built, events, options) {
433
- const machine = built._inner;
418
+ const replay = Effect.fn("effect-machine.replay")(function* (input, events, options) {
419
+ const machine = materializeMachine(input, options?.slots);
434
420
  let state = options?.from ?? machine.initial;
435
421
  const hasPostponeRules = machine.postponeRules.length > 0;
436
422
  const postponed = [];
@@ -476,4 +462,4 @@ const replay = Effect.fn("effect-machine.replay")(function* (built, events, opti
476
462
  const reply = makeReply;
477
463
  const deferReply = makeDeferReply;
478
464
  //#endregion
479
- export { BuiltMachine, Machine, deferReply, findTransitions, machine_exports, make, replay, reply, spawn };
465
+ export { Machine, deferReply, findTransitions, machine_exports, make, materializeMachine, replay, reply, spawn };
package/dist/schema.d.ts CHANGED
@@ -81,6 +81,20 @@ interface MachineSchemaBase<D extends Record<string, Schema.Struct.Fields>, Bran
81
81
  <R>(cases: MatchCases<D, R>): (value: VariantsUnion<D> & Brand) => R;
82
82
  <R>(value: VariantsUnion<D> & Brand, cases: MatchCases<D, R>): R;
83
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;
84
98
  /**
85
99
  * Reply schemas per variant tag. Only populated for event schemas
86
100
  * with variants defined via `Event.reply()`.
package/dist/schema.js CHANGED
@@ -67,6 +67,7 @@ const buildMachineSchema = (definition) => {
67
67
  for (const key of fieldNames) if (key in source) result[key] = source[key];
68
68
  if (partial !== void 0) for (const [key, value] of Object.entries(partial)) {
69
69
  if (RESERVED_DERIVE_KEYS.has(key)) continue;
70
+ if (!fieldNames.has(key)) continue;
70
71
  result[key] = value;
71
72
  }
72
73
  return result;
@@ -78,7 +79,7 @@ const buildMachineSchema = (definition) => {
78
79
  };
79
80
  }
80
81
  const variantArray = Object.values(variants);
81
- if (variantArray.length === 0) throw new InvalidSchemaError({});
82
+ if (variantArray.length === 0) throw new InvalidSchemaError({ message: "Schema must have at least one variant" });
82
83
  const unionSchema = variantArray.length === 1 ? variantArray[0] : Schema.Union(variantArray);
83
84
  const $is = (tag) => (u) => typeof u === "object" && u !== null && "_tag" in u && u._tag === tag;
84
85
  const $match = (valueOrCases, maybeCases) => {
@@ -111,12 +112,20 @@ const buildMachineSchema = (definition) => {
111
112
  */
112
113
  const createMachineSchema = (definition) => {
113
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
+ };
114
122
  return Object.assign(Object.create(schema), {
115
123
  variants,
116
124
  _definition,
117
125
  _replySchemas: replySchemas,
118
126
  $is,
119
127
  $match,
128
+ derive,
120
129
  ...constructors
121
130
  });
122
131
  };