@typeonce/effect-machine 0.1.0 → 0.3.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/dist/Machine.js CHANGED
@@ -104,6 +104,7 @@ const ActionRequirementTypeId = "~effect/Machine/ActionRequirement";
104
104
  const RuntimeCompatibilityErrorTypeId = "~effect/Machine/RuntimeCompatibilityError";
105
105
  const InvokeTypeId = Symbol.for("effect/Machine/Invoke");
106
106
  const SnapshotBuilderStateTypeId = Symbol("effect/Machine/SnapshotBuilderState");
107
+ const SnapshotBuilderConstructionTypeId = Symbol("effect/Machine/SnapshotBuilderConstruction");
107
108
  const ChildAddressTypeId = "~effect/Machine/ChildAddress";
108
109
  const ChildAddressCompatibilityErrorTypeId = "~effect/Machine/ChildAddressCompatibilityError";
109
110
  const ChildMachineTypeId = "~effect/Machine/ChildMachine";
@@ -121,6 +122,7 @@ const cloneWithHandlers = (self, handlers) => {
121
122
  const machine = Object.create(Proto);
122
123
  machine.states = self.states;
123
124
  machine.events = self.events;
125
+ machine.internalEvents = self.internalEvents;
124
126
  machine.emits = self.emits;
125
127
  machine.input = self.input;
126
128
  machine.id = self.id;
@@ -129,6 +131,7 @@ const cloneWithHandlers = (self, handlers) => {
129
131
  machine.makeTargetBuilder = self.makeTargetBuilder;
130
132
  machine.handlers = handlers;
131
133
  machine.handle = makeHandle(machine);
134
+ Model.copyProtocol(self, machine);
132
135
  return machine;
133
136
  };
134
137
  const flattenHandlers = (handlers, states, prefix, config) => {
@@ -174,10 +177,27 @@ export const isMachine = (u) => hasProperty(u, TypeId);
174
177
  * @since 4.0.0
175
178
  */
176
179
  export const isFinal = (machine, state) => internalPlanner.isFinal(machine, state);
180
+ const withFrom = (method, kind) => {
181
+ Object.defineProperty(method, "from", {
182
+ value: (...args) => {
183
+ const omitted = args.length === 0 || (kind === "nested" && args.length === 1 && typeof args[0] === "function");
184
+ const input = omitted ? {} : args[0];
185
+ const rest = omitted ? args : args.slice(1);
186
+ return Model.markStateConstruction(method(Model.makeStateInput(input), ...rest));
187
+ },
188
+ enumerable: false
189
+ });
190
+ return method;
191
+ };
177
192
  const makeSnapshotBuilder = (states, options) => {
178
193
  const builder = {};
179
194
  for (const key of Object.keys(states)) {
180
- builder[key] = (value, selector) => makeSnapshotForNode(states[key], key, value, selector, options);
195
+ if (states[key].type === "history") {
196
+ continue;
197
+ }
198
+ const path = options.prefix === "" ? key : `${options.prefix}.${key}`;
199
+ const node = Model.getStateNodeDefinition(path, states[key]);
200
+ builder[key] = withFrom((value, selector) => makeSnapshotForNode(states[key], key, value, selector, options), node.states === undefined ? "leaf" : "nested");
181
201
  }
182
202
  return builder;
183
203
  };
@@ -187,18 +207,28 @@ const makeParallelSnapshotBuilder = (states, options, regions) => {
187
207
  value: regions,
188
208
  enumerable: false
189
209
  });
210
+ Object.defineProperty(builder, SnapshotBuilderConstructionTypeId, {
211
+ value: Model.isStateConstruction(builder),
212
+ enumerable: false
213
+ });
190
214
  for (const key of Object.keys(states)) {
215
+ if (states[key].type === "history") {
216
+ continue;
217
+ }
191
218
  if (hasProperty(regions, key)) {
192
219
  continue;
193
220
  }
194
- builder[key] = (value, selector) => {
221
+ const path = options.prefix === "" ? key : `${options.prefix}.${key}`;
222
+ const node = Model.getStateNodeDefinition(path, states[key]);
223
+ builder[key] = withFrom((value, selector) => {
195
224
  const nextRegions = {};
196
225
  for (const regionKey of Object.keys(regions)) {
197
226
  nextRegions[regionKey] = regions[regionKey];
198
227
  }
199
228
  nextRegions[key] = makeSnapshotForNode(states[key], key, value, selector, options);
200
- return makeParallelSnapshotBuilder(states, options, nextRegions);
201
- };
229
+ const next = makeParallelSnapshotBuilder(states, options, nextRegions);
230
+ return Model.isStateConstruction(builder) ? Model.markStateConstruction(next) : next;
231
+ }, node.states === undefined ? "leaf" : "nested");
202
232
  }
203
233
  return builder;
204
234
  };
@@ -208,6 +238,9 @@ const getParallelSnapshotBuilderRegions = (path, states, builder) => {
208
238
  }
209
239
  const regions = builder[SnapshotBuilderStateTypeId];
210
240
  for (const key of Object.keys(states)) {
241
+ if (states[key].type === "history") {
242
+ continue;
243
+ }
211
244
  if (!hasProperty(regions, key)) {
212
245
  throw new Error(`Machine expected parallel state "${path}" builder callback to provide region "${key}"`);
213
246
  }
@@ -229,14 +262,16 @@ const makeSnapshotForNode = (definition, key, value, selector, options) => {
229
262
  }
230
263
  if (node.type === "parallel") {
231
264
  const builder = makeParallelSnapshotBuilder(node.states, { ...options, prefix: path }, {});
232
- snapshot.states = getParallelSnapshotBuilderRegions(path, node.states, selector(builder));
233
- return snapshot;
265
+ const selected = selector(builder);
266
+ snapshot.states = getParallelSnapshotBuilderRegions(path, node.states, selected);
267
+ return Model.isStateConstruction(selected) ? Model.markStateConstruction(snapshot) : snapshot;
234
268
  }
235
269
  const childStates = options.mode === "initial" && node.initial !== undefined
236
270
  ? { [node.initial]: node.states[node.initial] }
237
271
  : node.states;
238
- snapshot.state = selector(makeSnapshotBuilder(childStates, { ...options, prefix: path }));
239
- return snapshot;
272
+ const selected = selector(makeSnapshotBuilder(childStates, { ...options, prefix: path }));
273
+ snapshot.state = selected;
274
+ return Model.isStateConstruction(selected) ? Model.markStateConstruction(snapshot) : snapshot;
240
275
  };
241
276
  const getTargetBuilderNode = (stateNodes, path) => {
242
277
  const node = stateNodes.byPath.get(path);
@@ -263,6 +298,31 @@ const hasTargetValues = (values) => values !== undefined && Object.keys(values).
263
298
  const makeTargetWithValues = (path, value, values) => hasTargetValues(values)
264
299
  ? Model.makeTarget(path, value, { values: values })
265
300
  : Model.makeTarget(path, value);
301
+ const getTargetBuilderDefinition = (states, targetPath) => {
302
+ let children = states;
303
+ let path = "";
304
+ let definition;
305
+ for (const key of targetPath.split(".")) {
306
+ if (!hasProperty(children, key)) {
307
+ throw new Error(`Machine expected state path "${targetPath}" to exist`);
308
+ }
309
+ definition = children[key];
310
+ path = path === "" ? key : `${path}.${key}`;
311
+ const node = Model.getStateNodeDefinition(path, definition);
312
+ children = node.states ?? {};
313
+ }
314
+ return definition;
315
+ };
316
+ const makeParallelTarget = (states, node, value, selector, values) => {
317
+ if (selector === undefined) {
318
+ throw new Error(`Machine expected parallel target "${node.path}" builder to provide every active region`);
319
+ }
320
+ const snapshot = makeSnapshotForNode(getTargetBuilderDefinition(states, node.path), node.key, value, selector, { mode: "full", prefix: node.parent ?? "" });
321
+ return Model.makeTarget(node.path, value, {
322
+ snapshot: snapshot,
323
+ values: values
324
+ });
325
+ };
266
326
  const extendTargetValues = (values, path, value) => {
267
327
  const next = {};
268
328
  if (values !== undefined) {
@@ -273,100 +333,122 @@ const extendTargetValues = (values, path, value) => {
273
333
  next[path] = value;
274
334
  return next;
275
335
  };
276
- const makeLocalTargetChildBuilder = (stateNodes, parentPath, values) => {
336
+ const makeLocalTargetChildBuilder = (states, stateNodes, parentPath, values, source) => {
277
337
  const parent = getTargetBuilderNode(stateNodes, parentPath);
278
338
  const builder = {};
279
339
  for (const childPath of parent.children) {
280
340
  const child = getTargetBuilderNode(stateNodes, childPath);
281
- builder[child.key] = (value, selector) => {
341
+ builder[child.key] = withFrom((value, selector) => {
282
342
  if (child.type === "atomic" || child.type === "final") {
283
343
  return makeTargetWithValues(child.path, value, values);
284
344
  }
345
+ if (child.type === "parallel") {
346
+ if (source !== child.path && !source.startsWith(`${child.path}.`)) {
347
+ return makeParallelTarget(states, child, value, selector, values);
348
+ }
349
+ if (selector === undefined) {
350
+ throw new Error(`Machine expected target "${child.path}" builder to provide an active child state`);
351
+ }
352
+ return selector(makeLocalTargetChildBuilder(states, stateNodes, child.path, extendTargetValues(values, child.path, value), source));
353
+ }
285
354
  if (selector === undefined) {
286
355
  throw new Error(`Machine expected target "${child.path}" builder to provide an active child state`);
287
356
  }
288
- return selector(makeLocalTargetChildBuilder(stateNodes, child.path, extendTargetValues(values, child.path, value)));
289
- };
357
+ return selector(makeLocalTargetChildBuilder(states, stateNodes, child.path, extendTargetValues(values, child.path, value), source));
358
+ }, child.type === "atomic" || child.type === "final" ? "leaf" : "nested");
290
359
  }
291
360
  return builder;
292
361
  };
293
- const makeLocalTargetBuilder = (stateNodes, source) => {
362
+ const makeLocalTargetBuilder = (states, stateNodes, source) => {
294
363
  const scope = getLocalTargetScope(stateNodes, source);
295
364
  if (scope === undefined) {
296
365
  return {};
297
366
  }
298
- const builder = makeLocalTargetChildBuilder(stateNodes, scope, undefined);
299
- builder.with = (value, selector) => {
367
+ const builder = makeLocalTargetChildBuilder(states, stateNodes, scope, undefined, source);
368
+ builder.with = withFrom((value, selector) => {
300
369
  if (selector === undefined) {
301
370
  throw new Error(`Machine expected target "${scope}" builder to provide an active child state`);
302
371
  }
303
- return selector(makeLocalTargetChildBuilder(stateNodes, scope, { [scope]: value }));
304
- };
372
+ return selector(makeLocalTargetChildBuilder(states, stateNodes, scope, { [scope]: value }, source));
373
+ }, "nested");
305
374
  return builder;
306
375
  };
307
- const addBranchTargetChildren = (builder, stateNodes, parentPath, values) => {
376
+ const addBranchTargetChildren = (builder, states, stateNodes, parentPath, values, source) => {
308
377
  const parent = getTargetBuilderNode(stateNodes, parentPath);
309
378
  for (const childPath of parent.children) {
310
379
  const child = getTargetBuilderNode(stateNodes, childPath);
311
- builder[child.key] = makeBranchTargetNodeBuilder(stateNodes, child.path, values);
380
+ builder[child.key] = makeBranchTargetNodeBuilder(states, stateNodes, child.path, values, source);
312
381
  }
313
382
  };
314
- const makeBranchTargetNodeBuilder = (stateNodes, path, values) => {
383
+ const makeBranchTargetNodeBuilder = (states, stateNodes, path, values, source) => {
315
384
  const node = getTargetBuilderNode(stateNodes, path);
316
385
  if (node.type === "atomic" || node.type === "final") {
317
- return (value) => makeTargetWithValues(node.path, value, values);
386
+ return withFrom((value) => makeTargetWithValues(node.path, value, values), "leaf");
318
387
  }
319
- const builder = ((value, selector) => {
388
+ const builder = withFrom((value, selector) => {
389
+ if (node.type === "parallel") {
390
+ if (source !== node.path && !source.startsWith(`${node.path}.`)) {
391
+ return makeParallelTarget(states, node, value, selector, values);
392
+ }
393
+ if (selector === undefined) {
394
+ throw new Error(`Machine expected target "${node.path}" builder to provide an active child state`);
395
+ }
396
+ const nextBuilder = {};
397
+ addBranchTargetChildren(nextBuilder, states, stateNodes, node.path, extendTargetValues(values, node.path, value), source);
398
+ return selector(nextBuilder);
399
+ }
320
400
  if (selector === undefined) {
321
401
  throw new Error(`Machine expected target "${node.path}" builder to provide an active child state`);
322
402
  }
323
403
  const nextBuilder = {};
324
- addBranchTargetChildren(nextBuilder, stateNodes, node.path, extendTargetValues(values, node.path, value));
404
+ addBranchTargetChildren(nextBuilder, states, stateNodes, node.path, extendTargetValues(values, node.path, value), source);
325
405
  return selector(nextBuilder);
326
- });
327
- addBranchTargetChildren(builder, stateNodes, node.path, values);
406
+ }, "nested");
407
+ if (node.type !== "parallel" || source === node.path || source.startsWith(`${node.path}.`)) {
408
+ addBranchTargetChildren(builder, states, stateNodes, node.path, values, source);
409
+ }
328
410
  return builder;
329
411
  };
330
- const makeBranchTargetBuilder = (stateNodes, source) => {
412
+ const makeBranchTargetBuilder = (states, stateNodes, source) => {
331
413
  const rootPath = source.split(".")[0];
332
414
  const root = getTargetBuilderNode(stateNodes, rootPath);
333
415
  return {
334
- [root.key]: makeBranchTargetNodeBuilder(stateNodes, root.path, undefined)
416
+ [root.key]: makeBranchTargetNodeBuilder(states, stateNodes, root.path, undefined, source)
335
417
  };
336
418
  };
419
+ const makeHistoryTargetBuilder = (states, prefix) => {
420
+ const builder = {};
421
+ for (const key of Object.keys(states)) {
422
+ const path = prefix === "" ? key : `${prefix}.${key}`;
423
+ const definition = states[key];
424
+ if (definition.type === "history") {
425
+ const parent = getParentPathRuntime(path);
426
+ builder[key] = () => Model.makeHistoryTarget(path, parent);
427
+ continue;
428
+ }
429
+ if (typeof definition === "object" && definition !== null && hasProperty(definition, "states")) {
430
+ builder[key] = makeHistoryTargetBuilder(definition.states, path);
431
+ }
432
+ }
433
+ return builder;
434
+ };
435
+ const getParentPathRuntime = (path) => {
436
+ const separator = path.lastIndexOf(".");
437
+ if (separator < 0) {
438
+ throw new Error(`Machine expected history state "${path}" to have an active parent`);
439
+ }
440
+ return path.slice(0, separator);
441
+ };
337
442
  const makeTargetBuilder = (states, stateNodes) => {
338
443
  const full = makeSnapshotBuilder(states, { mode: "full", prefix: "" });
444
+ const history = makeHistoryTargetBuilder(states, "");
339
445
  return (source) => ({
340
- local: makeLocalTargetBuilder(stateNodes, source),
341
- branch: makeBranchTargetBuilder(stateNodes, source),
342
- full
446
+ local: makeLocalTargetBuilder(states, stateNodes, source),
447
+ branch: makeBranchTargetBuilder(states, stateNodes, source),
448
+ full,
449
+ history
343
450
  });
344
451
  };
345
- const getSnapshotByPath = (snapshot, path, parents) => {
346
- if (snapshot.path === path) {
347
- return Option.some(snapshot);
348
- }
349
- if (!path.startsWith(`${snapshot.path}.`)) {
350
- return Option.none();
351
- }
352
- if (parents !== undefined) {
353
- parents[snapshot.path] = snapshot.value;
354
- }
355
- if (hasProperty(snapshot, "state") && Model.isSnapshot(snapshot.state)) {
356
- return getSnapshotByPath(snapshot.state, path, parents);
357
- }
358
- if (hasProperty(snapshot, "states") && typeof snapshot.states === "object" && snapshot.states !== null) {
359
- for (const child of Object.values(snapshot.states)) {
360
- if (Model.isSnapshot(child)) {
361
- const result = getSnapshotByPath(child, path, parents);
362
- if (Option.isSome(result)) {
363
- return result;
364
- }
365
- }
366
- }
367
- }
368
- return Option.none();
369
- };
370
452
  /**
371
453
  * Defines a state tree while preserving literal state paths.
372
454
  *
@@ -385,7 +467,7 @@ const getSnapshotByPath = (snapshot, path, parents) => {
385
467
  *
386
468
  * ```ts
387
469
  * import { Schema } from "effect"
388
- * import { Machine } from "effect/unstable/machine"
470
+ * import { Machine } from "@typeonce/effect-machine"
389
471
  *
390
472
  * class Idle extends Schema.TaggedClass<Idle>("Idle")("Idle", {}) {}
391
473
  *
@@ -394,24 +476,24 @@ const getSnapshotByPath = (snapshot, path, parents) => {
394
476
  * Machine.make({
395
477
  * states: States.states,
396
478
  * events: [],
397
- * initial: () => States.initial.idle(new Idle({}))
479
+ * initial: () => States.initial.idle.from()
398
480
  * })
399
481
  * ```
400
482
  *
401
483
  * @category constructors
402
484
  * @since 4.0.0
403
485
  */
404
- export const defineStates = (states, ..._validation) => ({
486
+ export const defineStates = ((states) => ({
405
487
  states: states,
406
488
  initial: makeSnapshotBuilder(states, { mode: "initial", prefix: "" }),
407
- get: ((snapshot, path) => getSnapshotByPath(snapshot, path).pipe(Option.map((snapshot) => snapshot.value))),
489
+ get: ((snapshot, path) => Model.getSnapshotByPath(snapshot, path).pipe(Option.map((snapshot) => snapshot.value))),
408
490
  getWithParents: ((snapshot, path) => {
409
491
  const parents = {};
410
- return getSnapshotByPath(snapshot, path, parents).pipe(Option.map((snapshot) => ({ value: snapshot.value, parents })));
492
+ return Model.getSnapshotByPath(snapshot, path, parents).pipe(Option.map((snapshot) => ({ value: snapshot.value, parents })));
411
493
  }),
412
- getSnapshot: getSnapshotByPath,
413
- matches: (snapshot, path) => Option.isSome(getSnapshotByPath(snapshot, path))
414
- });
494
+ getSnapshot: Model.getSnapshotByPath,
495
+ matches: (snapshot, path) => Option.isSome(Model.getSnapshotByPath(snapshot, path))
496
+ }));
415
497
  /**
416
498
  * Creates a schema-first machine definition.
417
499
  *
@@ -423,11 +505,16 @@ export const defineStates = (states, ..._validation) => ({
423
505
  * `defineStates` or is passed inline. Call `handle` on the returned definition
424
506
  * to implement state behavior with ordinary TypeScript control flow.
425
507
  *
508
+ * Schemas in `events` define the public input protocol. Schemas in
509
+ * `internalEvents` are added to the complete handler protocol for invoke
510
+ * results, child emissions, and other machine-local deliveries. Their tags
511
+ * must be disjoint.
512
+ *
426
513
  * **Example** (Typed counter machine)
427
514
  *
428
515
  * ```ts
429
516
  * import { Schema } from "effect"
430
- * import { Machine } from "effect/unstable/machine"
517
+ * import { Machine } from "@typeonce/effect-machine"
431
518
  *
432
519
  * class Count extends Schema.TaggedClass<Count>("Count")("Count", {
433
520
  * value: Schema.Number
@@ -457,10 +544,11 @@ export const defineStates = (states, ..._validation) => ({
457
544
  * @category constructors
458
545
  * @since 4.0.0
459
546
  */
460
- export const make = (config, ..._validation) => {
547
+ export const make = ((config) => {
461
548
  const self = Object.create(Proto);
462
549
  self.states = config.states;
463
550
  self.events = config.events;
551
+ self.internalEvents = config.internalEvents ?? [];
464
552
  self.emits = config.emits ?? [];
465
553
  self.input = config.input;
466
554
  self.id = config.id;
@@ -469,8 +557,9 @@ export const make = (config, ..._validation) => {
469
557
  self.makeTargetBuilder = makeTargetBuilder(config.states, self.stateNodes);
470
558
  self.handlers = Object.create(null);
471
559
  self.handle = makeHandle(self);
560
+ Model.setProtocol(self);
472
561
  return self;
473
- };
562
+ });
474
563
  /**
475
564
  * Encodes a decoded machine snapshot into a normalized data representation.
476
565
  *
@@ -538,6 +627,8 @@ export const decodeSnapshot = Model.decodeSnapshot;
538
627
  * Invoked child processes run while their owning state is active and are
539
628
  * stopped before the state exits. An unrecovered child failure fails the owning
540
629
  * machine; recover inside the child Effect when failure should become an event.
630
+ * The `id` is a state-local lifecycle key, not a communication address. To
631
+ * send events to the invocation, pass a typed `childAddress` as `address`.
541
632
  * The `src` callback is intentionally independent from its parent state. When
542
633
  * construction depends on the typed state, lifecycle event, or runtime, use
543
634
  * the state config factory form `invoke: (context) => Machine.invoke(...)` and
@@ -547,7 +638,7 @@ export const decodeSnapshot = Model.decodeSnapshot;
547
638
  *
548
639
  * ```ts
549
640
  * import { Effect, Schema } from "effect"
550
- * import { Machine } from "effect/unstable/machine"
641
+ * import { Machine } from "@typeonce/effect-machine"
551
642
  *
552
643
  * class Loaded extends Schema.TaggedClass<Loaded>("Loaded")("Loaded", {
553
644
  * value: Schema.String
@@ -565,6 +656,64 @@ export const decodeSnapshot = Model.decodeSnapshot;
565
656
  * @since 4.0.0
566
657
  */
567
658
  export const invoke = (config) => ({ ...config, [InvokeTypeId]: undefined });
659
+ /**
660
+ * Invokes one Effect and maps its typed outcome into machine-local events.
661
+ *
662
+ * **Details**
663
+ *
664
+ * This is the high-level one-shot counterpart to `invoke`. Success and typed
665
+ * failure values are mapped independently, so callers do not need to recover
666
+ * an Effect into a common event union by hand. Defects and interruption remain
667
+ * failures of the owning machine.
668
+ *
669
+ * Declare mapped outcomes in `internalEvents` unless they are also legitimate
670
+ * public commands.
671
+ *
672
+ * @see {@link invoke} for arbitrary child process logic.
673
+ * @see {@link after} for a state-scoped delayed event.
674
+ * @category constructors
675
+ * @since 4.0.0
676
+ */
677
+ export const invokeEffect = (config) => ((config) => invoke({
678
+ id: config.id,
679
+ src: () => effect(config.onFailure === undefined
680
+ ? Effect.map(config.effect, config.onSuccess)
681
+ : Effect.matchEffect(config.effect, {
682
+ onFailure: (error) => Effect.succeed(config.onFailure(error)),
683
+ onSuccess: (value) => Effect.succeed(config.onSuccess(value))
684
+ }))
685
+ }))(config);
686
+ /**
687
+ * Creates a cancellable state-scoped delayed event.
688
+ *
689
+ * The timer starts when its owning state is entered and is interrupted when
690
+ * that state exits. The delayed value should normally be declared in
691
+ * `internalEvents`.
692
+ *
693
+ * @category constructors
694
+ * @since 4.0.0
695
+ */
696
+ export const after = (duration, event, options) => invoke({
697
+ id: options?.id ?? `Machine.after:${String(event._tag)}`,
698
+ src: () => effect(Effect.as(Effect.sleep(duration), event))
699
+ });
700
+ /**
701
+ * Constructs another tagged case from compatible source fields.
702
+ *
703
+ * The target must be one tagged struct or tagged class whose discriminator is
704
+ * supplied by its constructor. Union wrappers are rejected because their
705
+ * `make` operation cannot select a member after the source discriminator has
706
+ * been discarded. Missing or incompatible required target fields must be
707
+ * supplied by the patch, and the target schema's normal `make` validation
708
+ * remains authoritative at runtime.
709
+ *
710
+ * @category constructors
711
+ * @since 4.0.0
712
+ */
713
+ export const retag = (target, source, ...args) => {
714
+ const { _tag: _, ...fields } = source;
715
+ return target.make({ ...fields, ...(args[0] ?? {}) });
716
+ };
568
717
  /**
569
718
  * Creates an invoked child process from a complete statechart machine.
570
719
  *
@@ -594,7 +743,8 @@ export const invokeMachine = ((config) => {
594
743
  const machine = config.child.machine;
595
744
  return {
596
745
  id: config.child.id,
597
- addressable: true,
746
+ address: config.child.id,
747
+ descriptor: config.child,
598
748
  src: () => machine.input === undefined
599
749
  ? internalProcess.toProcessLogic(machine)
600
750
  : internalProcess.toProcessLogic(machine, config.input),
@@ -664,13 +814,14 @@ export const plan = internalPlanner.plan;
664
814
  * The action's error and service requirements are retained in the machine
665
815
  * type without becoming requirements of `plan` or `planInitial`. The managed
666
816
  * runtime executes staged actions sequentially before publishing the planned
667
- * state.
817
+ * state. Pass a second argument when the planning Effect should return that
818
+ * value after staging.
668
819
  *
669
820
  * **Example** (Typed staged action)
670
821
  *
671
822
  * ```ts
672
823
  * import { Context, Effect } from "effect"
673
- * import { Machine } from "effect/unstable/machine"
824
+ * import { Machine } from "@typeonce/effect-machine"
674
825
  *
675
826
  * class Audit extends Context.Service<Audit, {
676
827
  * readonly write: Effect.Effect<void, "AuditError">
@@ -685,7 +836,10 @@ export const plan = internalPlanner.plan;
685
836
  * @category combinators
686
837
  * @since 4.0.0
687
838
  */
688
- export const action = (effect) => internalPlanner.action(effect);
839
+ export const action = ((effect, ...next) => {
840
+ const staged = internalPlanner.action(effect);
841
+ return next.length === 0 ? staged : Effect.as(staged, next[0]);
842
+ });
689
843
  const processLocal = (operation) => Effect.die(new ProcessLocalErrorValue({ operation }));
690
844
  const standaloneProcessRuntime = internalRuntime.MachineRuntime.of({
691
845
  self: {
@@ -756,7 +910,7 @@ export const runtime = () => internalPlanner.runtime();
756
910
  *
757
911
  * ```ts
758
912
  * import { Effect, Schema } from "effect"
759
- * import { Machine } from "effect/unstable/machine"
913
+ * import { Machine } from "@typeonce/effect-machine"
760
914
  *
761
915
  * class LoadFailed extends Schema.TaggedClass<LoadFailed>("LoadFailed")("LoadFailed", {
762
916
  * reason: Schema.String
@@ -835,20 +989,29 @@ export const transition = (initial, transition) => logic({
835
989
  run: ({ receive, updateState }) => receive.pipe(Effect.flatMap((event) => updateState((state) => transition(state, event))), Effect.forever)
836
990
  });
837
991
  /**
838
- * Creates a typed parent-local child address or complete machine descriptor.
992
+ * Creates a typed descriptor for a complete child machine.
839
993
  *
840
- * **When to use**
994
+ * Descriptors identify a child by id and machine identity, so independently
995
+ * constructed descriptors for the same pair address the same invoked child.
996
+ *
997
+ * @category constructors
998
+ * @since 4.0.0
999
+ */
1000
+ export const child = (id, machine) => ({
1001
+ [ChildMachineTypeId]: ChildMachineTypeId,
1002
+ id,
1003
+ machine
1004
+ });
1005
+ /**
1006
+ * Creates a typed parent-local address for lower-level child process logic.
841
1007
  *
842
- * Use with a complete machine to create the descriptor shared by
843
- * `invokeMachine`, `sendTo`, and child lookup APIs. The one-argument form
844
- * creates an event-only address for lower-level process logic.
1008
+ * The default event protocol is `never`; provide an event type before using
1009
+ * the address with `spawn`, `invoke`, or `sendTo`.
845
1010
  *
846
1011
  * @category constructors
847
1012
  * @since 4.0.0
848
1013
  */
849
- export const child = ((id, machine) => machine === undefined
850
- ? id
851
- : { [ChildMachineTypeId]: ChildMachineTypeId, id, machine });
1014
+ export const childAddress = (id) => id;
852
1015
  /**
853
1016
  * Spawns a child process owned by the currently running machine.
854
1017
  *
@@ -876,14 +1039,14 @@ export const spawn = ((logic, options) => Effect.flatMap(internalRuntime.Machine
876
1039
  * @category runtime
877
1040
  * @since 4.0.0
878
1041
  */
879
- export const sendTo = ((child, event) => Effect.flatMap(internalRuntime.MachineRuntime, (runtime) => runtime.sendTo(typeof child === "string" ? child : child.id, event)));
1042
+ export const sendTo = ((child, event) => Effect.flatMap(internalRuntime.MachineRuntime, (runtime) => runtime.sendTo(child, event)));
880
1043
  /**
881
1044
  * Stops a named child process of the running machine.
882
1045
  *
883
1046
  * @category runtime
884
1047
  * @since 4.0.0
885
1048
  */
886
- export const stopChild = ((child) => Effect.flatMap(internalRuntime.MachineRuntime, (runtime) => runtime.stopChild(typeof child === "string" ? child : child.id)));
1049
+ export const stopChild = ((child) => Effect.flatMap(internalRuntime.MachineRuntime, (runtime) => runtime.stopChild(child)));
887
1050
  /**
888
1051
  * Returns a stream of terminal lifecycle outcomes for a running machine.
889
1052
  *