@typeonce/effect-machine 0.3.0 → 0.4.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 (149) hide show
  1. package/README.md +203 -16
  2. package/dist/Machine.d.ts +784 -289
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +158 -528
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/activities.d.ts +57 -0
  7. package/dist/internal/machine/activities.d.ts.map +1 -0
  8. package/dist/internal/machine/activities.js +49 -0
  9. package/dist/internal/machine/activities.js.map +1 -0
  10. package/dist/internal/machine/atom.d.ts +69 -0
  11. package/dist/internal/machine/atom.d.ts.map +1 -0
  12. package/dist/{AtomMachine.js → internal/machine/atom.js} +19 -94
  13. package/dist/internal/machine/atom.js.map +1 -0
  14. package/dist/internal/machine/cluster.d.ts +63 -0
  15. package/dist/internal/machine/cluster.d.ts.map +1 -0
  16. package/dist/{ClusterMachine.js → internal/machine/cluster.js} +14 -118
  17. package/dist/internal/machine/cluster.js.map +1 -0
  18. package/dist/internal/machine/command.d.ts +16 -0
  19. package/dist/internal/machine/command.d.ts.map +1 -0
  20. package/dist/internal/machine/command.js +45 -0
  21. package/dist/internal/machine/command.js.map +1 -0
  22. package/dist/internal/machine/commandRuntime.d.ts +13 -0
  23. package/dist/internal/machine/commandRuntime.d.ts.map +1 -0
  24. package/dist/internal/machine/commandRuntime.js +16 -0
  25. package/dist/internal/machine/commandRuntime.js.map +1 -0
  26. package/dist/internal/{machineModel.d.ts → machine/configuration.d.ts} +11 -60
  27. package/dist/internal/machine/configuration.d.ts.map +1 -0
  28. package/dist/internal/{machineModel.js → machine/configuration.js} +249 -544
  29. package/dist/internal/machine/configuration.js.map +1 -0
  30. package/dist/internal/{machineErrors.d.ts → machine/errors.d.ts} +1 -1
  31. package/dist/internal/machine/errors.d.ts.map +1 -0
  32. package/dist/internal/{machineErrors.js → machine/errors.js} +1 -1
  33. package/dist/internal/machine/errors.js.map +1 -0
  34. package/dist/internal/machine/executionPlan.d.ts +49 -0
  35. package/dist/internal/machine/executionPlan.d.ts.map +1 -0
  36. package/dist/internal/machine/executionPlan.js +667 -0
  37. package/dist/internal/machine/executionPlan.js.map +1 -0
  38. package/dist/internal/machine/invocation.d.ts +23 -0
  39. package/dist/internal/machine/invocation.d.ts.map +1 -0
  40. package/dist/internal/machine/invocation.js +77 -0
  41. package/dist/internal/machine/invocation.js.map +1 -0
  42. package/dist/internal/machine/machine.d.ts +183 -0
  43. package/dist/internal/machine/machine.d.ts.map +1 -0
  44. package/dist/internal/machine/machine.js +552 -0
  45. package/dist/internal/machine/machine.js.map +1 -0
  46. package/dist/internal/machine/planner.d.ts +182 -0
  47. package/dist/internal/machine/planner.d.ts.map +1 -0
  48. package/dist/internal/machine/planner.js +1082 -0
  49. package/dist/internal/machine/planner.js.map +1 -0
  50. package/dist/internal/{machineProcess.d.ts → machine/process.d.ts} +10 -4
  51. package/dist/internal/machine/process.d.ts.map +1 -0
  52. package/dist/internal/machine/process.js +446 -0
  53. package/dist/internal/machine/process.js.map +1 -0
  54. package/dist/internal/machine/protocol.d.ts +34 -0
  55. package/dist/internal/machine/protocol.d.ts.map +1 -0
  56. package/dist/internal/machine/protocol.js +182 -0
  57. package/dist/internal/machine/protocol.js.map +1 -0
  58. package/dist/internal/machine/readiness.d.ts +4 -0
  59. package/dist/internal/machine/readiness.d.ts.map +1 -0
  60. package/dist/internal/machine/readiness.js +2 -0
  61. package/dist/internal/machine/readiness.js.map +1 -0
  62. package/dist/internal/{machineRuntime.d.ts → machine/runtime.d.ts} +104 -2
  63. package/dist/internal/machine/runtime.d.ts.map +1 -0
  64. package/dist/internal/machine/runtime.js +1381 -0
  65. package/dist/internal/machine/runtime.js.map +1 -0
  66. package/dist/internal/machine/serialization.d.ts +14 -0
  67. package/dist/internal/machine/serialization.d.ts.map +1 -0
  68. package/dist/internal/machine/serialization.js +338 -0
  69. package/dist/internal/machine/serialization.js.map +1 -0
  70. package/dist/internal/machine/stateDefinition.d.ts +15 -0
  71. package/dist/internal/machine/stateDefinition.d.ts.map +1 -0
  72. package/dist/internal/machine/stateDefinition.js +210 -0
  73. package/dist/internal/machine/stateDefinition.js.map +1 -0
  74. package/dist/internal/machine/symbols.d.ts +3 -0
  75. package/dist/internal/machine/symbols.d.ts.map +1 -0
  76. package/dist/internal/machine/symbols.js +3 -0
  77. package/dist/internal/machine/symbols.js.map +1 -0
  78. package/dist/internal/machine/topology.d.ts +98 -0
  79. package/dist/internal/machine/topology.d.ts.map +1 -0
  80. package/dist/internal/machine/topology.js +347 -0
  81. package/dist/internal/machine/topology.js.map +1 -0
  82. package/dist/internal/testing/machine/arbitrary.d.ts +36 -0
  83. package/dist/internal/testing/machine/arbitrary.d.ts.map +1 -0
  84. package/dist/internal/testing/machine/arbitrary.js +68 -0
  85. package/dist/internal/testing/machine/arbitrary.js.map +1 -0
  86. package/dist/internal/testing/machine/finiteModel.d.ts +322 -0
  87. package/dist/internal/testing/machine/finiteModel.d.ts.map +1 -0
  88. package/dist/internal/testing/machine/finiteModel.js +976 -0
  89. package/dist/internal/testing/machine/finiteModel.js.map +1 -0
  90. package/dist/internal/testing/machine/referenceModel.d.ts +203 -0
  91. package/dist/internal/testing/machine/referenceModel.d.ts.map +1 -0
  92. package/dist/internal/testing/machine/referenceModel.js +1012 -0
  93. package/dist/internal/testing/machine/referenceModel.js.map +1 -0
  94. package/dist/internal/testing/machine/runtime.d.ts +328 -0
  95. package/dist/internal/testing/machine/runtime.d.ts.map +1 -0
  96. package/dist/internal/testing/machine/runtime.js +437 -0
  97. package/dist/internal/testing/machine/runtime.js.map +1 -0
  98. package/dist/internal/testing/machine/verification.d.ts +33 -0
  99. package/dist/internal/testing/machine/verification.d.ts.map +1 -0
  100. package/dist/internal/testing/machine/verification.js +1446 -0
  101. package/dist/internal/testing/machine/verification.js.map +1 -0
  102. package/dist/testing/MachineTest.d.ts +606 -0
  103. package/dist/testing/MachineTest.d.ts.map +1 -0
  104. package/dist/testing/MachineTest.js +102 -0
  105. package/dist/testing/MachineTest.js.map +1 -0
  106. package/dist/testing/index.d.ts +7 -0
  107. package/dist/testing/index.d.ts.map +1 -0
  108. package/dist/testing/index.js +7 -0
  109. package/dist/testing/index.js.map +1 -0
  110. package/dist/{ClusterMachine.d.ts → unstable/cluster/ClusterMachine.d.ts} +19 -27
  111. package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -0
  112. package/dist/unstable/cluster/ClusterMachine.js +126 -0
  113. package/dist/unstable/cluster/ClusterMachine.js.map +1 -0
  114. package/dist/{cluster.js → unstable/cluster/index.d.ts} +1 -1
  115. package/dist/unstable/cluster/index.d.ts.map +1 -0
  116. package/dist/{cluster.d.ts → unstable/cluster/index.js} +1 -1
  117. package/dist/unstable/cluster/index.js.map +1 -0
  118. package/dist/{AtomMachine.d.ts → unstable/reactivity/AtomMachine.d.ts} +35 -20
  119. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -0
  120. package/dist/unstable/reactivity/AtomMachine.js +113 -0
  121. package/dist/unstable/reactivity/AtomMachine.js.map +1 -0
  122. package/dist/{reactivity.js → unstable/reactivity/index.d.ts} +1 -1
  123. package/dist/unstable/reactivity/index.d.ts.map +1 -0
  124. package/dist/{reactivity.d.ts → unstable/reactivity/index.js} +1 -1
  125. package/dist/unstable/reactivity/index.js.map +1 -0
  126. package/docs/agent-guide.md +169 -31
  127. package/package.json +19 -10
  128. package/dist/AtomMachine.d.ts.map +0 -1
  129. package/dist/AtomMachine.js.map +0 -1
  130. package/dist/ClusterMachine.d.ts.map +0 -1
  131. package/dist/ClusterMachine.js.map +0 -1
  132. package/dist/cluster.d.ts.map +0 -1
  133. package/dist/cluster.js.map +0 -1
  134. package/dist/internal/machineErrors.d.ts.map +0 -1
  135. package/dist/internal/machineErrors.js.map +0 -1
  136. package/dist/internal/machineModel.d.ts.map +0 -1
  137. package/dist/internal/machineModel.js.map +0 -1
  138. package/dist/internal/machinePlanner.d.ts +0 -74
  139. package/dist/internal/machinePlanner.d.ts.map +0 -1
  140. package/dist/internal/machinePlanner.js +0 -797
  141. package/dist/internal/machinePlanner.js.map +0 -1
  142. package/dist/internal/machineProcess.d.ts.map +0 -1
  143. package/dist/internal/machineProcess.js +0 -181
  144. package/dist/internal/machineProcess.js.map +0 -1
  145. package/dist/internal/machineRuntime.d.ts.map +0 -1
  146. package/dist/internal/machineRuntime.js +0 -365
  147. package/dist/internal/machineRuntime.js.map +0 -1
  148. package/dist/reactivity.d.ts.map +0 -1
  149. package/dist/reactivity.js.map +0 -1
@@ -0,0 +1,1381 @@
1
+ /**
2
+ * Internal machine process runtime helpers.
3
+ *
4
+ * @since 4.0.0
5
+ */
6
+ import * as Cause from "effect/Cause";
7
+ import * as Channel from "effect/Channel";
8
+ import * as Context from "effect/Context";
9
+ import * as Deferred from "effect/Deferred";
10
+ import * as Effect from "effect/Effect";
11
+ import * as Exit from "effect/Exit";
12
+ import * as Fiber from "effect/Fiber";
13
+ import * as Option from "effect/Option";
14
+ import * as PubSub from "effect/PubSub";
15
+ import * as Queue from "effect/Queue";
16
+ import * as Scope from "effect/Scope";
17
+ import * as Stream from "effect/Stream";
18
+ import * as SynchronizedRef from "effect/SynchronizedRef";
19
+ import { ChildAlreadyExistsError, StoppedError } from "./errors.js";
20
+ /** @internal */
21
+ export const activeSnapshotObserver = Symbol.for("effect/Machine/activeSnapshotObserver");
22
+ /** @internal */
23
+ export const sendParentOverride = Symbol.for("effect/Machine/sendParentOverride");
24
+ const offerChildObservation = (observer, value) => {
25
+ if (observer.values === undefined) {
26
+ observer.values = [value];
27
+ }
28
+ else {
29
+ observer.values.push(value);
30
+ }
31
+ if (observer.waiter !== undefined) {
32
+ const waiter = observer.waiter;
33
+ observer.waiter = undefined;
34
+ Deferred.doneUnsafe(waiter, Effect.void);
35
+ }
36
+ };
37
+ const takeChildObservations = (observer) => Effect.suspend(() => {
38
+ if (observer.values !== undefined) {
39
+ const values = observer.values;
40
+ observer.values = undefined;
41
+ return Effect.succeed(values);
42
+ }
43
+ const waiter = Deferred.makeUnsafe();
44
+ observer.waiter = waiter;
45
+ return Deferred.await(waiter).pipe(Effect.andThen(takeChildObservations(observer)));
46
+ });
47
+ const matchesChild = (entry, child) => entry._tag === "Started" && (typeof child === "string" || (entry.descriptor !== undefined &&
48
+ entry.descriptor.id === child.id &&
49
+ entry.descriptor.machine === child.machine));
50
+ const selectRegistryChild = (registry, id, child) => {
51
+ if (registry.closed)
52
+ return Option.none();
53
+ const entry = registry.children.get(id);
54
+ return entry !== undefined && matchesChild(entry, child) ? Option.some(entry.ref) : Option.none();
55
+ };
56
+ const publishRegistryChange = (registry) => {
57
+ if (registry.observers === undefined)
58
+ return;
59
+ for (const observer of registry.observers) {
60
+ offerChildObservation(observer, selectRegistryChild(registry, observer.id, observer.child));
61
+ }
62
+ };
63
+ const unregisterChild = (registry, key, token) => {
64
+ const entry = registry.children.get(key);
65
+ if (entry === undefined || entry.token !== token)
66
+ return;
67
+ registry.children.delete(key);
68
+ if (typeof key === "string")
69
+ publishRegistryChange(registry);
70
+ };
71
+ const registerChild = (registry, key, token, ref, descriptor) => {
72
+ const entry = registry.children.get(key);
73
+ if (registry.closed || entry === undefined || entry._tag !== "Starting" || entry.token !== token) {
74
+ return false;
75
+ }
76
+ registry.children.delete(key);
77
+ const started = entry.ownerKey === undefined
78
+ ? { _tag: "Started", token, descriptor, ref }
79
+ : {
80
+ _tag: "Started",
81
+ token,
82
+ descriptor,
83
+ ref,
84
+ ownerKey: entry.ownerKey,
85
+ ownerPath: entry.ownerPath,
86
+ ownerActive: entry.ownerActive === true
87
+ };
88
+ registry.children.set(key, started);
89
+ if (typeof key === "string")
90
+ publishRegistryChange(registry);
91
+ return true;
92
+ };
93
+ const executionIsChildless = (execution) => execution?._tag === "Childless" || execution?.childless === true;
94
+ const offerCompactMailbox = (mailbox, event) => {
95
+ const items = mailbox.items ?? [];
96
+ mailbox.items = items;
97
+ items.push(event);
98
+ };
99
+ const pollCompactMailbox = (mailbox) => {
100
+ if (mailbox.items === undefined) {
101
+ return Option.none();
102
+ }
103
+ const event = mailbox.items[mailbox.index];
104
+ mailbox.index += 1;
105
+ if (mailbox.index === mailbox.items.length) {
106
+ mailbox.items = undefined;
107
+ mailbox.index = 0;
108
+ }
109
+ return Option.some(event);
110
+ };
111
+ const closeCompactMailbox = (mailbox) => {
112
+ mailbox.closed = true;
113
+ mailbox.items = undefined;
114
+ mailbox.index = 0;
115
+ };
116
+ export class MachineRuntime extends Context.Service()("effect/Machine/MachineRuntime") {
117
+ }
118
+ export const provideMachineRuntime = (effect, scope) => Effect.provideService(effect, MachineRuntime, scope);
119
+ const classifyOutcome = (snapshot) => {
120
+ switch (snapshot.status) {
121
+ case "active": {
122
+ return undefined;
123
+ }
124
+ case "done": {
125
+ return {
126
+ _tag: "Done",
127
+ output: snapshot.output,
128
+ snapshot
129
+ };
130
+ }
131
+ case "error": {
132
+ const failure = snapshot.cause.reasons.find(Cause.isFailReason);
133
+ if (failure !== undefined) {
134
+ return {
135
+ _tag: "Failure",
136
+ error: failure.error,
137
+ cause: snapshot.cause,
138
+ snapshot
139
+ };
140
+ }
141
+ const defect = snapshot.cause.reasons.find(Cause.isDieReason);
142
+ if (defect !== undefined) {
143
+ return {
144
+ _tag: "Defect",
145
+ defect: defect.defect,
146
+ cause: snapshot.cause,
147
+ snapshot
148
+ };
149
+ }
150
+ const interrupted = snapshot.cause.reasons.find(Cause.isInterruptReason);
151
+ if (interrupted !== undefined) {
152
+ return {
153
+ _tag: "Interrupted",
154
+ cause: snapshot.cause,
155
+ snapshot
156
+ };
157
+ }
158
+ return {
159
+ _tag: "Cause",
160
+ cause: snapshot.cause,
161
+ snapshot
162
+ };
163
+ }
164
+ case "stopped": {
165
+ return {
166
+ _tag: "Stopped",
167
+ snapshot
168
+ };
169
+ }
170
+ }
171
+ };
172
+ const notifyActiveSnapshot = (onSnapshot, snapshot) => Effect.suspend(() => onSnapshot(snapshot)).pipe(Effect.exit, Effect.asVoid);
173
+ export const watch = (ref) => ref.changes.pipe(Stream.filter((snapshot) => snapshot.status !== "active"), Stream.map((snapshot) => classifyOutcome(snapshot)), Stream.take(1));
174
+ const makeProcessRuntime = Effect.sync(() => {
175
+ let sessionIdCounter = 0;
176
+ return {
177
+ nextSessionId: Effect.sync(() => `machine:${sessionIdCounter++}`)
178
+ };
179
+ });
180
+ class OwnedChildRuntimeImpl {
181
+ registry;
182
+ self;
183
+ runtime;
184
+ services;
185
+ scopedServices;
186
+ constructor(registry, self, runtime, services) {
187
+ this.registry = registry;
188
+ this.self = self;
189
+ this.runtime = runtime;
190
+ this.services = services;
191
+ }
192
+ has(key) {
193
+ for (const entry of this.registry.children.values()) {
194
+ if (entry.ownerActive && entry.ownerKey === key)
195
+ return true;
196
+ }
197
+ return false;
198
+ }
199
+ stopEntry(entry) {
200
+ entry.ownerActive = false;
201
+ return entry._tag === "Started" ? entry.ref.stop : Effect.void;
202
+ }
203
+ spawn(makeLogic, options) {
204
+ const token = Symbol();
205
+ let startedChild;
206
+ const isCurrent = () => {
207
+ const entry = this.registry.children.get(options.id);
208
+ return entry?.token === token && entry.ownerKey === options.key && entry.ownerActive === true;
209
+ };
210
+ return Effect.suspend(() => {
211
+ if (this.registry.closed)
212
+ return Effect.interrupt;
213
+ if (this.has(options.key) || this.registry.children.has(options.id)) {
214
+ return Effect.fail(new ChildAlreadyExistsError({ id: options.duplicateId }));
215
+ }
216
+ const logic = makeLogic();
217
+ const scope = this.registry.scope ??= Scope.makeUnsafe("parallel");
218
+ this.registry.children.set(options.id, {
219
+ _tag: "Starting",
220
+ token,
221
+ ownerKey: options.key,
222
+ ownerPath: options.path,
223
+ ownerActive: true
224
+ });
225
+ const startOptions = {
226
+ detached: true,
227
+ id: options.id,
228
+ sendParent: (event) => options.sendParent(isCurrent, event),
229
+ onOutcome: (outcome) => options.onOutcome(isCurrent, outcome),
230
+ ...(options.onSnapshot === undefined
231
+ ? undefined
232
+ : { onSnapshot: (snapshot) => options.onSnapshot(isCurrent, snapshot) }),
233
+ onReadySync: (child) => {
234
+ startedChild = child;
235
+ return registerChild(this.registry, options.id, token, child, options.descriptor);
236
+ },
237
+ onStopSync: () => unregisterChild(this.registry, options.id, token),
238
+ skipStoppedOutcome: true,
239
+ parent: this.self,
240
+ runtime: this.runtime
241
+ };
242
+ const execution = logic.execution;
243
+ const synchronous = this.services !== undefined && options.onSnapshot === undefined &&
244
+ execution?._tag === "Compiled" && execution.childless && execution.drain._tag === "Owned" &&
245
+ execution.initialSync !== undefined;
246
+ const start = synchronous
247
+ ? Effect.flatMap(this.runtime.nextSessionId, (sessionId) => new CompiledProcess(logic, startOptions, this.scopedServices ??= Context.add(this.services, Scope.Scope, scope), sessionId).initializeCompiledSync())
248
+ : startLogicInternal(logic, startOptions);
249
+ const guarded = start.pipe(Effect.onExit((exit) => {
250
+ if (Exit.isSuccess(exit))
251
+ return Effect.void;
252
+ unregisterChild(this.registry, options.id, token);
253
+ return startedChild === undefined ? Effect.void : startedChild.stop;
254
+ }));
255
+ return (synchronous ? guarded : Scope.provide(guarded, scope)).pipe(Effect.asVoid);
256
+ });
257
+ }
258
+ stopAll() {
259
+ return Effect.suspend(() => {
260
+ const effects = [];
261
+ for (const entry of this.registry.children.values()) {
262
+ if (entry.ownerActive)
263
+ effects.push(this.stopEntry(entry));
264
+ }
265
+ return effects.length === 0
266
+ ? Effect.void
267
+ : effects.length === 1
268
+ ? effects[0]
269
+ : Effect.all(effects, { concurrency: "unbounded", discard: true });
270
+ });
271
+ }
272
+ stopPaths(paths) {
273
+ if (paths.length === 0)
274
+ return undefined;
275
+ const pathSet = new Set(paths);
276
+ const effects = [];
277
+ for (const entry of this.registry.children.values()) {
278
+ if (entry.ownerActive && entry.ownerPath !== undefined && pathSet.has(entry.ownerPath)) {
279
+ effects.push(this.stopEntry(entry));
280
+ }
281
+ }
282
+ return effects.length === 0
283
+ ? undefined
284
+ : effects.length === 1
285
+ ? effects[0]
286
+ : Effect.all(effects, { concurrency: "unbounded", discard: true });
287
+ }
288
+ }
289
+ const noChildChanges = Stream.succeed(Option.none()).pipe(Stream.concat(Stream.never));
290
+ const noParentSend = (_event) => Effect.void;
291
+ const childlessRuntime = {
292
+ close: () => Effect.void,
293
+ spawn: (() => Effect.die(new Error("Childless machine logic cannot spawn a process"))),
294
+ get: () => Effect.succeed(Option.none()),
295
+ changes: () => noChildChanges,
296
+ sendTo: () => Effect.void,
297
+ stop: () => Effect.void,
298
+ owned: {
299
+ spawn: () => Effect.die(new Error("Childless machine logic cannot spawn an owned process")),
300
+ stopAll: () => Effect.void,
301
+ stopPaths: () => undefined
302
+ }
303
+ };
304
+ const makeChildRuntimeSync = (self, runtime, services) => {
305
+ // Child-registry decisions are synchronous and every access below runs in
306
+ // one Effect.sync / Effect.suspend step. Keep the unobserved representation
307
+ // compact; selector-specific handoffs are installed only while
308
+ // childChanges streams are running.
309
+ const registry = {
310
+ closed: false,
311
+ children: new Map(),
312
+ observers: undefined,
313
+ scope: undefined
314
+ };
315
+ const close = (exit) => Effect.suspend(() => {
316
+ if (registry.closed) {
317
+ return Effect.void;
318
+ }
319
+ registry.closed = true;
320
+ if (registry.scope === undefined) {
321
+ return Effect.void;
322
+ }
323
+ const finalizers = Scope.closeUnsafe(registry.scope, exit);
324
+ let first;
325
+ let rest;
326
+ for (const entry of registry.children.values()) {
327
+ if (entry._tag !== "Started") {
328
+ continue;
329
+ }
330
+ if (first === undefined) {
331
+ first = entry.ref.stop;
332
+ }
333
+ else {
334
+ rest ??= [first];
335
+ rest.push(entry.ref.stop);
336
+ }
337
+ }
338
+ if (finalizers !== undefined) {
339
+ if (first === undefined) {
340
+ first = finalizers;
341
+ }
342
+ else {
343
+ rest ??= [first];
344
+ rest.push(finalizers);
345
+ }
346
+ }
347
+ const cleanup = rest ?? first;
348
+ return cleanup === undefined
349
+ ? Effect.void
350
+ : Array.isArray(cleanup)
351
+ ? Effect.all(cleanup, { concurrency: "unbounded", discard: true })
352
+ : cleanup;
353
+ });
354
+ const unregister = (key, token) => Effect.sync(() => unregisterChild(registry, key, token));
355
+ const register = (key, token, ref, descriptor) => Effect.sync(() => registerChild(registry, key, token, ref, descriptor));
356
+ const get = (child) => {
357
+ const id = typeof child === "string" ? child : child.id;
358
+ return Effect.sync(() => {
359
+ if (registry.closed) {
360
+ return Option.none();
361
+ }
362
+ const entry = registry.children.get(id);
363
+ return entry !== undefined && matchesChild(entry, child)
364
+ ? Option.some(entry.ref)
365
+ : Option.none();
366
+ });
367
+ };
368
+ const changes = (child) => {
369
+ const id = typeof child === "string" ? child : child.id;
370
+ return Stream.fromChannel(Channel.fromTransform((_, streamScope) => Effect.sync(() => ({ child, id, values: undefined, waiter: undefined })).pipe(Effect.flatMap((observer) => {
371
+ const removeObserver = Effect.sync(() => {
372
+ if (registry.observers !== undefined) {
373
+ registry.observers.delete(observer);
374
+ if (registry.observers.size === 0) {
375
+ registry.observers = undefined;
376
+ }
377
+ }
378
+ observer.values = undefined;
379
+ observer.waiter = undefined;
380
+ });
381
+ return Scope.addFinalizer(streamScope, removeObserver).pipe(Effect.andThen(Effect.sync(() => {
382
+ if (!registry.closed && streamScope.state._tag !== "Closed") {
383
+ registry.observers ??= new Set();
384
+ registry.observers.add(observer);
385
+ }
386
+ offerChildObservation(observer, selectRegistryChild(registry, id, child));
387
+ })), Effect.as(takeChildObservations(observer)));
388
+ }))));
389
+ };
390
+ const sendTo = (child, event) => {
391
+ const id = typeof child === "string" ? child : child.id;
392
+ return Effect.suspend(() => {
393
+ if (registry.closed) {
394
+ return Effect.void;
395
+ }
396
+ const entry = registry.children.get(id);
397
+ return entry !== undefined && matchesChild(entry, child)
398
+ ? entry.ref.send(event)
399
+ : Effect.void;
400
+ });
401
+ };
402
+ const stop = (child) => {
403
+ const id = typeof child === "string" ? child : child.id;
404
+ return Effect.suspend(() => {
405
+ if (registry.closed) {
406
+ return Effect.void;
407
+ }
408
+ const entry = registry.children.get(id);
409
+ return entry !== undefined && matchesChild(entry, child)
410
+ ? entry.ref.stop
411
+ : Effect.void;
412
+ });
413
+ };
414
+ function spawn(logic, spawnOptions) {
415
+ const token = Symbol();
416
+ const key = spawnOptions?.id ?? token;
417
+ let startedChild;
418
+ return Effect.suspend(() => {
419
+ if (registry.closed) {
420
+ return Effect.interrupt;
421
+ }
422
+ if (typeof key === "string" && registry.children.has(key)) {
423
+ return Effect.fail(new ChildAlreadyExistsError({ id: key }));
424
+ }
425
+ registry.scope ??= Scope.makeUnsafe("parallel");
426
+ registry.children.set(key, { _tag: "Starting", token });
427
+ return startLogicInternal(logic, {
428
+ detached: true,
429
+ ...(spawnOptions?.id === undefined ? undefined : { id: spawnOptions.id }),
430
+ ...(spawnOptions?.onOutcome === undefined ? undefined : { onOutcome: spawnOptions.onOutcome }),
431
+ ...(spawnOptions?.[activeSnapshotObserver] === undefined
432
+ ? undefined
433
+ : { onSnapshot: spawnOptions[activeSnapshotObserver] }),
434
+ ...(spawnOptions?.[sendParentOverride] === undefined
435
+ ? undefined
436
+ : { sendParent: spawnOptions[sendParentOverride] }),
437
+ onReady: (child, requestChildStop) => Effect.sync(() => {
438
+ startedChild = child;
439
+ }).pipe(Effect.andThen(register(key, token, child, spawnOptions?.descriptor)), Effect.flatMap((registered) => registered ? Effect.void : requestChildStop)),
440
+ onStop: unregister(key, token),
441
+ parent: self,
442
+ runtime
443
+ }).pipe(Effect.onExit((exit) => Exit.isFailure(exit)
444
+ ? unregister(key, token).pipe(Effect.andThen(startedChild === undefined ? Effect.void : startedChild.stop))
445
+ : Effect.void), Scope.provide(registry.scope));
446
+ });
447
+ }
448
+ return {
449
+ close,
450
+ spawn,
451
+ get,
452
+ changes,
453
+ sendTo,
454
+ stop,
455
+ owned: new OwnedChildRuntimeImpl(registry, self, runtime, services)
456
+ };
457
+ };
458
+ const makeChildRuntime = (self, runtime, services) => Effect.sync(() => makeChildRuntimeSync(self, runtime, services));
459
+ // `Machine.logic` permits an arbitrary Effect program, including programs that
460
+ // suspend or supervise their own fibers. Keep its two-fiber worker/supervisor
461
+ // protocol as the general contract rather than weakening it for statecharts.
462
+ const startGenericInternal = Effect.fnUntraced(function* (logic, options) {
463
+ const { detached, id: requestedId, onOutcome, onReady, onReadySync, onSnapshot, onStop, onStopSync, parent, runtime, sendParent: overrideSendParent } = options;
464
+ const sessionId = yield* runtime.nextSessionId;
465
+ const id = requestedId ?? sessionId;
466
+ const queue = yield* Queue.unbounded();
467
+ const termination = yield* Deferred.make();
468
+ const done = yield* Deferred.make();
469
+ const awaitCompletion = Deferred.await(done).pipe(Effect.exit, Effect.asVoid);
470
+ let initializing = true;
471
+ const requestStop = Deferred.succeed(termination, { _tag: "Stopped" }).pipe(Effect.asVoid);
472
+ const self = {
473
+ id,
474
+ sessionId,
475
+ // Initialization must finish constructing a state before a stopped
476
+ // snapshot can be published. A stop requested there is therefore recorded
477
+ // and returns so initialization can finish. Once running, the requesting
478
+ // process waits forever and is interrupted by the supervisor after the
479
+ // stop request wins, so execution never continues after `self.stop`.
480
+ stop: Effect.suspend(() => initializing
481
+ ? requestStop
482
+ : requestStop.pipe(Effect.andThen(Effect.never))),
483
+ send: (event) => Queue.offer(queue, event).pipe(Effect.flatMap((accepted) => accepted ? Effect.void : Effect.fail(new StoppedError())))
484
+ };
485
+ let { changes: childChanges, close: closeChildren, get: getChild, owned: ownedChildren, sendTo, spawn, stop: stopChild } = childlessRuntime;
486
+ if (!executionIsChildless(logic.execution)) {
487
+ ;
488
+ ({
489
+ changes: childChanges,
490
+ close: closeChildren,
491
+ get: getChild,
492
+ owned: ownedChildren,
493
+ sendTo,
494
+ spawn,
495
+ stop: stopChild
496
+ } = yield* makeChildRuntime(self, runtime));
497
+ }
498
+ const cleanupStartupFailure = (exit) => Exit.isFailure(exit)
499
+ ? closeChildren(exit)
500
+ : Effect.void;
501
+ const cleanup = onStopSync === undefined ? onStop ?? Effect.void : Effect.sync(onStopSync);
502
+ const sendParent = overrideSendParent ?? (parent === undefined ? noParentSend : parent.send);
503
+ const scope = {
504
+ self,
505
+ parent,
506
+ spawn,
507
+ sendParent,
508
+ sendTo,
509
+ stopChild,
510
+ failCause: (cause) => Deferred.succeed(termination, {
511
+ _tag: "Failure",
512
+ cause: cause
513
+ }).pipe(Effect.asVoid)
514
+ };
515
+ const initial = yield* logic.initial(scope).pipe(Effect.onExit(cleanupStartupFailure), Effect.ensuring(Effect.sync(() => {
516
+ initializing = false;
517
+ })));
518
+ const current = yield* SynchronizedRef.make({
519
+ revision: 0,
520
+ terminalizing: false,
521
+ changes: undefined,
522
+ snapshot: {
523
+ status: "active",
524
+ state: initial
525
+ }
526
+ });
527
+ const publishSnapshot = onSnapshot === undefined
528
+ ? (snapshot) => snapshot.changes === undefined
529
+ ? Effect.succeed(snapshot)
530
+ : PubSub.publish(snapshot.changes, [snapshot]).pipe(Effect.as(snapshot))
531
+ : (snapshot) => {
532
+ const publish = snapshot.changes === undefined
533
+ ? Effect.succeed(snapshot)
534
+ : PubSub.publish(snapshot.changes, [snapshot]).pipe(Effect.as(snapshot));
535
+ const runtimeSnapshot = snapshot.snapshot;
536
+ return runtimeSnapshot.status !== "active"
537
+ ? publish
538
+ : publish.pipe(Effect.tap(() => notifyActiveSnapshot(onSnapshot, runtimeSnapshot)));
539
+ };
540
+ const completeChanges = (snapshot) => snapshot.changes === undefined
541
+ ? Effect.void
542
+ : PubSub.publish(snapshot.changes, Exit.succeed(undefined)).pipe(Effect.asVoid);
543
+ const completeIfTerminal = (snapshot) => {
544
+ if (snapshot.snapshot.status === "active") {
545
+ return Effect.succeed(snapshot);
546
+ }
547
+ return completeChanges(snapshot).pipe(Effect.as(snapshot));
548
+ };
549
+ const publishIfCurrent = (snapshot) => SynchronizedRef.get(current).pipe(Effect.flatMap((currentSnapshot) => currentSnapshot.revision === snapshot.revision
550
+ ? publishSnapshot(snapshot).pipe(Effect.flatMap(completeIfTerminal))
551
+ : Effect.succeed(undefined)));
552
+ const updateSnapshot = (f) => SynchronizedRef.modifyEffect(current, (current) => current.terminalizing
553
+ ? Effect.succeed([undefined, current])
554
+ : Effect.map(f(current.snapshot), (next) => {
555
+ if (next === undefined) {
556
+ return [undefined, current];
557
+ }
558
+ const versioned = {
559
+ revision: current.revision + 1,
560
+ snapshot: next,
561
+ terminalizing: false,
562
+ changes: current.changes
563
+ };
564
+ return [versioned, versioned];
565
+ })).pipe(Effect.flatMap((versioned) => versioned === undefined ? Effect.succeed(undefined) : publishIfCurrent(versioned)), Effect.map((published) => published?.snapshot));
566
+ const reserveTerminalSnapshot = (f) => SynchronizedRef.modify(current, (current) => {
567
+ if (current.terminalizing || current.snapshot.status !== "active") {
568
+ return [undefined, current];
569
+ }
570
+ return [
571
+ {
572
+ revision: current.revision + 1,
573
+ snapshot: f(current.snapshot),
574
+ terminalizing: true,
575
+ changes: current.changes
576
+ },
577
+ { ...current, terminalizing: true }
578
+ ];
579
+ }).pipe(Effect.map((versioned) => versioned?.snapshot));
580
+ const setAndPublishSnapshot = (snapshot) => SynchronizedRef.updateAndGet(current, (current) => ({
581
+ revision: current.revision + 1,
582
+ snapshot,
583
+ terminalizing: true,
584
+ changes: current.changes
585
+ })).pipe(Effect.flatMap(publishSnapshot), Effect.flatMap(completeIfTerminal), Effect.asVoid);
586
+ const setActiveState = (state) => updateSnapshot((snapshot) => Effect.succeed(snapshot.status === "active"
587
+ ? {
588
+ status: "active",
589
+ state
590
+ }
591
+ : undefined)).pipe(Effect.asVoid);
592
+ const terminalizeWith = (snapshot, exit, completeDone) => {
593
+ const notifyOutcome = onOutcome === undefined || (snapshot.status === "stopped" && options.skipStoppedOutcome === true)
594
+ ? Effect.void
595
+ : Effect.suspend(() => onOutcome(classifyOutcome(snapshot))).pipe(Effect.exit, Effect.asVoid);
596
+ return Effect.uninterruptible(Queue.shutdown(queue).pipe(Effect.andThen(closeChildren(exit)), Effect.andThen(setAndPublishSnapshot(snapshot)), Effect.andThen(notifyOutcome), Effect.andThen(cleanup), Effect.andThen(completeDone)));
597
+ };
598
+ const reserveStoppedSnapshot = reserveTerminalSnapshot((snapshot) => ({
599
+ status: "stopped",
600
+ state: snapshot.state
601
+ }));
602
+ const reserveFailureSnapshot = (cause) => reserveTerminalSnapshot((snapshot) => ({
603
+ status: "error",
604
+ state: snapshot.state,
605
+ cause
606
+ }));
607
+ const reserveSuccessSnapshot = (output) => reserveTerminalSnapshot((snapshot) => ({
608
+ status: "done",
609
+ state: snapshot.state,
610
+ output
611
+ }));
612
+ const terminalizeReservedStop = (snapshot) => {
613
+ const exit = Exit.void;
614
+ return terminalizeWith(snapshot, exit, Deferred.fail(done, new StoppedError()));
615
+ };
616
+ const terminalizeReservedFailure = (snapshot, cause) => {
617
+ const exit = Exit.failCause(cause);
618
+ return terminalizeWith(snapshot, exit, Deferred.failCause(done, cause));
619
+ };
620
+ const terminalizeReservedSuccess = (snapshot, output) => {
621
+ const exit = Exit.succeed(output);
622
+ return terminalizeWith(snapshot, exit, Deferred.succeed(done, output));
623
+ };
624
+ const stop = Effect.uninterruptible(requestStop.pipe(Effect.andThen(awaitCompletion)));
625
+ const context = {
626
+ ...scope,
627
+ ...(logic.execution?._tag === "Compiled" && !logic.execution.childless ? { ownedChildren } : undefined),
628
+ receive: Queue.take(queue),
629
+ mailbox: queue,
630
+ state: SynchronizedRef.get(current).pipe(Effect.map((current) => current.snapshot.state)),
631
+ setState: setActiveState,
632
+ updateState: (f) => updateSnapshot((snapshot) => snapshot.status === "active"
633
+ ? f(snapshot.state).pipe(Effect.map((state) => ({
634
+ status: "active",
635
+ state
636
+ })))
637
+ : Effect.succeed(undefined)).pipe(Effect.asVoid)
638
+ };
639
+ const getOrCreateChanges = SynchronizedRef.modifyEffect(current, (current) => {
640
+ if (current.snapshot.status !== "active") {
641
+ return Effect.succeed([undefined, current]);
642
+ }
643
+ if (current.changes !== undefined) {
644
+ return Effect.succeed([current.changes, current]);
645
+ }
646
+ return PubSub.unbounded({ replay: 1 }).pipe(Effect.map((changes) => [changes, { ...current, changes }]));
647
+ });
648
+ const changesStream = Stream.unwrap(Effect.gen(function* () {
649
+ const changes = yield* getOrCreateChanges;
650
+ if (changes === undefined) {
651
+ return Stream.succeed((yield* SynchronizedRef.get(current)).snapshot);
652
+ }
653
+ const subscription = yield* PubSub.subscribe(changes);
654
+ const captured = yield* SynchronizedRef.get(current);
655
+ if (captured.snapshot.status !== "active") {
656
+ return Stream.succeed(captured.snapshot);
657
+ }
658
+ return Stream.succeed(captured.snapshot).pipe(Stream.concat(Stream.fromChannel(Channel.fromEffectTake(PubSub.take(subscription))).pipe(Stream.filter((next) => next.revision > captured.revision), Stream.map((next) => next.snapshot))));
659
+ }));
660
+ const ref = {
661
+ id,
662
+ sessionId,
663
+ state: SynchronizedRef.get(current).pipe(Effect.map((current) => current.snapshot.state)),
664
+ snapshot: SynchronizedRef.get(current).pipe(Effect.map((current) => current.snapshot)),
665
+ changes: changesStream,
666
+ join: Deferred.await(done),
667
+ stop,
668
+ send: self.send,
669
+ child: getChild,
670
+ childChanges
671
+ };
672
+ if (onReadySync !== undefined && !onReadySync(ref)) {
673
+ yield* requestStop;
674
+ }
675
+ else if (onReady !== undefined) {
676
+ yield* onReady(ref, requestStop);
677
+ }
678
+ if (onSnapshot !== undefined) {
679
+ yield* notifyActiveSnapshot(onSnapshot, { status: "active", state: initial });
680
+ }
681
+ const reserveTermination = (termination) => {
682
+ switch (termination._tag) {
683
+ case "Stopped":
684
+ return reserveStoppedSnapshot;
685
+ case "Done":
686
+ return reserveSuccessSnapshot(termination.output);
687
+ case "Failure":
688
+ return reserveFailureSnapshot(termination.cause);
689
+ }
690
+ };
691
+ const completeTermination = (termination, snapshot) => {
692
+ switch (termination._tag) {
693
+ case "Stopped":
694
+ return terminalizeReservedStop(snapshot);
695
+ case "Done":
696
+ return terminalizeReservedSuccess(snapshot, termination.output);
697
+ case "Failure":
698
+ return terminalizeReservedFailure(snapshot, termination.cause);
699
+ }
700
+ };
701
+ const forkRuntime = (effect) => detached === true
702
+ ? Effect.forkDetach(effect)
703
+ : Effect.forkChild(effect);
704
+ const pendingTermination = yield* Deferred.poll(termination);
705
+ const worker = Option.isNone(pendingTermination)
706
+ ? yield* Effect.uninterruptibleMask((restore) => restore(Effect.suspend(() => logic.run(context))).pipe(Effect.exit, Effect.flatMap((exit) => Deferred.succeed(termination, Exit.isFailure(exit)
707
+ ? { _tag: "Failure", cause: exit.cause }
708
+ : { _tag: "Done", output: exit.value })))).pipe(forkRuntime)
709
+ : undefined;
710
+ // One Deferred arbitrates all terminal causes. The supervisor reserves the
711
+ // terminal snapshot before interrupting the worker, so worker finalizers
712
+ // cannot mutate the frozen state. It then waits for those finalizers before
713
+ // publishing and completing `join` / `stop`.
714
+ const runFiber = Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
715
+ const requested = Option.isSome(pendingTermination)
716
+ ? yield* pendingTermination.value
717
+ : yield* restore(Deferred.await(termination));
718
+ const snapshot = yield* reserveTermination(requested);
719
+ if (worker !== undefined) {
720
+ yield* Fiber.interrupt(worker);
721
+ }
722
+ if (snapshot === undefined) {
723
+ return yield* awaitCompletion;
724
+ }
725
+ return yield* completeTermination(requested, snapshot);
726
+ }));
727
+ yield* forkRuntime(runFiber);
728
+ yield* Effect.yieldNow;
729
+ return ref;
730
+ });
731
+ // Stopping is commonly used only for resource cleanup. Keep that path free of
732
+ // Error stack capture and materialize the typed join failure only if observed.
733
+ const CompiledStoppedCompletion = Symbol("effect/Machine/CompiledStoppedCompletion");
734
+ /**
735
+ * Compact runtime for compiled statecharts.
736
+ *
737
+ * All long-lived state is stored directly on this object. Operations are
738
+ * implemented by shared prototype methods, and public Effect / Stream values
739
+ * are materialized only when accessed. Arbitrary process logic continues to
740
+ * use the general runtime above.
741
+ */
742
+ class CompiledProcess {
743
+ logic;
744
+ options;
745
+ services;
746
+ id;
747
+ sessionId;
748
+ send;
749
+ mailbox = {
750
+ items: undefined,
751
+ index: 0,
752
+ closed: false
753
+ };
754
+ address;
755
+ childRuntime = childlessRuntime;
756
+ processScope;
757
+ processContext;
758
+ compiledContext;
759
+ current;
760
+ /**
761
+ * `Active` has no terminal payload, `TerminationRequested` owns
762
+ * `termination` and its optional reserved snapshot, and `Completed` owns
763
+ * `completion`. `runState` independently tracks worker activity.
764
+ */
765
+ lifecycle = "Active";
766
+ runState = "Initializing";
767
+ completion;
768
+ waiter;
769
+ termination;
770
+ terminationSnapshot;
771
+ worker;
772
+ interruptRequested = false;
773
+ offerRevision = 0;
774
+ constructor(logic, options, services, sessionId) {
775
+ this.logic = logic;
776
+ this.options = options;
777
+ this.services = services;
778
+ this.sessionId = sessionId;
779
+ this.id = options.id ?? sessionId;
780
+ this.send = (event) => this.sendEffect(event);
781
+ this.address = {
782
+ id: this.id,
783
+ sessionId,
784
+ stop: Effect.suspend(() => this.stopFromProcess()),
785
+ send: this.send
786
+ };
787
+ }
788
+ get execution() {
789
+ return this.logic.execution;
790
+ }
791
+ initializeCompiledSync() {
792
+ if (!this.execution.childless) {
793
+ this.childRuntime = makeChildRuntimeSync(this.address, this.options.runtime, this.services);
794
+ }
795
+ const parent = this.options.parent;
796
+ const sendParent = this.options.sendParent ?? (parent === undefined ? noParentSend : parent.send);
797
+ this.processScope = {
798
+ self: this.address,
799
+ parent,
800
+ spawn: this.childRuntime.spawn,
801
+ sendParent,
802
+ sendTo: this.childRuntime.sendTo,
803
+ stopChild: this.childRuntime.stop,
804
+ failCause: (cause) => this.failCause(cause)
805
+ };
806
+ const compiledInitial = this.execution.initialSync;
807
+ let initialized;
808
+ try {
809
+ initialized = compiledInitial(this.processScope);
810
+ }
811
+ catch (error) {
812
+ this.runState = "Idle";
813
+ return Effect.fail(error);
814
+ }
815
+ this.runState = "Idle";
816
+ this.current = {
817
+ revision: 0,
818
+ terminalizing: false,
819
+ changes: undefined,
820
+ snapshot: { status: "active", state: initialized.state }
821
+ };
822
+ this.compiledContext = new CompiledProcessContextImpl(this.processScope, this.childRuntime.owned, this);
823
+ if ("executionState" in initialized) {
824
+ this.compiledContext.executionState = initialized.executionState;
825
+ }
826
+ if (this.options.onReadySync !== undefined && !this.options.onReadySync(this)) {
827
+ this.requestTerminationSync({ _tag: "Stopped" });
828
+ }
829
+ if (initialized.done === true && this.lifecycle === "Active") {
830
+ this.requestTerminationSync({ _tag: "Done", output: initialized.output });
831
+ }
832
+ if (initialized.done === false && this.execution.childless &&
833
+ this.lifecycle === "Active" &&
834
+ this.mailbox.items === undefined) {
835
+ return Effect.succeed(this);
836
+ }
837
+ this.runState = "Draining";
838
+ return Effect.provideContext(this.drainRuntime(), this.services).pipe(Effect.as(this));
839
+ }
840
+ initialize() {
841
+ const self = this;
842
+ return Effect.gen(function* () {
843
+ if (!self.execution.childless) {
844
+ self.childRuntime = yield* makeChildRuntime(self.address, self.options.runtime, self.services);
845
+ }
846
+ const parent = self.options.parent;
847
+ const sendParent = self.options.sendParent ?? (parent === undefined ? noParentSend : parent.send);
848
+ self.processScope = {
849
+ self: self.address,
850
+ parent,
851
+ spawn: self.childRuntime.spawn,
852
+ sendParent,
853
+ sendTo: self.childRuntime.sendTo,
854
+ stopChild: self.childRuntime.stop,
855
+ failCause: (cause) => self.failCause(cause)
856
+ };
857
+ const cleanupStartupFailure = (exit) => Exit.isFailure(exit) ? self.childRuntime.close(exit) : Effect.void;
858
+ const compiledInitial = self.execution.initial;
859
+ const initializeEffect = compiledInitial === undefined
860
+ ? self.logic.initial(self.processScope).pipe(Effect.map((state) => ({ state, done: undefined, output: undefined })))
861
+ : compiledInitial(self.processScope);
862
+ const initialized = yield* initializeEffect.pipe(Effect.onExit(cleanupStartupFailure), Effect.ensuring(Effect.sync(() => {
863
+ self.runState = "Idle";
864
+ })));
865
+ const initial = initialized.state;
866
+ self.current = {
867
+ revision: 0,
868
+ terminalizing: false,
869
+ changes: undefined,
870
+ snapshot: { status: "active", state: initial }
871
+ };
872
+ if (self.execution.drain._tag === "Process") {
873
+ self.processContext = {
874
+ ...self.processScope,
875
+ receive: Effect.never,
876
+ poll: Effect.sync(() => pollCompactMailbox(self.mailbox)),
877
+ state: Effect.sync(() => self.current.snapshot.state),
878
+ setState: (state) => self.setActiveState(state),
879
+ updateState: (f) => self.updateState(f)
880
+ };
881
+ }
882
+ else {
883
+ self.compiledContext = new CompiledProcessContextImpl(self.processScope, self.childRuntime.owned, self);
884
+ if ("executionState" in initialized) {
885
+ self.compiledContext.executionState = initialized.executionState;
886
+ }
887
+ }
888
+ if (self.options.onReadySync !== undefined && !self.options.onReadySync(self)) {
889
+ yield* self.requestTermination({ _tag: "Stopped" });
890
+ }
891
+ else if (self.options.onReady !== undefined) {
892
+ yield* self.options.onReady(self, self.requestTermination({ _tag: "Stopped" }).pipe(Effect.asVoid));
893
+ }
894
+ if (self.options.onSnapshot !== undefined) {
895
+ yield* notifyActiveSnapshot(self.options.onSnapshot, { status: "active", state: initial });
896
+ }
897
+ if (initialized.done === true && self.lifecycle === "Active") {
898
+ yield* self.requestTermination({ _tag: "Done", output: initialized.output });
899
+ }
900
+ // A compiled machine startup plan has already settled entry actions,
901
+ // raised events, and eventless transitions. If it is known active and
902
+ // neither startup hooks nor emitted work queued an event, there is no
903
+ // first drain to perform. Future sends observe an idle run state and
904
+ // schedule the ordinary compiled worker.
905
+ if (initialized.done === false && self.execution.childless &&
906
+ self.lifecycle === "Active" && self.mailbox.items === undefined) {
907
+ return self;
908
+ }
909
+ self.runState = "Draining";
910
+ yield* self.drainRuntime();
911
+ return self;
912
+ });
913
+ }
914
+ get state() {
915
+ return Effect.sync(() => this.current.snapshot.state);
916
+ }
917
+ get snapshot() {
918
+ return Effect.sync(() => this.current.snapshot);
919
+ }
920
+ get changes() {
921
+ return this.changesStream();
922
+ }
923
+ get join() {
924
+ return Effect.suspend(() => {
925
+ if (this.lifecycle === "Completed") {
926
+ return this.resolveCompletion(this.completion);
927
+ }
928
+ this.waiter ??= Deferred.makeUnsafe();
929
+ return Deferred.await(this.waiter);
930
+ });
931
+ }
932
+ get stop() {
933
+ return Effect.uninterruptible(this.stopEffect());
934
+ }
935
+ child(child) {
936
+ return this.childRuntime.get(child);
937
+ }
938
+ childChanges(child) {
939
+ return this.childRuntime.changes(child);
940
+ }
941
+ requestTermination(requested) {
942
+ return Effect.sync(() => this.requestTerminationSync(requested));
943
+ }
944
+ hasTerminationRequest() {
945
+ return this.lifecycle === "TerminationRequested";
946
+ }
947
+ requestTerminationSync(requested) {
948
+ if (this.lifecycle !== "Active") {
949
+ return false;
950
+ }
951
+ this.lifecycle = "TerminationRequested";
952
+ this.termination = requested;
953
+ this.terminationSnapshot = this.reserveTermination(requested);
954
+ return true;
955
+ }
956
+ reserveTermination(requested) {
957
+ const latest = this.current;
958
+ if (latest === undefined || latest.terminalizing || latest.snapshot.status !== "active") {
959
+ return undefined;
960
+ }
961
+ const snapshot = requested._tag === "Stopped"
962
+ ? { status: "stopped", state: latest.snapshot.state }
963
+ : requested._tag === "Done"
964
+ ? { status: "done", state: latest.snapshot.state, output: requested.output }
965
+ : { status: "error", state: latest.snapshot.state, cause: requested.cause };
966
+ this.current = { ...latest, terminalizing: true };
967
+ return snapshot;
968
+ }
969
+ stopFromProcess() {
970
+ const request = this.requestTermination({ _tag: "Stopped" }).pipe(Effect.asVoid);
971
+ return this.runState === "Initializing" ? request : request.pipe(Effect.andThen(Effect.interrupt));
972
+ }
973
+ failCause(cause) {
974
+ const requested = { _tag: "Failure", cause };
975
+ return this.requestTermination(requested).pipe(Effect.flatMap((accepted) => accepted
976
+ ? Effect.forkDetach(this.settleRequestedTermination()).pipe(Effect.asVoid)
977
+ : Effect.void));
978
+ }
979
+ sendEffect(event) {
980
+ return Effect.uninterruptible(Effect.suspend(() => {
981
+ if (this.mailbox.closed || this.lifecycle !== "Active") {
982
+ return Effect.fail(new StoppedError());
983
+ }
984
+ offerCompactMailbox(this.mailbox, event);
985
+ this.offerRevision += 1;
986
+ if (this.runState === "Draining") {
987
+ return Effect.void;
988
+ }
989
+ this.runState = "Draining";
990
+ const scheduled = Effect.yieldNow.pipe(Effect.andThen(Effect.provideContext(this.drainRuntime(), this.services)));
991
+ const fork = this.options.detached === true
992
+ ? Effect.forkDetach(scheduled, { startImmediately: true })
993
+ : Effect.forkChild(scheduled, { startImmediately: true });
994
+ return fork.pipe(Effect.flatMap((fiber) => Effect.sync(() => {
995
+ this.worker = fiber;
996
+ if (!this.interruptRequested) {
997
+ return false;
998
+ }
999
+ this.interruptRequested = false;
1000
+ return true;
1001
+ }).pipe(Effect.flatMap((interrupt) => interrupt ? this.interruptAndFinish(fiber) : Effect.void))), Effect.asVoid);
1002
+ }));
1003
+ }
1004
+ stopEffect() {
1005
+ return Effect.suspend(() => {
1006
+ if (this.lifecycle === "Completed") {
1007
+ return Effect.void;
1008
+ }
1009
+ if (this.finishIdleChildlessStop()) {
1010
+ return Effect.void;
1011
+ }
1012
+ const requested = { _tag: "Stopped" };
1013
+ return this.requestTermination(requested).pipe(Effect.flatMap((accepted) => accepted
1014
+ ? this.settleRequestedTermination()
1015
+ : this.awaitCompletion()));
1016
+ });
1017
+ }
1018
+ finishIdleChildlessStop() {
1019
+ if (!this.execution.childless || this.runState !== "Idle" || this.worker !== undefined ||
1020
+ this.lifecycle !== "Active" ||
1021
+ (this.options.onOutcome !== undefined && this.options.skipStoppedOutcome !== true) ||
1022
+ this.options.onStop !== undefined ||
1023
+ this.current.changes !== undefined ||
1024
+ this.current.terminalizing || this.current.snapshot.status !== "active") {
1025
+ return false;
1026
+ }
1027
+ const snapshot = { status: "stopped", state: this.current.snapshot.state };
1028
+ this.lifecycle = "TerminationRequested";
1029
+ this.termination = { _tag: "Stopped" };
1030
+ this.terminationSnapshot = snapshot;
1031
+ closeCompactMailbox(this.mailbox);
1032
+ this.current = {
1033
+ revision: this.current.revision + 1,
1034
+ terminalizing: true,
1035
+ changes: undefined,
1036
+ snapshot
1037
+ };
1038
+ this.interruptRequested = false;
1039
+ if (this.compiledContext !== undefined) {
1040
+ this.compiledContext.executionState = undefined;
1041
+ }
1042
+ this.options.onStopSync?.();
1043
+ this.completion = CompiledStoppedCompletion;
1044
+ this.lifecycle = "Completed";
1045
+ if (this.waiter !== undefined) {
1046
+ Deferred.doneUnsafe(this.waiter, this.resolveCompletion(CompiledStoppedCompletion));
1047
+ this.waiter = undefined;
1048
+ }
1049
+ return true;
1050
+ }
1051
+ settleRequestedTermination() {
1052
+ return Effect.suspend(() => {
1053
+ if (this.runState !== "Draining") {
1054
+ return this.finishRequestedTermination();
1055
+ }
1056
+ if (this.worker === undefined) {
1057
+ this.interruptRequested = true;
1058
+ return this.awaitCompletion();
1059
+ }
1060
+ return this.interruptAndFinish(this.worker).pipe(Effect.andThen(this.awaitCompletion()));
1061
+ });
1062
+ }
1063
+ interruptAndFinish(worker) {
1064
+ return Fiber.interrupt(worker).pipe(Effect.andThen(Effect.suspend(() => this.lifecycle !== "Completed"
1065
+ ? this.finishRequestedTermination()
1066
+ : Effect.void)));
1067
+ }
1068
+ awaitCompletion() {
1069
+ return this.lifecycle !== "Completed"
1070
+ ? this.join.pipe(Effect.exit, Effect.asVoid)
1071
+ : Effect.void;
1072
+ }
1073
+ resolveCompletion(completion) {
1074
+ if (completion !== CompiledStoppedCompletion) {
1075
+ return completion;
1076
+ }
1077
+ const stopped = Effect.fail(new StoppedError());
1078
+ this.completion = stopped;
1079
+ return stopped;
1080
+ }
1081
+ drainRuntime() {
1082
+ const self = this;
1083
+ return Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
1084
+ let observedRevision = self.offerRevision;
1085
+ while (true) {
1086
+ if (self.hasTerminationRequest()) {
1087
+ return yield* self.finishRequestedTermination();
1088
+ }
1089
+ const exit = yield* restore(Effect.suspend(() => {
1090
+ const drain = self.execution.drain;
1091
+ return drain._tag === "Process"
1092
+ ? drain.run(self.processContext)
1093
+ : drain.run(self.compiledContext);
1094
+ })).pipe(Effect.exit);
1095
+ self.flushPendingChanges();
1096
+ if (Exit.isFailure(exit)) {
1097
+ if (self.lifecycle === "Active") {
1098
+ yield* self.requestTermination({ _tag: "Failure", cause: exit.cause });
1099
+ }
1100
+ return yield* self.finishRequestedTermination();
1101
+ }
1102
+ if (Option.isSome(exit.value)) {
1103
+ yield* self.requestTermination({ _tag: "Done", output: exit.value.value });
1104
+ return yield* self.finishRequestedTermination();
1105
+ }
1106
+ if (self.hasTerminationRequest()) {
1107
+ return yield* self.finishRequestedTermination();
1108
+ }
1109
+ if (self.offerRevision !== observedRevision) {
1110
+ observedRevision = self.offerRevision;
1111
+ continue;
1112
+ }
1113
+ self.runState = "Idle";
1114
+ self.worker = undefined;
1115
+ return;
1116
+ }
1117
+ }));
1118
+ }
1119
+ finishRequestedTermination() {
1120
+ return Effect.suspend(() => {
1121
+ if (this.lifecycle !== "TerminationRequested") {
1122
+ return Effect.void;
1123
+ }
1124
+ const requested = this.termination;
1125
+ if (requested === undefined) {
1126
+ return Effect.void;
1127
+ }
1128
+ const snapshot = this.terminationSnapshot ?? this.reserveTermination(requested);
1129
+ if (snapshot === undefined) {
1130
+ return this.awaitCompletion();
1131
+ }
1132
+ const exit = requested._tag === "Stopped"
1133
+ ? Exit.void
1134
+ : requested._tag === "Done"
1135
+ ? Exit.succeed(requested.output)
1136
+ : Exit.failCause(requested.cause);
1137
+ const completion = requested._tag === "Stopped"
1138
+ ? CompiledStoppedCompletion
1139
+ : requested._tag === "Done"
1140
+ ? Effect.succeed(requested.output)
1141
+ : Effect.failCause(requested.cause);
1142
+ const notifyOutcome = this.options.onOutcome === undefined ||
1143
+ (requested._tag === "Stopped" && this.options.skipStoppedOutcome === true)
1144
+ ? Effect.void
1145
+ : Effect.suspend(() => this.options.onOutcome(classifyOutcome(snapshot))).pipe(Effect.exit, Effect.asVoid);
1146
+ return Effect.uninterruptible(Effect.sync(() => closeCompactMailbox(this.mailbox)).pipe(Effect.andThen(this.childRuntime.close(exit)), Effect.andThen(this.setAndPublishSnapshot(snapshot)), Effect.andThen(notifyOutcome), Effect.andThen(this.options.onStop ?? Effect.void), Effect.andThen(Effect.sync(() => {
1147
+ this.options.onStopSync?.();
1148
+ this.runState = "Idle";
1149
+ this.worker = undefined;
1150
+ this.interruptRequested = false;
1151
+ if (this.compiledContext !== undefined) {
1152
+ this.compiledContext.executionState = undefined;
1153
+ }
1154
+ this.completion = completion;
1155
+ this.lifecycle = "Completed";
1156
+ if (this.waiter !== undefined) {
1157
+ Deferred.doneUnsafe(this.waiter, this.resolveCompletion(completion));
1158
+ this.waiter = undefined;
1159
+ }
1160
+ }))));
1161
+ });
1162
+ }
1163
+ publishSnapshot(snapshot) {
1164
+ const publish = snapshot.changes === undefined
1165
+ ? Effect.succeed(snapshot)
1166
+ : PubSub.publish(snapshot.changes, [snapshot]).pipe(Effect.as(snapshot));
1167
+ const current = snapshot.snapshot;
1168
+ return this.options.onSnapshot === undefined || current.status !== "active"
1169
+ ? publish
1170
+ : publish.pipe(Effect.tap(() => notifyActiveSnapshot(this.options.onSnapshot, current)));
1171
+ }
1172
+ completeChanges(snapshot) {
1173
+ return snapshot.changes === undefined
1174
+ ? Effect.void
1175
+ : PubSub.publish(snapshot.changes, Exit.succeed(undefined)).pipe(Effect.asVoid);
1176
+ }
1177
+ setAndPublishSnapshot(snapshot) {
1178
+ return Effect.suspend(() => {
1179
+ this.flushPendingChanges();
1180
+ const versioned = {
1181
+ revision: this.current.revision + 1,
1182
+ snapshot,
1183
+ terminalizing: true,
1184
+ changes: this.current.changes
1185
+ };
1186
+ this.current = versioned;
1187
+ return this.publishSnapshot(versioned).pipe(Effect.flatMap((published) => this.completeChanges(published)), Effect.asVoid);
1188
+ });
1189
+ }
1190
+ setActiveState(state) {
1191
+ return Effect.suspend(() => this.commitActiveState(state) ?? Effect.void);
1192
+ }
1193
+ pollCompiledEvent() {
1194
+ return pollCompactMailbox(this.mailbox);
1195
+ }
1196
+ compiledState() {
1197
+ return this.current.snapshot.state;
1198
+ }
1199
+ commitCompiledState(state) {
1200
+ return this.commitActiveState(state, true);
1201
+ }
1202
+ commitActiveState(state, batchChanges = false) {
1203
+ const latest = this.current;
1204
+ if (latest.terminalizing || latest.snapshot.status !== "active") {
1205
+ return undefined;
1206
+ }
1207
+ const pendingChanges = latest.pendingChanges;
1208
+ if (pendingChanges !== undefined) {
1209
+ latest.pendingChanges = undefined;
1210
+ }
1211
+ const activeSnapshot = { status: "active", state };
1212
+ const versioned = {
1213
+ revision: latest.revision + 1,
1214
+ snapshot: activeSnapshot,
1215
+ terminalizing: false,
1216
+ changes: latest.changes
1217
+ };
1218
+ this.current = versioned;
1219
+ if (versioned.changes !== undefined) {
1220
+ if (pendingChanges === undefined) {
1221
+ if (batchChanges) {
1222
+ versioned.pendingChanges = [versioned];
1223
+ }
1224
+ else {
1225
+ PubSub.publishUnsafe(versioned.changes, [versioned]);
1226
+ }
1227
+ }
1228
+ else {
1229
+ pendingChanges.push(versioned);
1230
+ if (batchChanges) {
1231
+ versioned.pendingChanges = pendingChanges;
1232
+ }
1233
+ else {
1234
+ PubSub.publishUnsafe(versioned.changes, pendingChanges);
1235
+ }
1236
+ }
1237
+ }
1238
+ return this.options.onSnapshot === undefined
1239
+ ? undefined
1240
+ : notifyActiveSnapshot(this.options.onSnapshot, activeSnapshot);
1241
+ }
1242
+ flushPendingChanges() {
1243
+ const current = this.current;
1244
+ const pendingChanges = current?.pendingChanges;
1245
+ if (pendingChanges === undefined || current.changes === undefined) {
1246
+ return;
1247
+ }
1248
+ current.pendingChanges = undefined;
1249
+ PubSub.publishUnsafe(current.changes, pendingChanges);
1250
+ }
1251
+ updateState(f) {
1252
+ return Effect.suspend(() => {
1253
+ const observed = this.current;
1254
+ if (observed.terminalizing || observed.snapshot.status !== "active") {
1255
+ return Effect.void;
1256
+ }
1257
+ return f(observed.snapshot.state).pipe(Effect.flatMap((state) => {
1258
+ const latest = this.current;
1259
+ return latest.terminalizing || latest.revision !== observed.revision
1260
+ ? Effect.void
1261
+ : this.setActiveState(state);
1262
+ }));
1263
+ });
1264
+ }
1265
+ getOrCreateChanges() {
1266
+ return Effect.suspend(() => {
1267
+ const observed = this.current;
1268
+ if (observed.snapshot.status !== "active") {
1269
+ return Effect.succeed(undefined);
1270
+ }
1271
+ if (observed.changes !== undefined) {
1272
+ return Effect.succeed(observed.changes);
1273
+ }
1274
+ return PubSub.unbounded({ replay: 1 }).pipe(Effect.flatMap((candidate) => Effect.sync(() => {
1275
+ const latest = this.current;
1276
+ if (latest.snapshot.status !== "active") {
1277
+ return [undefined, true];
1278
+ }
1279
+ if (latest.changes !== undefined) {
1280
+ return [latest.changes, true];
1281
+ }
1282
+ this.current = { ...latest, changes: candidate };
1283
+ return [candidate, false];
1284
+ }).pipe(Effect.flatMap(([changes, discard]) => discard ? PubSub.shutdown(candidate).pipe(Effect.as(changes)) : Effect.succeed(changes)))));
1285
+ });
1286
+ }
1287
+ changesStream() {
1288
+ const self = this;
1289
+ return Stream.unwrap(Effect.gen(function* () {
1290
+ const changes = yield* self.getOrCreateChanges();
1291
+ if (changes === undefined) {
1292
+ return Stream.succeed(self.current.snapshot);
1293
+ }
1294
+ const subscription = yield* PubSub.subscribe(changes);
1295
+ const captured = self.current;
1296
+ if (captured.snapshot.status !== "active") {
1297
+ return Stream.succeed(captured.snapshot);
1298
+ }
1299
+ return Stream.succeed(captured.snapshot).pipe(Stream.concat(Stream.fromChannel(Channel.fromEffectTake(PubSub.take(subscription))).pipe(Stream.filter((next) => next.revision > captured.revision), Stream.map((next) => next.snapshot))));
1300
+ }));
1301
+ }
1302
+ }
1303
+ class CompiledProcessContextImpl {
1304
+ scope;
1305
+ ownedChildren;
1306
+ process;
1307
+ executionState;
1308
+ constructor(scope, ownedChildren, process) {
1309
+ this.scope = scope;
1310
+ this.ownedChildren = ownedChildren;
1311
+ this.process = process;
1312
+ }
1313
+ poll() {
1314
+ return this.process.pollCompiledEvent();
1315
+ }
1316
+ state() {
1317
+ return this.process.compiledState();
1318
+ }
1319
+ commit(state) {
1320
+ return this.process.commitCompiledState(state);
1321
+ }
1322
+ runAfterChanges(effect) {
1323
+ this.process.flushPendingChanges();
1324
+ return effect;
1325
+ }
1326
+ }
1327
+ const startCompactCompiledInternal = Effect.fnUntraced(function* (logic, options) {
1328
+ const sessionId = yield* options.runtime.nextSessionId;
1329
+ const services = yield* Effect.context();
1330
+ const execution = logic.execution;
1331
+ const process = new CompiledProcess(logic, options, services, sessionId);
1332
+ // A compiled initializer is synchronous by construction. Only startup
1333
+ // callbacks that themselves return Effects need the generic initialization
1334
+ // program; the compiled drain is still provided the complete service context.
1335
+ return yield* execution.initialSync !== undefined &&
1336
+ options.onReady === undefined && options.onSnapshot === undefined
1337
+ ? process.initializeCompiledSync()
1338
+ : process.initialize();
1339
+ });
1340
+ const startLogicInternal = ((logic, options) => logic.execution?._tag === "Compiled"
1341
+ ? startCompactCompiledInternal(logic, options)
1342
+ : startGenericInternal(logic, options));
1343
+ const startProcessWithStrategy = Effect.fnUntraced(function* (logic, strategy, options) {
1344
+ const runtime = yield* makeProcessRuntime;
1345
+ const internalOptions = options === undefined
1346
+ ? {
1347
+ detached: true,
1348
+ runtime
1349
+ }
1350
+ : {
1351
+ ...options,
1352
+ detached: true,
1353
+ runtime
1354
+ };
1355
+ if (strategy === "generic") {
1356
+ return yield* startGenericInternal(logic, internalOptions);
1357
+ }
1358
+ if (strategy === "compiled") {
1359
+ if (logic.execution?._tag !== "Compiled") {
1360
+ return yield* Effect.die(new Error("Machine cannot force the compiled runtime for generic process logic"));
1361
+ }
1362
+ return yield* startCompactCompiledInternal(logic, internalOptions);
1363
+ }
1364
+ return yield* startLogicInternal(logic, internalOptions);
1365
+ });
1366
+ /** @internal Test-only startup strategy selection. */
1367
+ export const startProcessWithStrategyForTesting = (logic, strategy, options) => startProcessWithStrategy(logic, strategy, options);
1368
+ export const startProcess = Effect.fnUntraced(function* (logic, options) {
1369
+ const runtime = yield* makeProcessRuntime;
1370
+ return yield* startLogicInternal(logic, options === undefined
1371
+ ? {
1372
+ detached: true,
1373
+ runtime
1374
+ }
1375
+ : {
1376
+ ...options,
1377
+ detached: true,
1378
+ runtime
1379
+ });
1380
+ });
1381
+ //# sourceMappingURL=runtime.js.map