@typeonce/effect-machine 0.7.0 → 0.9.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 (77) hide show
  1. package/README.md +85 -24
  2. package/dist/Machine.d.ts +545 -444
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +69 -172
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/activities.d.ts +5 -12
  7. package/dist/internal/machine/activities.d.ts.map +1 -1
  8. package/dist/internal/machine/activities.js +47 -17
  9. package/dist/internal/machine/activities.js.map +1 -1
  10. package/dist/internal/machine/atom.d.ts +10 -4
  11. package/dist/internal/machine/atom.d.ts.map +1 -1
  12. package/dist/internal/machine/atom.js.map +1 -1
  13. package/dist/internal/machine/configuration.d.ts.map +1 -1
  14. package/dist/internal/machine/configuration.js +103 -20
  15. package/dist/internal/machine/configuration.js.map +1 -1
  16. package/dist/internal/machine/executionPlan.d.ts.map +1 -1
  17. package/dist/internal/machine/executionPlan.js +31 -0
  18. package/dist/internal/machine/executionPlan.js.map +1 -1
  19. package/dist/internal/machine/invocation.d.ts +10 -2
  20. package/dist/internal/machine/invocation.d.ts.map +1 -1
  21. package/dist/internal/machine/invocation.js +93 -34
  22. package/dist/internal/machine/invocation.js.map +1 -1
  23. package/dist/internal/machine/invocationEvent.d.ts +39 -0
  24. package/dist/internal/machine/invocationEvent.d.ts.map +1 -0
  25. package/dist/internal/machine/invocationEvent.js +43 -0
  26. package/dist/internal/machine/invocationEvent.js.map +1 -0
  27. package/dist/internal/machine/machine.d.ts +6 -50
  28. package/dist/internal/machine/machine.d.ts.map +1 -1
  29. package/dist/internal/machine/machine.js +30 -81
  30. package/dist/internal/machine/machine.js.map +1 -1
  31. package/dist/internal/machine/planner.d.ts +12 -1
  32. package/dist/internal/machine/planner.d.ts.map +1 -1
  33. package/dist/internal/machine/planner.js +90 -37
  34. package/dist/internal/machine/planner.js.map +1 -1
  35. package/dist/internal/machine/protocol.d.ts +9 -0
  36. package/dist/internal/machine/protocol.d.ts.map +1 -1
  37. package/dist/internal/machine/protocol.js +114 -0
  38. package/dist/internal/machine/protocol.js.map +1 -1
  39. package/dist/internal/machine/serialization.d.ts.map +1 -1
  40. package/dist/internal/machine/serialization.js +53 -21
  41. package/dist/internal/machine/serialization.js.map +1 -1
  42. package/dist/internal/machine/stateDefinition.d.ts +4 -4
  43. package/dist/internal/machine/stateDefinition.d.ts.map +1 -1
  44. package/dist/internal/machine/stateDefinition.js +18 -11
  45. package/dist/internal/machine/stateDefinition.js.map +1 -1
  46. package/dist/internal/machine/symbols.d.ts +2 -0
  47. package/dist/internal/machine/symbols.d.ts.map +1 -1
  48. package/dist/internal/machine/symbols.js +2 -0
  49. package/dist/internal/machine/symbols.js.map +1 -1
  50. package/dist/internal/machine/topology.d.ts +5 -5
  51. package/dist/internal/machine/topology.d.ts.map +1 -1
  52. package/dist/internal/machine/topology.js +39 -13
  53. package/dist/internal/machine/topology.js.map +1 -1
  54. package/dist/internal/testing/machine/verification.d.ts.map +1 -1
  55. package/dist/internal/testing/machine/verification.js +10 -3
  56. package/dist/internal/testing/machine/verification.js.map +1 -1
  57. package/dist/unstable/reactivity/AtomMachine.d.ts +11 -5
  58. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
  59. package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
  60. package/docs/agent-guide.md +174 -121
  61. package/package.json +3 -3
  62. package/src/Machine.ts +1336 -872
  63. package/src/internal/machine/activities.ts +48 -33
  64. package/src/internal/machine/atom.ts +13 -6
  65. package/src/internal/machine/configuration.ts +102 -23
  66. package/src/internal/machine/executionPlan.ts +35 -0
  67. package/src/internal/machine/invocation.ts +180 -49
  68. package/src/internal/machine/invocationEvent.ts +72 -0
  69. package/src/internal/machine/machine.ts +105 -403
  70. package/src/internal/machine/planner.ts +110 -48
  71. package/src/internal/machine/protocol.ts +149 -0
  72. package/src/internal/machine/serialization.ts +56 -31
  73. package/src/internal/machine/stateDefinition.ts +22 -11
  74. package/src/internal/machine/symbols.ts +3 -0
  75. package/src/internal/machine/topology.ts +44 -18
  76. package/src/internal/testing/machine/verification.ts +13 -3
  77. package/src/unstable/reactivity/AtomMachine.ts +12 -5
@@ -4,28 +4,26 @@
4
4
  * @since 0.4.0
5
5
  */
6
6
 
7
+ import * as Cause from "effect/Cause"
7
8
  import * as Effect from "effect/Effect"
8
- import type { Machine } from "../../Machine.js"
9
+ import type { ChildMachine, Logic, Machine } from "../../Machine.js"
9
10
  import * as Configuration from "./configuration.js"
11
+ import { InfiniteTransitionError, MachineSchemaDecodeError, StoppedError } from "./errors.js"
12
+ import * as InvocationEvent from "./invocationEvent.js"
10
13
  import * as Planner from "./planner.js"
11
14
  import type * as Runtime from "./runtime.js"
15
+ import { ChildMachineLogicTypeId } from "./symbols.js"
12
16
 
13
17
  /** @internal */
14
- export type AnyConfig = Machine.InvokeConfig<
15
- any,
16
- any,
17
- any,
18
- any,
19
- any,
20
- any,
21
- any,
22
- any,
23
- any,
24
- any,
25
- any,
26
- any,
27
- any
28
- >
18
+ export interface AnyConfig {
19
+ readonly id: string
20
+ readonly address?: string
21
+ readonly descriptor?: ChildMachine.Any
22
+ readonly src: () => Runtime.ProcessLogic<any, any, any, any, any, any>
23
+ readonly onDone?: unknown
24
+ readonly onFailure?: unknown
25
+ readonly onSnapshot?: unknown
26
+ }
29
27
 
30
28
  /** @internal */
31
29
  export const makeKey = (path: string, id: string): string => `${path.length}:${path}${id}`
@@ -33,16 +31,79 @@ export const makeKey = (path: string, id: string): string => `${path.length}:${p
33
31
  /** @internal */
34
32
  export const makeChildId = (path: string, id: string): string => `Machine.invoke:${makeKey(path, id)}`
35
33
 
36
- const resolve = (
37
- config: Machine.AnyStateConfig | undefined,
34
+ const oneShot = (effect: Effect.Effect<any, any, any>): Logic<void, never, any, any, any> => ({
35
+ initial: () => Effect.void,
36
+ run: () => effect
37
+ })
38
+
39
+ const resolveValue = (value: unknown, context: Machine.InvokeContext<any, any, any, any>): unknown =>
40
+ typeof value === "function" ? value(context) : value
41
+
42
+ const resolveOne = (
43
+ raw: Record<PropertyKey, any>,
38
44
  context: Machine.InvokeContext<any, any, any, any>
39
- ): ReadonlyArray<AnyConfig> => {
40
- const definition = config?.invoke
41
- const invokes = typeof definition === "function" ? definition(context) : definition
42
- if (invokes === undefined) return []
43
- return Array.isArray(invokes) ? invokes as ReadonlyArray<AnyConfig> : [invokes as AnyConfig]
45
+ ): AnyConfig => {
46
+ if ("effect" in raw) {
47
+ return {
48
+ id: String(raw.id),
49
+ src: () =>
50
+ oneShot(resolveValue(raw.effect, context) as Effect.Effect<any, any, any>) as unknown as Runtime.ProcessLogic<
51
+ any,
52
+ any,
53
+ any,
54
+ any,
55
+ any,
56
+ any
57
+ >,
58
+ onDone: raw.onDone,
59
+ onFailure: raw.onFailure,
60
+ onSnapshot: raw.onSnapshot
61
+ }
62
+ }
63
+ if ("after" in raw) {
64
+ return {
65
+ id: String(raw.id),
66
+ src: () =>
67
+ oneShot(Effect.sleep(resolveValue(raw.after, context) as any)) as unknown as Runtime.ProcessLogic<
68
+ any,
69
+ any,
70
+ any,
71
+ any,
72
+ any,
73
+ any
74
+ >,
75
+ onDone: raw.onDone
76
+ }
77
+ }
78
+ if ("logic" in raw) {
79
+ return {
80
+ id: String(raw.id),
81
+ address: String(raw.address),
82
+ src: () => resolveValue(raw.logic, context) as Runtime.ProcessLogic<any, any, any, any, any, any>,
83
+ onDone: raw.onDone,
84
+ onFailure: raw.onFailure,
85
+ onSnapshot: raw.onSnapshot
86
+ }
87
+ }
88
+ if ("child" in raw) {
89
+ const descriptor = raw.child as ChildMachine.Any
90
+ return {
91
+ id: descriptor.id,
92
+ address: descriptor.id,
93
+ descriptor,
94
+ src: () =>
95
+ descriptor[ChildMachineLogicTypeId](
96
+ "input" in raw ? resolveValue(raw.input, context) : undefined
97
+ ) as Runtime.ProcessLogic<any, any, any, any, any, any>,
98
+ onDone: raw.onDone,
99
+ onFailure: raw.onFailure,
100
+ onSnapshot: raw.onSnapshot
101
+ }
102
+ }
103
+ throw new Error("Machine invoke must define exactly one of effect, after, logic, or child")
44
104
  }
45
105
 
106
+ /** @internal */
46
107
  const runSequentialDiscard = <E, R>(
47
108
  effects: ReadonlyArray<Effect.Effect<void, E, R>>
48
109
  ): Effect.Effect<void, E, R> =>
@@ -52,45 +113,110 @@ const runSequentialDiscard = <E, R>(
52
113
  ? effects[0]!
53
114
  : Effect.all(effects, { discard: true })
54
115
 
55
- const start = (
116
+ const sendLifecycle = (
117
+ scope: Runtime.ProcessScope<any>,
118
+ event: InvocationEvent.InvocationEvent
119
+ ): Effect.Effect<void> => scope.self.send(event).pipe(Effect.catchTag("StoppedError", () => Effect.void))
120
+
121
+ const isFrameworkFailure = (error: unknown): boolean =>
122
+ error instanceof InfiniteTransitionError || error instanceof MachineSchemaDecodeError || error instanceof StoppedError
123
+
124
+ const startResolved = (
56
125
  scope: Runtime.ProcessScope<any>,
57
126
  ownedChildren: Runtime.OwnedChildRuntime,
58
127
  path: string,
59
- config: AnyConfig
128
+ invokeId: string,
129
+ childId: string,
130
+ descriptor: ChildMachine.Any | undefined,
131
+ src: () => Runtime.ProcessLogic<any, any, any, any, any, any>,
132
+ onDone: unknown,
133
+ onFailure: unknown,
134
+ onSnapshot: unknown
60
135
  ): Effect.Effect<void, any, any> =>
61
136
  Effect.suspend(() => {
62
- const invokeId = String(config.id)
63
137
  const key = makeKey(path, invokeId)
64
- const childId = config.address === undefined ? makeChildId(path, invokeId) : String(config.address)
65
- return ownedChildren.spawn(config.src as () => Runtime.ProcessLogic<any, any, any, any, any, any>, {
138
+ return ownedChildren.spawn(src, {
66
139
  key,
67
140
  path,
68
141
  id: childId,
69
142
  duplicateId: invokeId,
70
- ...(config.descriptor === undefined ? undefined : { descriptor: config.descriptor }),
143
+ ...(descriptor === undefined ? undefined : { descriptor }),
71
144
  sendParent: (isCurrent, event) => isCurrent() ? scope.self.send(event) : Effect.void,
72
145
  onOutcome: (isCurrent, outcome) => {
73
146
  if (outcome._tag === "Stopped" || !isCurrent()) return Effect.void
74
- if (outcome._tag !== "Done") return scope.failCause(outcome.cause)
75
- const mappedEvent = config.onDone === undefined
76
- ? outcome.output
77
- : config.onDone({ id: config.id, output: outcome.output })
78
- return mappedEvent === undefined
79
- ? Effect.void
80
- : scope.self.send(mappedEvent).pipe(Effect.catchTag("StoppedError", () => Effect.void))
81
- },
82
- ...(config.snapshot === undefined ? undefined : {
83
- onSnapshot: (isCurrent: () => boolean, snapshot: any) => {
84
- if (!isCurrent()) return Effect.void
85
- const mappedEvent = config.snapshot!({ id: config.id, snapshot })
86
- return mappedEvent === undefined
87
- ? Effect.void
88
- : scope.self.send(mappedEvent).pipe(Effect.catchTag("StoppedError", () => Effect.void))
147
+ if (outcome._tag === "Done") {
148
+ if (onDone === undefined) {
149
+ return scope.failCause(Cause.die(
150
+ new Error(
151
+ `Invocation "${invokeId}" completed without the required onDone handler`
152
+ )
153
+ ))
154
+ }
155
+ return sendLifecycle(scope, InvocationEvent.done(path, invokeId, outcome.output))
89
156
  }
157
+ if (
158
+ outcome._tag === "Failure" &&
159
+ onFailure !== undefined &&
160
+ (descriptor === undefined || !isFrameworkFailure(outcome.error))
161
+ ) {
162
+ return sendLifecycle(scope, InvocationEvent.failure(path, invokeId, outcome.error))
163
+ }
164
+ return scope.failCause(outcome.cause)
165
+ },
166
+ ...(onSnapshot === undefined ? undefined : {
167
+ onSnapshot: (isCurrent: () => boolean, snapshot: any) =>
168
+ isCurrent()
169
+ ? sendLifecycle(scope, InvocationEvent.snapshot(path, invokeId, snapshot))
170
+ : Effect.void
90
171
  })
91
172
  })
92
173
  })
93
174
 
175
+ const start = (
176
+ scope: Runtime.ProcessScope<any>,
177
+ ownedChildren: Runtime.OwnedChildRuntime,
178
+ path: string,
179
+ config: AnyConfig
180
+ ): Effect.Effect<void, any, any> => {
181
+ const invokeId = String(config.id)
182
+ return startResolved(
183
+ scope,
184
+ ownedChildren,
185
+ path,
186
+ invokeId,
187
+ config.address === undefined ? makeChildId(path, invokeId) : String(config.address),
188
+ config.descriptor,
189
+ config.src,
190
+ config.onDone,
191
+ config.onFailure,
192
+ config.onSnapshot
193
+ )
194
+ }
195
+
196
+ const startStaticChild = (
197
+ scope: Runtime.ProcessScope<any>,
198
+ ownedChildren: Runtime.OwnedChildRuntime,
199
+ path: string,
200
+ raw: Record<PropertyKey, any>
201
+ ): Effect.Effect<void, any, any> => {
202
+ // A zero-input child has no entry-context dependency. Reuse the descriptor's
203
+ // source function so each running parent does not retain a resolved config
204
+ // object and an otherwise redundant source closure.
205
+ const descriptor = raw.child as ChildMachine.Any
206
+ return startResolved(
207
+ scope,
208
+ ownedChildren,
209
+ path,
210
+ descriptor.id,
211
+ descriptor.id,
212
+ descriptor,
213
+ descriptor[ChildMachineLogicTypeId],
214
+ raw.onDone,
215
+ raw.onFailure,
216
+ raw.onSnapshot
217
+ )
218
+ }
219
+
94
220
  /**
95
221
  * Starts every invocation owned by active entry paths in deterministic entry
96
222
  * order. `undefined` keeps the compiled drain free of empty Effect nodes.
@@ -107,13 +233,18 @@ export const startAll = (
107
233
  ): Effect.Effect<void, any, any> | undefined => {
108
234
  const effects = Planner.sortEntryPaths(machine, paths)
109
235
  .filter((path) => configuration.active.has(path))
110
- .flatMap((path) =>
111
- resolve(Configuration.getStateConfigByPath(machine, path), {
112
- state: Configuration.getActiveValue(configuration, path),
236
+ .flatMap((path) => {
237
+ const context = {
238
+ state: configuration.values.get(path),
113
239
  parent: Configuration.getParentValue(machine, configuration, path),
114
240
  parents: Configuration.getParentValues(machine, configuration, path),
115
241
  event
116
- }).map((config) => start(scope, ownedChildren, path, config))
117
- )
242
+ }
243
+ return InvocationEvent.definitions(Configuration.getStateConfigByPath(machine, path)?.invoke).map((definition) =>
244
+ "child" in definition && !("input" in definition)
245
+ ? startStaticChild(scope, ownedChildren, path, definition)
246
+ : start(scope, ownedChildren, path, resolveOne(definition, context))
247
+ )
248
+ })
118
249
  return effects.length === 0 ? undefined : runSequentialDiscard(effects)
119
250
  }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Private mailbox messages used to route invocation lifecycle changes through
3
+ * the owning machine planner.
4
+ *
5
+ * @since 0.9.0
6
+ */
7
+
8
+ /** @internal */
9
+ export const InvocationEventTypeId: unique symbol = Symbol("effect/Machine/InvocationEvent")
10
+
11
+ /** @internal */
12
+ export type InvocationEvent =
13
+ | {
14
+ readonly [InvocationEventTypeId]: true
15
+ readonly path: string
16
+ readonly id: string
17
+ readonly type: "done"
18
+ readonly output: unknown
19
+ }
20
+ | {
21
+ readonly [InvocationEventTypeId]: true
22
+ readonly path: string
23
+ readonly id: string
24
+ readonly type: "failure"
25
+ readonly error: unknown
26
+ }
27
+ | {
28
+ readonly [InvocationEventTypeId]: true
29
+ readonly path: string
30
+ readonly id: string
31
+ readonly type: "snapshot"
32
+ readonly snapshot: unknown
33
+ }
34
+
35
+ /** @internal */
36
+ export const done = (path: string, id: string, output: unknown): InvocationEvent => ({
37
+ [InvocationEventTypeId]: true,
38
+ path,
39
+ id,
40
+ type: "done",
41
+ output
42
+ })
43
+
44
+ /** @internal */
45
+ export const failure = (path: string, id: string, error: unknown): InvocationEvent => ({
46
+ [InvocationEventTypeId]: true,
47
+ path,
48
+ id,
49
+ type: "failure",
50
+ error
51
+ })
52
+
53
+ /** @internal */
54
+ export const snapshot = (path: string, id: string, value: unknown): InvocationEvent => ({
55
+ [InvocationEventTypeId]: true,
56
+ path,
57
+ id,
58
+ type: "snapshot",
59
+ snapshot: value
60
+ })
61
+
62
+ /** @internal */
63
+ export const isInvocationEvent = (value: unknown): value is InvocationEvent =>
64
+ typeof value === "object" && value !== null && InvocationEventTypeId in value
65
+
66
+ /** @internal */
67
+ export const definitions = (invoke: unknown): ReadonlyArray<Record<PropertyKey, any>> => {
68
+ if (invoke === undefined) return []
69
+ return Array.isArray(invoke)
70
+ ? invoke as ReadonlyArray<Record<PropertyKey, any>>
71
+ : [invoke as Record<PropertyKey, any>]
72
+ }