effect-machine 0.13.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.
- package/README.md +14 -9
- package/dist/actor.d.ts +9 -6
- package/dist/actor.js +97 -43
- package/dist/cluster/entity-machine.d.ts +1 -1
- package/dist/cluster/entity-machine.js +1 -1
- package/dist/cluster/to-entity.d.ts +1 -1
- package/dist/errors.d.ts +9 -2
- package/dist/errors.js +8 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/internal/runtime.d.ts +2 -2
- package/dist/internal/runtime.js +5 -10
- package/dist/internal/transition.d.ts +9 -9
- package/dist/internal/transition.js +3 -5
- package/dist/machine.d.ts +122 -135
- package/dist/machine.js +97 -112
- package/dist/schema.d.ts +14 -0
- package/dist/schema.js +9 -0
- package/dist/slot.d.ts +112 -86
- package/dist/slot.js +92 -59
- package/dist/testing.d.ts +16 -16
- package/dist/testing.js +3 -3
- package/package.json +3 -3
- package/v3/dist/actor.d.ts +19 -12
- package/v3/dist/actor.js +130 -75
- package/v3/dist/cluster/entity-machine.d.ts +1 -1
- package/v3/dist/cluster/to-entity.d.ts +1 -1
- package/v3/dist/errors.d.ts +12 -3
- package/v3/dist/errors.js +10 -4
- package/v3/dist/index.d.ts +6 -6
- package/v3/dist/index.js +2 -2
- package/v3/dist/inspection.d.ts +3 -22
- package/v3/dist/inspection.js +1 -15
- package/v3/dist/internal/brands.d.ts +4 -8
- package/v3/dist/internal/inspection.js +1 -1
- package/v3/dist/internal/runtime.d.ts +8 -8
- package/v3/dist/internal/runtime.js +45 -28
- package/v3/dist/internal/transition.d.ts +10 -10
- package/v3/dist/internal/transition.js +8 -10
- package/v3/dist/internal/utils.js +5 -1
- package/v3/dist/machine.d.ts +153 -120
- package/v3/dist/machine.js +118 -115
- package/v3/dist/schema.d.ts +25 -11
- package/v3/dist/schema.js +18 -5
- package/v3/dist/slot.d.ts +112 -86
- package/v3/dist/slot.js +92 -59
- package/v3/dist/testing.d.ts +16 -16
- package/v3/dist/testing.js +7 -7
package/v3/dist/machine.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
* - `
|
|
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 */
|
|
94
|
-
/** @internal */
|
|
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
|
|
122
|
-
return this.
|
|
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,
|
|
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.
|
|
139
|
-
this.
|
|
140
|
-
this.
|
|
141
|
-
this.
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}))
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
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.
|
|
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
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
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 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
|
+
* ```
|
|
406
406
|
*/
|
|
407
|
-
const spawn = Effect.fn("effect-machine.spawn")(function* (machine,
|
|
408
|
-
const opts = typeof
|
|
409
|
-
const actor = yield* createActor(opts?.id ?? `actor-${
|
|
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
|
+
persist: opts?.persist
|
|
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 };
|
package/v3/dist/schema.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
54
|
-
readonly derive: (source: object, partial?: Partial<Schema.Struct.
|
|
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>,
|
|
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>,
|
|
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
|
|
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
|
-
|
|
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,
|
|
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);
|