@typeonce/effect-machine 0.15.0 → 0.16.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 (58) hide show
  1. package/README.md +31 -3
  2. package/dist/Machine.d.ts +258 -88
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +33 -33
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/activities.d.ts +2 -0
  7. package/dist/internal/machine/activities.d.ts.map +1 -1
  8. package/dist/internal/machine/activities.js +4 -0
  9. package/dist/internal/machine/activities.js.map +1 -1
  10. package/dist/internal/machine/executionPlan.d.ts.map +1 -1
  11. package/dist/internal/machine/executionPlan.js +6 -1
  12. package/dist/internal/machine/executionPlan.js.map +1 -1
  13. package/dist/internal/machine/invocation.d.ts +1 -1
  14. package/dist/internal/machine/invocation.d.ts.map +1 -1
  15. package/dist/internal/machine/invocation.js +25 -3
  16. package/dist/internal/machine/invocation.js.map +1 -1
  17. package/dist/internal/machine/invocationEvent.d.ts +8 -0
  18. package/dist/internal/machine/invocationEvent.d.ts.map +1 -1
  19. package/dist/internal/machine/invocationEvent.js +8 -0
  20. package/dist/internal/machine/invocationEvent.js.map +1 -1
  21. package/dist/internal/machine/machine.d.ts.map +1 -1
  22. package/dist/internal/machine/machine.js +119 -48
  23. package/dist/internal/machine/machine.js.map +1 -1
  24. package/dist/internal/machine/planner.d.ts +7 -0
  25. package/dist/internal/machine/planner.d.ts.map +1 -1
  26. package/dist/internal/machine/planner.js +20 -11
  27. package/dist/internal/machine/planner.js.map +1 -1
  28. package/dist/internal/machine/runtime.d.ts +1 -0
  29. package/dist/internal/machine/runtime.d.ts.map +1 -1
  30. package/dist/internal/machine/runtime.js +25 -16
  31. package/dist/internal/machine/runtime.js.map +1 -1
  32. package/dist/internal/machine/topology.d.ts +11 -0
  33. package/dist/internal/machine/topology.d.ts.map +1 -1
  34. package/dist/internal/machine/topology.js +17 -6
  35. package/dist/internal/machine/topology.js.map +1 -1
  36. package/dist/internal/testing/machine/transitionCoverage.d.ts.map +1 -1
  37. package/dist/internal/testing/machine/transitionCoverage.js +6 -2
  38. package/dist/internal/testing/machine/transitionCoverage.js.map +1 -1
  39. package/dist/internal/testing/machine/verification.d.ts.map +1 -1
  40. package/dist/internal/testing/machine/verification.js +6 -0
  41. package/dist/internal/testing/machine/verification.js.map +1 -1
  42. package/dist/testing/MachineTest.d.ts +2 -1
  43. package/dist/testing/MachineTest.d.ts.map +1 -1
  44. package/dist/testing/MachineTest.js.map +1 -1
  45. package/docs/agent-guide.md +63 -37
  46. package/package.json +1 -1
  47. package/src/Machine.ts +592 -159
  48. package/src/internal/machine/activities.ts +7 -0
  49. package/src/internal/machine/executionPlan.ts +6 -1
  50. package/src/internal/machine/invocation.ts +39 -4
  51. package/src/internal/machine/invocationEvent.ts +16 -0
  52. package/src/internal/machine/machine.ts +160 -47
  53. package/src/internal/machine/planner.ts +17 -3
  54. package/src/internal/machine/runtime.ts +61 -25
  55. package/src/internal/machine/topology.ts +31 -2
  56. package/src/internal/testing/machine/transitionCoverage.ts +6 -2
  57. package/src/internal/testing/machine/verification.ts +11 -0
  58. package/src/testing/MachineTest.ts +2 -0
@@ -355,6 +355,9 @@ interface ProcessAddress<in Event> {
355
355
  source: Inspection.Subject | undefined,
356
356
  causedBy: Inspection.Causation | undefined
357
357
  ) => Effect.Effect<void, StoppedError>
358
+ readonly [acknowledgedSend]?: (
359
+ event: Event
360
+ ) => Effect.Effect<AcknowledgedDelivery<unknown>, unknown | StoppedError>
358
361
  }
359
362
 
360
363
  const isMachineTarget = (value: unknown): value is MachineTarget<unknown> =>
@@ -843,7 +846,10 @@ class OwnedChildRuntimeImpl implements OwnedChildRuntime {
843
846
  private readonly registry: ChildRegistry,
844
847
  private readonly self: ProcessAddress<any>,
845
848
  private readonly runtime: ProcessRuntime,
846
- private readonly services?: Context.Context<any>
849
+ private readonly services?: Context.Context<any>,
850
+ private readonly sendAcknowledged?: (
851
+ event: unknown
852
+ ) => Effect.Effect<AcknowledgedDelivery<unknown>, unknown | StoppedError>
847
853
  ) {}
848
854
 
849
855
  private has(key: string): boolean {
@@ -886,7 +892,15 @@ class OwnedChildRuntimeImpl implements OwnedChildRuntime {
886
892
  ...this.self,
887
893
  send: (event) => options.sendParent(isCurrent, event),
888
894
  sendInspected: (event, source, causedBy) =>
889
- isCurrent() ? sendMachineTarget(this.self, event, source, causedBy) : Effect.void
895
+ isCurrent() ? sendMachineTarget(this.self, event, source, causedBy) : Effect.void,
896
+ ...(this.sendAcknowledged === undefined
897
+ ? undefined
898
+ : {
899
+ [acknowledgedSend]: (event: unknown) =>
900
+ isCurrent()
901
+ ? this.sendAcknowledged!(event)
902
+ : Effect.interrupt
903
+ })
890
904
  }
891
905
  const startOptions: StartInternalOptions = {
892
906
  detached: true,
@@ -1048,7 +1062,10 @@ const childlessRuntime: ChildRuntime = {
1048
1062
  const makeChildRuntimeSync = (
1049
1063
  self: ProcessAddress<any>,
1050
1064
  runtime: ProcessRuntime,
1051
- services?: Context.Context<any>
1065
+ services?: Context.Context<any>,
1066
+ sendAcknowledged?: (
1067
+ event: unknown
1068
+ ) => Effect.Effect<AcknowledgedDelivery<unknown>, unknown | StoppedError>
1052
1069
  ): ChildRuntime => {
1053
1070
  // Child-registry decisions are synchronous and every access below runs in
1054
1071
  // one Effect.sync / Effect.suspend step. Keep the unobserved representation
@@ -1290,15 +1307,18 @@ const makeChildRuntimeSync = (
1290
1307
  changes,
1291
1308
  sendTo,
1292
1309
  stop,
1293
- owned: new OwnedChildRuntimeImpl(registry, self, runtime, services)
1310
+ owned: new OwnedChildRuntimeImpl(registry, self, runtime, services, sendAcknowledged)
1294
1311
  }
1295
1312
  }
1296
1313
 
1297
1314
  const makeChildRuntime = (
1298
1315
  self: ProcessAddress<any>,
1299
1316
  runtime: ProcessRuntime,
1300
- services?: Context.Context<any>
1301
- ): Effect.Effect<ChildRuntime> => Effect.sync(() => makeChildRuntimeSync(self, runtime, services))
1317
+ services?: Context.Context<any>,
1318
+ sendAcknowledged?: (
1319
+ event: unknown
1320
+ ) => Effect.Effect<AcknowledgedDelivery<unknown>, unknown | StoppedError>
1321
+ ): Effect.Effect<ChildRuntime> => Effect.sync(() => makeChildRuntimeSync(self, runtime, services, sendAcknowledged))
1302
1322
 
1303
1323
  // `Machine.logic` permits an arbitrary Effect program, including programs that
1304
1324
  // suspend or supervise their own fibers. Keep its two-fiber worker/supervisor
@@ -1376,6 +1396,23 @@ const startGenericInternal: <
1376
1396
  Effect.tap(() => Effect.sync(() => publishInspectedSent(inspector, subject!, message)))
1377
1397
  )
1378
1398
  })
1399
+ const sendAcknowledged:
1400
+ | ((event: Event) => Effect.Effect<AcknowledgedDelivery<State>, Error | StoppedError>)
1401
+ | undefined = logic.execution?._tag !== "Compiled"
1402
+ ? undefined
1403
+ : (event) =>
1404
+ Effect.uninterruptibleMask((restore) =>
1405
+ Deferred.make<AcknowledgedDelivery<unknown>, unknown>().pipe(
1406
+ Effect.flatMap((deferred) => {
1407
+ const offered = inspector === undefined
1408
+ ? offerDirect({ [AcknowledgedMessageTypeId]: true as const, event, deferred })
1409
+ : offerInspected!(event, undefined, undefined, deferred)
1410
+ return offered.pipe(Effect.andThen(restore(Deferred.await(deferred))))
1411
+ }),
1412
+ Effect.map((delivery) => delivery as AcknowledgedDelivery<State>)
1413
+ )
1414
+ ) as Effect.Effect<AcknowledgedDelivery<State>, Error | StoppedError>
1415
+
1379
1416
  const self: ProcessAddress<Event> = {
1380
1417
  id,
1381
1418
  sessionId,
@@ -1396,22 +1433,6 @@ const startGenericInternal: <
1396
1433
  ? undefined
1397
1434
  : { inspectionSubject: subject!, sendInspected: offerInspected! })
1398
1435
  }
1399
- const sendAcknowledged:
1400
- | ((event: Event) => Effect.Effect<AcknowledgedDelivery<State>, Error | StoppedError>)
1401
- | undefined = logic.execution?._tag !== "Compiled"
1402
- ? undefined
1403
- : (event) =>
1404
- Effect.uninterruptibleMask((restore) =>
1405
- Deferred.make<AcknowledgedDelivery<unknown>, unknown>().pipe(
1406
- Effect.flatMap((deferred) => {
1407
- const offered = inspector === undefined
1408
- ? offerDirect({ [AcknowledgedMessageTypeId]: true as const, event, deferred })
1409
- : offerInspected!(event, undefined, undefined, deferred)
1410
- return offered.pipe(Effect.andThen(restore(Deferred.await(deferred))))
1411
- }),
1412
- Effect.map((delivery) => delivery as AcknowledgedDelivery<State>)
1413
- )
1414
- ) as Effect.Effect<AcknowledgedDelivery<State>, Error | StoppedError>
1415
1436
 
1416
1437
  let {
1417
1438
  changes: childChanges,
@@ -1431,7 +1452,12 @@ const startGenericInternal: <
1431
1452
  sendTo,
1432
1453
  spawn,
1433
1454
  stop: stopChild
1434
- } = yield* makeChildRuntime(self, runtime))
1455
+ } = yield* makeChildRuntime(
1456
+ self,
1457
+ runtime,
1458
+ undefined,
1459
+ sendAcknowledged === undefined ? undefined : (event) => sendAcknowledged(event as Event)
1460
+ ))
1435
1461
  }
1436
1462
  const cleanupStartupFailure = <A, E>(exit: Exit.Exit<A, E>): Effect.Effect<void> => {
1437
1463
  if (Exit.isSuccess(exit)) return Effect.void
@@ -2227,7 +2253,12 @@ class CompiledProcess implements MachineRef<any, any, any, any> {
2227
2253
  initializeCompiledSync(): Effect.Effect<MachineRef<any, any, any, any>, unknown> {
2228
2254
  this.publishCreated()
2229
2255
  if (!this.execution.childless) {
2230
- this.childRuntime = makeChildRuntimeSync(this.address, this.options.runtime, this.services)
2256
+ this.childRuntime = makeChildRuntimeSync(
2257
+ this.address,
2258
+ this.options.runtime,
2259
+ this.services,
2260
+ (event) => this.sendAcknowledgedEffect(event)
2261
+ )
2231
2262
  }
2232
2263
  const parent = this.options.parent
2233
2264
  const sendParent = this.options.sendParent ?? (parent === undefined
@@ -2304,7 +2335,12 @@ class CompiledProcess implements MachineRef<any, any, any, any> {
2304
2335
  return Effect.gen(function*() {
2305
2336
  self.publishCreated()
2306
2337
  if (!self.execution.childless) {
2307
- self.childRuntime = yield* makeChildRuntime(self.address, self.options.runtime, self.services)
2338
+ self.childRuntime = yield* makeChildRuntime(
2339
+ self.address,
2340
+ self.options.runtime,
2341
+ self.services,
2342
+ (event) => self.sendAcknowledgedEffect(event)
2343
+ )
2308
2344
  }
2309
2345
  const parent = self.options.parent
2310
2346
  const sendParent = self.options.sendParent ?? (parent === undefined
@@ -27,6 +27,8 @@ export const NoTargetTypeId: unique symbol = Symbol("effect/Machine/NoTarget")
27
27
 
28
28
  export const TargetSelectionTypeId: unique symbol = Symbol("effect/Machine/TargetSelection")
29
29
 
30
+ export const SelectedBranchTypeId: unique symbol = Symbol("effect/Machine/SelectedBranch")
31
+
30
32
  interface StateInput {
31
33
  readonly [StateInputTypeId]: typeof StateInputTypeId
32
34
  readonly input: unknown
@@ -76,6 +78,15 @@ export interface TargetSelection {
76
78
  readonly path: string | undefined
77
79
  }
78
80
 
81
+ /** One branch selection returned by a compiled branching transition. */
82
+ export interface SelectedBranch {
83
+ readonly [SelectedBranchTypeId]: typeof SelectedBranchTypeId
84
+ readonly owner: object
85
+ readonly branchIndex: number
86
+ readonly branchKey: string
87
+ readonly result: unknown
88
+ }
89
+
79
90
  export const makeTargetSelection = (
80
91
  kind: TargetSelectionKind,
81
92
  path?: string,
@@ -90,6 +101,22 @@ export const makeTargetSelection = (
90
101
 
91
102
  export const isTargetSelection = (u: unknown): u is TargetSelection => hasProperty(u, TargetSelectionTypeId)
92
103
 
104
+ export const makeSelectedBranch = (
105
+ owner: object,
106
+ branchIndex: number,
107
+ branchKey: string,
108
+ result: unknown
109
+ ): SelectedBranch =>
110
+ Object.freeze({
111
+ [SelectedBranchTypeId]: SelectedBranchTypeId,
112
+ owner,
113
+ branchIndex,
114
+ branchKey,
115
+ result
116
+ })
117
+
118
+ export const isSelectedBranch = (u: unknown): u is SelectedBranch => hasProperty(u, SelectedBranchTypeId)
119
+
93
120
  const noTarget = Object.freeze({
94
121
  [NoTargetTypeId]: NoTargetTypeId
95
122
  }) as NoTarget
@@ -467,8 +494,10 @@ export const transitionDefinitions = (
467
494
  : [config.invoke]
468
495
  for (const invoke of invokes) {
469
496
  const id = "child" in invoke ? String(invoke.child.id) : String(invoke.id)
470
- for (const outcome of ["done", "failure", "snapshot"] as const) {
471
- const handler = outcome === "done"
497
+ for (const outcome of ["element", "done", "failure", "snapshot"] as const) {
498
+ const handler = outcome === "element"
499
+ ? invoke.onElement
500
+ : outcome === "done"
472
501
  ? invoke.onDone
473
502
  : outcome === "failure"
474
503
  ? invoke.onFailure
@@ -76,10 +76,12 @@ export const makeTransitionCoverageCollector = <M extends AnyMachine>(
76
76
  branchOffsets.push(branches.length)
77
77
  const definition = definitions[definitionIndex]!
78
78
  definition.branches.forEach((branch, branchIndex) => {
79
+ const branchKey = branch.type === "branch" ? branch.key : undefined
79
80
  branches.push({
80
- id: `transition:${definitionIndex}:branch:${branchIndex}`,
81
+ id: `transition:${definitionIndex}:branch:${branchKey ?? "direct"}`,
81
82
  definitionIndex,
82
83
  branchIndex,
84
+ branchKey,
83
85
  source: definition.source,
84
86
  trigger: definition.trigger,
85
87
  reenter: definition.reenter,
@@ -100,9 +102,11 @@ export const makeTransitionCoverageCollector = <M extends AnyMachine>(
100
102
  if (definitionIndex === -1) continue
101
103
  definitionHits.add(definitionIndex)
102
104
  const definition = definitions[definitionIndex]!
105
+ const branch = definition.branches[retained.branchIndex]
103
106
  if (
104
107
  Number.isSafeInteger(retained.branchIndex) && retained.branchIndex >= 0 &&
105
- retained.branchIndex < definition.branches.length
108
+ retained.branchIndex < definition.branches.length &&
109
+ retained.branchKey === (branch?.type === "branch" ? branch.key : undefined)
106
110
  ) {
107
111
  branchHits.add(branchOffsets[definitionIndex]! + retained.branchIndex)
108
112
  }
@@ -1512,6 +1512,17 @@ export const verify = <M extends AnyMachine>(
1512
1512
  return undefined
1513
1513
  }
1514
1514
  const branch = definition.branches[transition.branchIndex]!
1515
+ const expectedBranchKey = branch.type === "branch" ? branch.key : undefined
1516
+ if (transition.branchKey !== expectedBranchKey) {
1517
+ add(
1518
+ "definitions.branchKey",
1519
+ location,
1520
+ `retained transition from "${transition.source}" selected branch key ` +
1521
+ `"${String(transition.branchKey)}" instead of "${String(expectedBranchKey)}"`,
1522
+ transition.source
1523
+ )
1524
+ return undefined
1525
+ }
1515
1526
  if (!targetWithinSelection(transition.target, branch, byPath)) {
1516
1527
  const expected = branch.selection.kind === "none"
1517
1528
  ? "an explicitly targetless result"
@@ -1721,6 +1721,7 @@ export interface TransitionBranchCoverageItem<
1721
1721
  readonly id: string
1722
1722
  readonly definitionIndex: number
1723
1723
  readonly branchIndex: number
1724
+ readonly branchKey: string | undefined
1724
1725
  readonly source: SourcePath
1725
1726
  readonly trigger: Machine.Machine.TransitionTrigger<EventTag>
1726
1727
  readonly reenter: boolean
@@ -2063,6 +2064,7 @@ export type VerificationLaw =
2063
2064
  | "definitions.initial"
2064
2065
  | "definitions.transition"
2065
2066
  | "definitions.branchIndex"
2067
+ | "definitions.branchKey"
2066
2068
  | "definitions.selection"
2067
2069
  | "definitions.resolution"
2068
2070