@typeonce/effect-machine 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +8 -8
- package/src/Machine.ts +6873 -0
- package/src/index.ts +1 -0
- package/src/internal/machine/activities.ts +108 -0
- package/src/internal/machine/atom.ts +636 -0
- package/src/internal/machine/cluster.ts +394 -0
- package/src/internal/machine/command.ts +58 -0
- package/src/internal/machine/commandRuntime.ts +43 -0
- package/src/internal/machine/configuration.ts +1331 -0
- package/src/internal/machine/errors.ts +87 -0
- package/src/internal/machine/executionPlan.ts +996 -0
- package/src/internal/machine/invocation.ts +119 -0
- package/src/internal/machine/machine.ts +1747 -0
- package/src/internal/machine/planner.ts +1933 -0
- package/src/internal/machine/process.ts +906 -0
- package/src/internal/machine/protocol.ts +322 -0
- package/src/internal/machine/readiness.ts +10 -0
- package/src/internal/machine/runtime.ts +2512 -0
- package/src/internal/machine/serialization.ts +498 -0
- package/src/internal/machine/stateDefinition.ts +270 -0
- package/src/internal/machine/symbols.ts +2 -0
- package/src/internal/machine/topology.ts +479 -0
- package/src/internal/testing/machine/arbitrary.ts +102 -0
- package/src/internal/testing/machine/exploration.ts +331 -0
- package/src/internal/testing/machine/finiteModel.ts +1498 -0
- package/src/internal/testing/machine/invariant.ts +372 -0
- package/src/internal/testing/machine/probe.ts +79 -0
- package/src/internal/testing/machine/referenceModel.ts +1505 -0
- package/src/internal/testing/machine/runtime.ts +1710 -0
- package/src/internal/testing/machine/runtimeInvariant.ts +486 -0
- package/src/internal/testing/machine/trace.ts +150 -0
- package/src/internal/testing/machine/verification.ts +1890 -0
- package/src/testing/MachineTest.ts +2067 -0
- package/src/testing/index.ts +7 -0
- package/src/unstable/cluster/ClusterMachine.ts +390 -0
- package/src/unstable/cluster/index.ts +1 -0
- package/src/unstable/reactivity/AtomMachine.ts +649 -0
- package/src/unstable/reactivity/index.ts +1 -0
|
@@ -0,0 +1,2512 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal machine process runtime helpers.
|
|
3
|
+
*
|
|
4
|
+
* @since 0.4.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as Cause from "effect/Cause"
|
|
8
|
+
import * as Channel from "effect/Channel"
|
|
9
|
+
import * as Context from "effect/Context"
|
|
10
|
+
import * as Deferred from "effect/Deferred"
|
|
11
|
+
import * as Effect from "effect/Effect"
|
|
12
|
+
import * as Exit from "effect/Exit"
|
|
13
|
+
import * as Fiber from "effect/Fiber"
|
|
14
|
+
import * as Option from "effect/Option"
|
|
15
|
+
import * as PubSub from "effect/PubSub"
|
|
16
|
+
import * as Queue from "effect/Queue"
|
|
17
|
+
import * as Scope from "effect/Scope"
|
|
18
|
+
import * as Stream from "effect/Stream"
|
|
19
|
+
import * as SynchronizedRef from "effect/SynchronizedRef"
|
|
20
|
+
import type * as Take from "effect/Take"
|
|
21
|
+
import { ChildAlreadyExistsError, StoppedError } from "./errors.js"
|
|
22
|
+
|
|
23
|
+
type ChildDescriptor = {
|
|
24
|
+
readonly id: string
|
|
25
|
+
readonly machine: object
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type ChildEntry =
|
|
29
|
+
| {
|
|
30
|
+
readonly _tag: "Starting"
|
|
31
|
+
readonly token: symbol
|
|
32
|
+
readonly ownerKey?: string
|
|
33
|
+
readonly ownerPath?: string
|
|
34
|
+
ownerActive?: boolean
|
|
35
|
+
}
|
|
36
|
+
| {
|
|
37
|
+
readonly _tag: "Started"
|
|
38
|
+
readonly token: symbol
|
|
39
|
+
readonly descriptor: ChildDescriptor | undefined
|
|
40
|
+
readonly ref: MachineRef<any, any, any, any>
|
|
41
|
+
readonly ownerKey?: string
|
|
42
|
+
readonly ownerPath?: string
|
|
43
|
+
ownerActive?: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type ChildSelector = string | ChildDescriptor
|
|
47
|
+
type ChildKey = string | symbol
|
|
48
|
+
|
|
49
|
+
/** @internal */
|
|
50
|
+
export const activeSnapshotObserver: unique symbol = Symbol.for("effect/Machine/activeSnapshotObserver")
|
|
51
|
+
|
|
52
|
+
/** @internal */
|
|
53
|
+
export const sendParentOverride: unique symbol = Symbol.for("effect/Machine/sendParentOverride")
|
|
54
|
+
|
|
55
|
+
/** @internal */
|
|
56
|
+
export const acknowledgedSend: unique symbol = Symbol.for("effect/Machine/acknowledgedSend")
|
|
57
|
+
|
|
58
|
+
/** @internal */
|
|
59
|
+
export interface AcknowledgedDelivery<State> {
|
|
60
|
+
readonly before: State
|
|
61
|
+
readonly plan: unknown
|
|
62
|
+
readonly after: State
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const AcknowledgedMessageTypeId: unique symbol = Symbol("effect/Machine/AcknowledgedMessage")
|
|
66
|
+
|
|
67
|
+
/** @internal */
|
|
68
|
+
export interface AcknowledgedMessage<Event> {
|
|
69
|
+
readonly [AcknowledgedMessageTypeId]: true
|
|
70
|
+
readonly event: Event
|
|
71
|
+
readonly deferred: Deferred.Deferred<AcknowledgedDelivery<unknown>, unknown>
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** @internal */
|
|
75
|
+
export type ProcessMessage<Event> = Event | AcknowledgedMessage<Event>
|
|
76
|
+
|
|
77
|
+
/** @internal */
|
|
78
|
+
export const isAcknowledgedMessage = <Event>(
|
|
79
|
+
message: ProcessMessage<Event>
|
|
80
|
+
): message is AcknowledgedMessage<Event> =>
|
|
81
|
+
typeof message === "object" && message !== null && AcknowledgedMessageTypeId in message
|
|
82
|
+
|
|
83
|
+
/** @internal */
|
|
84
|
+
export const messageEvent = <Event>(message: ProcessMessage<Event>): Event =>
|
|
85
|
+
isAcknowledgedMessage(message) ? message.event : message
|
|
86
|
+
|
|
87
|
+
const succeedAcknowledgedMessage = <State>(
|
|
88
|
+
message: ProcessMessage<unknown> | undefined,
|
|
89
|
+
delivery: AcknowledgedDelivery<State>
|
|
90
|
+
): void => {
|
|
91
|
+
if (message !== undefined && isAcknowledgedMessage(message)) {
|
|
92
|
+
Deferred.doneUnsafe(message.deferred, Effect.succeed(delivery as AcknowledgedDelivery<unknown>))
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const failAcknowledgedMessage = (
|
|
97
|
+
message: ProcessMessage<unknown> | undefined,
|
|
98
|
+
cause: Cause.Cause<unknown>
|
|
99
|
+
): void => {
|
|
100
|
+
if (message !== undefined && isAcknowledgedMessage(message)) {
|
|
101
|
+
Deferred.doneUnsafe(message.deferred, Effect.failCause(cause))
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const stopAcknowledgedMessage = (message: ProcessMessage<unknown> | undefined): void => {
|
|
106
|
+
if (message !== undefined && isAcknowledgedMessage(message)) {
|
|
107
|
+
Deferred.doneUnsafe(message.deferred, Effect.fail(new StoppedError()))
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
type ChildObservation = Option.Option<MachineRef<any, any, any, any>>
|
|
112
|
+
type ChildObservationBatch = [ChildObservation, ...Array<ChildObservation>]
|
|
113
|
+
|
|
114
|
+
interface ChildObserver {
|
|
115
|
+
readonly child: ChildSelector
|
|
116
|
+
readonly id: string
|
|
117
|
+
values: ChildObservationBatch | undefined
|
|
118
|
+
waiter: Deferred.Deferred<void> | undefined
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const offerChildObservation = (
|
|
122
|
+
observer: ChildObserver,
|
|
123
|
+
value: ChildObservation
|
|
124
|
+
): void => {
|
|
125
|
+
if (observer.values === undefined) {
|
|
126
|
+
observer.values = [value]
|
|
127
|
+
} else {
|
|
128
|
+
observer.values.push(value)
|
|
129
|
+
}
|
|
130
|
+
if (observer.waiter !== undefined) {
|
|
131
|
+
const waiter = observer.waiter
|
|
132
|
+
observer.waiter = undefined
|
|
133
|
+
Deferred.doneUnsafe(waiter, Effect.void)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const takeChildObservations = (
|
|
138
|
+
observer: ChildObserver
|
|
139
|
+
): Effect.Effect<ChildObservationBatch> =>
|
|
140
|
+
Effect.suspend(() => {
|
|
141
|
+
if (observer.values !== undefined) {
|
|
142
|
+
const values = observer.values
|
|
143
|
+
observer.values = undefined
|
|
144
|
+
return Effect.succeed(values)
|
|
145
|
+
}
|
|
146
|
+
const waiter = Deferred.makeUnsafe<void>()
|
|
147
|
+
observer.waiter = waiter
|
|
148
|
+
return Deferred.await(waiter).pipe(Effect.andThen(takeChildObservations(observer)))
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
interface ChildRegistry {
|
|
152
|
+
closed: boolean
|
|
153
|
+
readonly children: Map<ChildKey, ChildEntry>
|
|
154
|
+
observers: Set<ChildObserver> | undefined
|
|
155
|
+
scope: Scope.Closeable | undefined
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const matchesChild = (
|
|
159
|
+
entry: ChildEntry,
|
|
160
|
+
child: ChildSelector
|
|
161
|
+
): entry is Extract<ChildEntry, { readonly _tag: "Started" }> =>
|
|
162
|
+
entry._tag === "Started" && (typeof child === "string" || (
|
|
163
|
+
entry.descriptor !== undefined &&
|
|
164
|
+
entry.descriptor.id === child.id &&
|
|
165
|
+
entry.descriptor.machine === child.machine
|
|
166
|
+
))
|
|
167
|
+
|
|
168
|
+
const selectRegistryChild = (
|
|
169
|
+
registry: ChildRegistry,
|
|
170
|
+
id: string,
|
|
171
|
+
child: ChildSelector
|
|
172
|
+
): ChildObservation => {
|
|
173
|
+
if (registry.closed) return Option.none()
|
|
174
|
+
const entry = registry.children.get(id)
|
|
175
|
+
return entry !== undefined && matchesChild(entry, child) ? Option.some(entry.ref) : Option.none()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const publishRegistryChange = (registry: ChildRegistry): void => {
|
|
179
|
+
if (registry.observers === undefined) return
|
|
180
|
+
for (const observer of registry.observers) {
|
|
181
|
+
offerChildObservation(observer, selectRegistryChild(registry, observer.id, observer.child))
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const unregisterChild = (registry: ChildRegistry, key: ChildKey, token: symbol): void => {
|
|
186
|
+
const entry = registry.children.get(key)
|
|
187
|
+
if (entry === undefined || entry.token !== token) return
|
|
188
|
+
registry.children.delete(key)
|
|
189
|
+
if (typeof key === "string") publishRegistryChange(registry)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const registerChild = (
|
|
193
|
+
registry: ChildRegistry,
|
|
194
|
+
key: ChildKey,
|
|
195
|
+
token: symbol,
|
|
196
|
+
ref: MachineRef<any, any, any, any>,
|
|
197
|
+
descriptor: ChildDescriptor | undefined
|
|
198
|
+
): boolean => {
|
|
199
|
+
const entry = registry.children.get(key)
|
|
200
|
+
if (registry.closed || entry === undefined || entry._tag !== "Starting" || entry.token !== token) {
|
|
201
|
+
return false
|
|
202
|
+
}
|
|
203
|
+
registry.children.delete(key)
|
|
204
|
+
const started: ChildEntry = entry.ownerKey === undefined
|
|
205
|
+
? { _tag: "Started", token, descriptor, ref }
|
|
206
|
+
: {
|
|
207
|
+
_tag: "Started",
|
|
208
|
+
token,
|
|
209
|
+
descriptor,
|
|
210
|
+
ref,
|
|
211
|
+
ownerKey: entry.ownerKey,
|
|
212
|
+
ownerPath: entry.ownerPath!,
|
|
213
|
+
ownerActive: entry.ownerActive === true
|
|
214
|
+
}
|
|
215
|
+
registry.children.set(key, started)
|
|
216
|
+
if (typeof key === "string") publishRegistryChange(registry)
|
|
217
|
+
return true
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export type RuntimeSnapshot<State, Error = never, Output = never> =
|
|
221
|
+
| {
|
|
222
|
+
readonly status: "active"
|
|
223
|
+
readonly state: State
|
|
224
|
+
}
|
|
225
|
+
| {
|
|
226
|
+
readonly status: "done"
|
|
227
|
+
readonly state: State
|
|
228
|
+
readonly output: Output
|
|
229
|
+
}
|
|
230
|
+
| {
|
|
231
|
+
readonly status: "error"
|
|
232
|
+
readonly state: State
|
|
233
|
+
readonly cause: Cause.Cause<Error>
|
|
234
|
+
}
|
|
235
|
+
| {
|
|
236
|
+
readonly status: "stopped"
|
|
237
|
+
readonly state: State
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
interface VersionedSnapshot<State, Error, Output> {
|
|
241
|
+
readonly revision: number
|
|
242
|
+
readonly snapshot: RuntimeSnapshot<State, Error, Output>
|
|
243
|
+
readonly terminalizing: boolean
|
|
244
|
+
readonly changes: PubSub.PubSub<Take.Take<VersionedSnapshot<State, Error, Output>>> | undefined
|
|
245
|
+
/** Compiled drains retain one non-empty publication chunk until their next Effect boundary. */
|
|
246
|
+
pendingChanges?: VersionedSnapshotBatch<State, Error, Output> | undefined
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
type VersionedSnapshotBatch<State, Error, Output> = [
|
|
250
|
+
VersionedSnapshot<State, Error, Output>,
|
|
251
|
+
...Array<VersionedSnapshot<State, Error, Output>>
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
export type RuntimeOutcome<State, Error = never, Output = never> =
|
|
255
|
+
| {
|
|
256
|
+
readonly _tag: "Done"
|
|
257
|
+
readonly output: Output
|
|
258
|
+
readonly snapshot: Extract<RuntimeSnapshot<State, Error, Output>, { readonly status: "done" }>
|
|
259
|
+
}
|
|
260
|
+
| {
|
|
261
|
+
readonly _tag: "Failure"
|
|
262
|
+
readonly error: Error
|
|
263
|
+
readonly cause: Cause.Cause<Error>
|
|
264
|
+
readonly snapshot: Extract<RuntimeSnapshot<State, Error, Output>, { readonly status: "error" }>
|
|
265
|
+
}
|
|
266
|
+
| {
|
|
267
|
+
readonly _tag: "Defect"
|
|
268
|
+
readonly defect: unknown
|
|
269
|
+
readonly cause: Cause.Cause<Error>
|
|
270
|
+
readonly snapshot: Extract<RuntimeSnapshot<State, Error, Output>, { readonly status: "error" }>
|
|
271
|
+
}
|
|
272
|
+
| {
|
|
273
|
+
readonly _tag: "Interrupted"
|
|
274
|
+
readonly cause: Cause.Cause<Error>
|
|
275
|
+
readonly snapshot: Extract<RuntimeSnapshot<State, Error, Output>, { readonly status: "error" }>
|
|
276
|
+
}
|
|
277
|
+
| {
|
|
278
|
+
readonly _tag: "Cause"
|
|
279
|
+
readonly cause: Cause.Cause<Error>
|
|
280
|
+
readonly snapshot: Extract<RuntimeSnapshot<State, Error, Output>, { readonly status: "error" }>
|
|
281
|
+
}
|
|
282
|
+
| {
|
|
283
|
+
readonly _tag: "Stopped"
|
|
284
|
+
readonly snapshot: Extract<RuntimeSnapshot<State, Error, Output>, { readonly status: "stopped" }>
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export interface MachineRef<out State, in Event, out Error = never, out Output = never> {
|
|
288
|
+
readonly id: string
|
|
289
|
+
readonly sessionId: string
|
|
290
|
+
readonly state: Effect.Effect<State>
|
|
291
|
+
readonly snapshot: Effect.Effect<RuntimeSnapshot<State, Error, Output>>
|
|
292
|
+
readonly changes: Stream.Stream<RuntimeSnapshot<State, Error, Output>>
|
|
293
|
+
readonly join: Effect.Effect<Output, Error | StoppedError>
|
|
294
|
+
readonly stop: Effect.Effect<void>
|
|
295
|
+
readonly send: (event: Event) => Effect.Effect<void, StoppedError>
|
|
296
|
+
readonly [acknowledgedSend]?: (
|
|
297
|
+
event: Event
|
|
298
|
+
) => Effect.Effect<AcknowledgedDelivery<State>, Error | StoppedError>
|
|
299
|
+
readonly child: (child: any) => Effect.Effect<Option.Option<any>>
|
|
300
|
+
readonly childChanges: (child: any) => Stream.Stream<Option.Option<any>>
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
interface ProcessAddress<in Event> {
|
|
304
|
+
readonly id: string
|
|
305
|
+
readonly sessionId: string
|
|
306
|
+
readonly stop: Effect.Effect<void>
|
|
307
|
+
readonly send: (event: Event) => Effect.Effect<void, StoppedError>
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export interface ProcessScope<Event> {
|
|
311
|
+
readonly self: ProcessAddress<Event>
|
|
312
|
+
readonly parent: ProcessAddress<unknown> | undefined
|
|
313
|
+
readonly spawn: ProcessSpawn
|
|
314
|
+
readonly sendParent: (event: unknown) => Effect.Effect<void, StoppedError>
|
|
315
|
+
readonly sendTo: (child: ChildSelector, event: unknown) => Effect.Effect<void, StoppedError>
|
|
316
|
+
readonly stopChild: (child: ChildSelector) => Effect.Effect<void>
|
|
317
|
+
/** @internal */
|
|
318
|
+
readonly failCause: (cause: Cause.Cause<unknown>) => Effect.Effect<void>
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export interface ProcessContext<State, Event> extends ProcessScope<Event> {
|
|
322
|
+
readonly receive: Effect.Effect<Event>
|
|
323
|
+
/** @internal */
|
|
324
|
+
readonly poll?: Effect.Effect<Option.Option<Event>>
|
|
325
|
+
/** @internal */
|
|
326
|
+
readonly receiveMessage?: Effect.Effect<ProcessMessage<Event>>
|
|
327
|
+
/** @internal */
|
|
328
|
+
readonly pollMessage?: Effect.Effect<Option.Option<ProcessMessage<Event>>>
|
|
329
|
+
/** @internal */
|
|
330
|
+
readonly completeMessage?: (delivery: AcknowledgedDelivery<State>) => void
|
|
331
|
+
readonly state: Effect.Effect<State>
|
|
332
|
+
readonly setState: (state: State) => Effect.Effect<void>
|
|
333
|
+
readonly updateState: <E, R>(
|
|
334
|
+
f: (state: State) => Effect.Effect<State, E, R>
|
|
335
|
+
) => Effect.Effect<void, E, R>
|
|
336
|
+
/** Present only when a compiled statechart is forced through the generic runtime. @internal */
|
|
337
|
+
readonly ownedChildren?: OwnedChildRuntime
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Owner-local execution context for compiled statecharts.
|
|
342
|
+
*
|
|
343
|
+
* Unlike `ProcessContext`, synchronous mailbox and state operations do not
|
|
344
|
+
* introduce an Effect boundary. The compiled drain still returns an Effect so
|
|
345
|
+
* actor commands, invokes, observation callbacks, interruption, and the Effect
|
|
346
|
+
* scheduler remain explicit at their actual boundaries.
|
|
347
|
+
*
|
|
348
|
+
* @internal
|
|
349
|
+
*/
|
|
350
|
+
export interface CompiledProcessContext<State, Event> {
|
|
351
|
+
readonly scope: ProcessScope<Event>
|
|
352
|
+
readonly ownedChildren: OwnedChildRuntime
|
|
353
|
+
readonly poll: () => Option.Option<Event>
|
|
354
|
+
readonly pollMessage: () => Option.Option<ProcessMessage<Event>>
|
|
355
|
+
readonly state: () => State
|
|
356
|
+
readonly completeMessage: (delivery: AcknowledgedDelivery<State>) => void
|
|
357
|
+
readonly commit: (state: State) => Effect.Effect<void> | undefined
|
|
358
|
+
/**
|
|
359
|
+
* Publishes the current synchronous segment before continuing with work that
|
|
360
|
+
* may suspend, run user effects, or make the committed state observable.
|
|
361
|
+
*/
|
|
362
|
+
readonly runAfterChanges: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>
|
|
363
|
+
executionState: unknown
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
type CompiledProcessInitial<State, Output> =
|
|
367
|
+
| { readonly state: State; readonly done: false; readonly output: undefined }
|
|
368
|
+
| { readonly state: State; readonly done: true; readonly output: Output }
|
|
369
|
+
| {
|
|
370
|
+
readonly state: State
|
|
371
|
+
readonly done: boolean
|
|
372
|
+
readonly output: Output | undefined
|
|
373
|
+
readonly executionState: unknown
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export type CompiledProcessDrain<State, Event, Error, Requirements, Output> =
|
|
377
|
+
| {
|
|
378
|
+
readonly _tag: "Process"
|
|
379
|
+
readonly run: (
|
|
380
|
+
context: ProcessContext<State, Event>
|
|
381
|
+
) => Effect.Effect<Option.Option<Output>, Error, Requirements>
|
|
382
|
+
}
|
|
383
|
+
| {
|
|
384
|
+
readonly _tag: "Owned"
|
|
385
|
+
readonly run: (
|
|
386
|
+
context: CompiledProcessContext<State, Event>
|
|
387
|
+
) => Effect.Effect<Option.Option<Output>, Error, Requirements>
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* The complete capability descriptor consumed by the compact process runtime.
|
|
392
|
+
* Generic process logic omits this field entirely.
|
|
393
|
+
*
|
|
394
|
+
* @internal
|
|
395
|
+
*/
|
|
396
|
+
export interface CompiledProcessExecution<
|
|
397
|
+
State,
|
|
398
|
+
Event,
|
|
399
|
+
Error,
|
|
400
|
+
Requirements,
|
|
401
|
+
Output,
|
|
402
|
+
InitialError
|
|
403
|
+
> {
|
|
404
|
+
readonly _tag: "Compiled"
|
|
405
|
+
readonly childless: boolean
|
|
406
|
+
readonly initial?: (
|
|
407
|
+
scope: ProcessScope<Event>
|
|
408
|
+
) => Effect.Effect<CompiledProcessInitial<State, Output>, InitialError, Requirements>
|
|
409
|
+
readonly initialSync?: (
|
|
410
|
+
scope: ProcessScope<Event>
|
|
411
|
+
) => CompiledProcessInitial<State, Output>
|
|
412
|
+
readonly drain: CompiledProcessDrain<State, Event, Error, Requirements, Output>
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export type ProcessExecution<State, Event, Error, Requirements, Output, InitialError> =
|
|
416
|
+
| {
|
|
417
|
+
readonly _tag: "Childless"
|
|
418
|
+
}
|
|
419
|
+
| CompiledProcessExecution<State, Event, Error, Requirements, Output, InitialError>
|
|
420
|
+
|
|
421
|
+
const executionIsChildless = (
|
|
422
|
+
execution: ProcessExecution<any, any, any, any, any, any> | undefined
|
|
423
|
+
): boolean => execution?._tag === "Childless" || execution?.childless === true
|
|
424
|
+
|
|
425
|
+
interface CompactProcessMailbox<Event> {
|
|
426
|
+
items: Array<ProcessMessage<Event>> | undefined
|
|
427
|
+
index: number
|
|
428
|
+
closed: boolean
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const offerCompactMailbox = <Event>(mailbox: CompactProcessMailbox<Event>, event: ProcessMessage<Event>): void => {
|
|
432
|
+
const items = mailbox.items ?? []
|
|
433
|
+
mailbox.items = items
|
|
434
|
+
items.push(event)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const pollCompactMailbox = <Event>(mailbox: CompactProcessMailbox<Event>): Option.Option<ProcessMessage<Event>> => {
|
|
438
|
+
if (mailbox.items === undefined) {
|
|
439
|
+
return Option.none()
|
|
440
|
+
}
|
|
441
|
+
const event = mailbox.items[mailbox.index]!
|
|
442
|
+
mailbox.index += 1
|
|
443
|
+
if (mailbox.index === mailbox.items.length) {
|
|
444
|
+
mailbox.items = undefined
|
|
445
|
+
mailbox.index = 0
|
|
446
|
+
}
|
|
447
|
+
return Option.some(event)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const closeCompactMailbox = (mailbox: CompactProcessMailbox<unknown>): void => {
|
|
451
|
+
if (mailbox.items !== undefined) {
|
|
452
|
+
for (let index = mailbox.index; index < mailbox.items.length; index += 1) {
|
|
453
|
+
stopAcknowledgedMessage(mailbox.items[index])
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
mailbox.closed = true
|
|
457
|
+
mailbox.items = undefined
|
|
458
|
+
mailbox.index = 0
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export interface ProcessLogic<
|
|
462
|
+
State,
|
|
463
|
+
Event,
|
|
464
|
+
out Error = never,
|
|
465
|
+
out Requirements = never,
|
|
466
|
+
out Output = never,
|
|
467
|
+
out InitialError = never
|
|
468
|
+
> {
|
|
469
|
+
/** @internal */
|
|
470
|
+
readonly execution?: ProcessExecution<State, Event, Error, Requirements, Output, InitialError>
|
|
471
|
+
initial(scope: ProcessScope<Event>): Effect.Effect<State, InitialError, Requirements>
|
|
472
|
+
run(context: ProcessContext<State, Event>): Effect.Effect<Output, Error, Requirements>
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export interface ProcessSpawn {
|
|
476
|
+
<ChildState, ChildEvent, ChildError, ChildRequirements, ChildOutput, ChildInitialError = never>(
|
|
477
|
+
logic: ProcessLogic<ChildState, ChildEvent, ChildError, ChildRequirements, ChildOutput, ChildInitialError>
|
|
478
|
+
): Effect.Effect<
|
|
479
|
+
MachineRef<ChildState, ChildEvent, ChildError, ChildOutput>,
|
|
480
|
+
ChildInitialError,
|
|
481
|
+
Exclude<ChildRequirements, Scope.Scope>
|
|
482
|
+
>
|
|
483
|
+
<ChildState, ChildEvent, ChildError, ChildRequirements, ChildOutput, ChildInitialError = never>(
|
|
484
|
+
logic: ProcessLogic<ChildState, ChildEvent, ChildError, ChildRequirements, ChildOutput, ChildInitialError>,
|
|
485
|
+
options: {
|
|
486
|
+
readonly id: string
|
|
487
|
+
readonly descriptor?: ChildDescriptor
|
|
488
|
+
readonly onOutcome?: (
|
|
489
|
+
outcome: RuntimeOutcome<ChildState, ChildError, ChildOutput>
|
|
490
|
+
) => Effect.Effect<void>
|
|
491
|
+
readonly [activeSnapshotObserver]?: (
|
|
492
|
+
snapshot: Extract<RuntimeSnapshot<ChildState, ChildError, ChildOutput>, { readonly status: "active" }>
|
|
493
|
+
) => Effect.Effect<void>
|
|
494
|
+
readonly [sendParentOverride]?: (event: unknown) => Effect.Effect<void, StoppedError>
|
|
495
|
+
}
|
|
496
|
+
): Effect.Effect<
|
|
497
|
+
MachineRef<ChildState, ChildEvent, ChildError, ChildOutput>,
|
|
498
|
+
ChildAlreadyExistsError | ChildInitialError,
|
|
499
|
+
Exclude<ChildRequirements, Scope.Scope>
|
|
500
|
+
>
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export class MachineRuntime extends Context.Service<MachineRuntime, ProcessScope<any>>()(
|
|
504
|
+
"effect/Machine/MachineRuntime"
|
|
505
|
+
) {}
|
|
506
|
+
|
|
507
|
+
export const provideMachineRuntime = <A, E, R, Event>(
|
|
508
|
+
effect: Effect.Effect<A, E, R>,
|
|
509
|
+
scope: ProcessScope<Event>
|
|
510
|
+
): Effect.Effect<A, E, Exclude<R, MachineRuntime>> =>
|
|
511
|
+
Effect.provideService(effect, MachineRuntime, scope as ProcessScope<any>)
|
|
512
|
+
|
|
513
|
+
const classifyOutcome = <State, Error, Output>(
|
|
514
|
+
snapshot: RuntimeSnapshot<State, Error, Output>
|
|
515
|
+
): RuntimeOutcome<State, Error, Output> | undefined => {
|
|
516
|
+
switch (snapshot.status) {
|
|
517
|
+
case "active": {
|
|
518
|
+
return undefined
|
|
519
|
+
}
|
|
520
|
+
case "done": {
|
|
521
|
+
return {
|
|
522
|
+
_tag: "Done",
|
|
523
|
+
output: snapshot.output,
|
|
524
|
+
snapshot
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
case "error": {
|
|
528
|
+
const failure = snapshot.cause.reasons.find(Cause.isFailReason)
|
|
529
|
+
if (failure !== undefined) {
|
|
530
|
+
return {
|
|
531
|
+
_tag: "Failure",
|
|
532
|
+
error: failure.error,
|
|
533
|
+
cause: snapshot.cause,
|
|
534
|
+
snapshot
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
const defect = snapshot.cause.reasons.find(Cause.isDieReason)
|
|
538
|
+
if (defect !== undefined) {
|
|
539
|
+
return {
|
|
540
|
+
_tag: "Defect",
|
|
541
|
+
defect: defect.defect,
|
|
542
|
+
cause: snapshot.cause,
|
|
543
|
+
snapshot
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const interrupted = snapshot.cause.reasons.find(Cause.isInterruptReason)
|
|
547
|
+
if (interrupted !== undefined) {
|
|
548
|
+
return {
|
|
549
|
+
_tag: "Interrupted",
|
|
550
|
+
cause: snapshot.cause,
|
|
551
|
+
snapshot
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return {
|
|
555
|
+
_tag: "Cause",
|
|
556
|
+
cause: snapshot.cause,
|
|
557
|
+
snapshot
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
case "stopped": {
|
|
561
|
+
return {
|
|
562
|
+
_tag: "Stopped",
|
|
563
|
+
snapshot
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const notifyActiveSnapshot = <State, Error, Output>(
|
|
570
|
+
onSnapshot: (
|
|
571
|
+
snapshot: Extract<RuntimeSnapshot<State, Error, Output>, { readonly status: "active" }>
|
|
572
|
+
) => Effect.Effect<void>,
|
|
573
|
+
snapshot: Extract<RuntimeSnapshot<State, Error, Output>, { readonly status: "active" }>
|
|
574
|
+
): Effect.Effect<void> =>
|
|
575
|
+
Effect.suspend(() => onSnapshot(snapshot)).pipe(
|
|
576
|
+
Effect.exit,
|
|
577
|
+
Effect.asVoid
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
export const watch = <State, Event, Error = never, Output = never>(
|
|
581
|
+
ref: MachineRef<State, Event, Error, Output>
|
|
582
|
+
): Stream.Stream<RuntimeOutcome<State, Error, Output>> =>
|
|
583
|
+
ref.changes.pipe(
|
|
584
|
+
Stream.filter((snapshot) => snapshot.status !== "active"),
|
|
585
|
+
Stream.map((snapshot) => classifyOutcome(snapshot)!),
|
|
586
|
+
Stream.take(1)
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
interface ProcessRuntime {
|
|
590
|
+
readonly nextSessionId: Effect.Effect<string>
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const makeProcessRuntime: Effect.Effect<ProcessRuntime> = Effect.sync(() => {
|
|
594
|
+
let sessionIdCounter = 0
|
|
595
|
+
return {
|
|
596
|
+
nextSessionId: Effect.sync(() => `machine:${sessionIdCounter++}`)
|
|
597
|
+
}
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
interface StartInternalOptions {
|
|
601
|
+
readonly detached?: boolean
|
|
602
|
+
readonly id?: string
|
|
603
|
+
readonly onOutcome?: (outcome: RuntimeOutcome<any, any, any>) => Effect.Effect<void>
|
|
604
|
+
readonly onSnapshot?: (
|
|
605
|
+
snapshot: Extract<RuntimeSnapshot<any, any, any>, { readonly status: "active" }>
|
|
606
|
+
) => Effect.Effect<void>
|
|
607
|
+
readonly onReady?: (
|
|
608
|
+
ref: MachineRef<any, any, any, any>,
|
|
609
|
+
requestStop: Effect.Effect<void>
|
|
610
|
+
) => Effect.Effect<void>
|
|
611
|
+
readonly onReadySync?: (ref: MachineRef<any, any, any, any>) => boolean
|
|
612
|
+
readonly onStop?: Effect.Effect<void>
|
|
613
|
+
readonly onStopSync?: () => void
|
|
614
|
+
readonly skipStoppedOutcome?: boolean
|
|
615
|
+
readonly parent?: ProcessAddress<unknown>
|
|
616
|
+
readonly runtime: ProcessRuntime
|
|
617
|
+
readonly sendParent?: (event: unknown) => Effect.Effect<void, StoppedError>
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/** @internal */
|
|
621
|
+
export interface OwnedChildSpawnOptions {
|
|
622
|
+
readonly key: string
|
|
623
|
+
readonly path: string
|
|
624
|
+
readonly id: string
|
|
625
|
+
readonly duplicateId: string
|
|
626
|
+
readonly descriptor?: ChildDescriptor
|
|
627
|
+
readonly onOutcome: (
|
|
628
|
+
isCurrent: () => boolean,
|
|
629
|
+
outcome: RuntimeOutcome<any, any, any>
|
|
630
|
+
) => Effect.Effect<void>
|
|
631
|
+
readonly onSnapshot?: (
|
|
632
|
+
isCurrent: () => boolean,
|
|
633
|
+
snapshot: Extract<RuntimeSnapshot<any, any, any>, { readonly status: "active" }>
|
|
634
|
+
) => Effect.Effect<void>
|
|
635
|
+
readonly sendParent: (
|
|
636
|
+
isCurrent: () => boolean,
|
|
637
|
+
event: unknown
|
|
638
|
+
) => Effect.Effect<void, StoppedError>
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** @internal */
|
|
642
|
+
export interface OwnedChildRuntime {
|
|
643
|
+
readonly spawn: (
|
|
644
|
+
makeLogic: () => ProcessLogic<any, any, any, any, any, any>,
|
|
645
|
+
options: OwnedChildSpawnOptions
|
|
646
|
+
) => Effect.Effect<void, any, any>
|
|
647
|
+
readonly stopAll: () => Effect.Effect<void>
|
|
648
|
+
readonly stopPaths: (paths: ReadonlyArray<string>) => Effect.Effect<void> | undefined
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
interface ChildRuntime {
|
|
652
|
+
readonly close: <A, E>(exit: Exit.Exit<A, E>) => Effect.Effect<void>
|
|
653
|
+
readonly spawn: ProcessSpawn
|
|
654
|
+
readonly get: <ChildState, ChildEvent, ChildError, ChildOutput>(
|
|
655
|
+
child: ChildSelector
|
|
656
|
+
) => Effect.Effect<Option.Option<MachineRef<ChildState, ChildEvent, ChildError, ChildOutput>>>
|
|
657
|
+
readonly changes: <ChildState, ChildEvent, ChildError, ChildOutput>(
|
|
658
|
+
child: ChildSelector
|
|
659
|
+
) => Stream.Stream<Option.Option<MachineRef<ChildState, ChildEvent, ChildError, ChildOutput>>>
|
|
660
|
+
readonly sendTo: (child: ChildSelector, event: unknown) => Effect.Effect<void, StoppedError>
|
|
661
|
+
readonly stop: (child: ChildSelector) => Effect.Effect<void>
|
|
662
|
+
readonly owned: OwnedChildRuntime
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
class OwnedChildRuntimeImpl implements OwnedChildRuntime {
|
|
666
|
+
private scopedServices: Context.Context<any> | undefined
|
|
667
|
+
|
|
668
|
+
constructor(
|
|
669
|
+
private readonly registry: ChildRegistry,
|
|
670
|
+
private readonly self: ProcessAddress<any>,
|
|
671
|
+
private readonly runtime: ProcessRuntime,
|
|
672
|
+
private readonly services?: Context.Context<any>
|
|
673
|
+
) {}
|
|
674
|
+
|
|
675
|
+
private has(key: string): boolean {
|
|
676
|
+
for (const entry of this.registry.children.values()) {
|
|
677
|
+
if (entry.ownerActive && entry.ownerKey === key) return true
|
|
678
|
+
}
|
|
679
|
+
return false
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
private stopEntry(entry: ChildEntry): Effect.Effect<void> {
|
|
683
|
+
entry.ownerActive = false
|
|
684
|
+
return entry._tag === "Started" ? entry.ref.stop : Effect.void
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
spawn(
|
|
688
|
+
makeLogic: () => ProcessLogic<any, any, any, any, any, any>,
|
|
689
|
+
options: OwnedChildSpawnOptions
|
|
690
|
+
): Effect.Effect<void, any, any> {
|
|
691
|
+
const token = Symbol()
|
|
692
|
+
let startedChild: MachineRef<any, any, any, any> | undefined
|
|
693
|
+
const isCurrent = (): boolean => {
|
|
694
|
+
const entry = this.registry.children.get(options.id)
|
|
695
|
+
return entry?.token === token && entry.ownerKey === options.key && entry.ownerActive === true
|
|
696
|
+
}
|
|
697
|
+
return Effect.suspend(() => {
|
|
698
|
+
if (this.registry.closed) return Effect.interrupt
|
|
699
|
+
if (this.has(options.key) || this.registry.children.has(options.id)) {
|
|
700
|
+
return Effect.fail(new ChildAlreadyExistsError({ id: options.duplicateId }))
|
|
701
|
+
}
|
|
702
|
+
const logic = makeLogic()
|
|
703
|
+
const scope = this.registry.scope ??= Scope.makeUnsafe("parallel")
|
|
704
|
+
this.registry.children.set(options.id, {
|
|
705
|
+
_tag: "Starting",
|
|
706
|
+
token,
|
|
707
|
+
ownerKey: options.key,
|
|
708
|
+
ownerPath: options.path,
|
|
709
|
+
ownerActive: true
|
|
710
|
+
})
|
|
711
|
+
const startOptions: StartInternalOptions = {
|
|
712
|
+
detached: true,
|
|
713
|
+
id: options.id,
|
|
714
|
+
sendParent: (event) => options.sendParent(isCurrent, event),
|
|
715
|
+
onOutcome: (outcome) => options.onOutcome(isCurrent, outcome),
|
|
716
|
+
...(options.onSnapshot === undefined
|
|
717
|
+
? undefined
|
|
718
|
+
: { onSnapshot: (snapshot) => options.onSnapshot!(isCurrent, snapshot) }),
|
|
719
|
+
onReadySync: (child) => {
|
|
720
|
+
startedChild = child
|
|
721
|
+
return registerChild(this.registry, options.id, token, child, options.descriptor)
|
|
722
|
+
},
|
|
723
|
+
onStopSync: () => unregisterChild(this.registry, options.id, token),
|
|
724
|
+
skipStoppedOutcome: true,
|
|
725
|
+
parent: this.self,
|
|
726
|
+
runtime: this.runtime
|
|
727
|
+
}
|
|
728
|
+
const execution = logic.execution
|
|
729
|
+
const synchronous = this.services !== undefined && options.onSnapshot === undefined &&
|
|
730
|
+
execution?._tag === "Compiled" && execution.childless && execution.drain._tag === "Owned" &&
|
|
731
|
+
execution.initialSync !== undefined
|
|
732
|
+
const start = synchronous
|
|
733
|
+
? Effect.flatMap(
|
|
734
|
+
this.runtime.nextSessionId,
|
|
735
|
+
(sessionId) =>
|
|
736
|
+
new CompiledProcess(
|
|
737
|
+
logic,
|
|
738
|
+
startOptions,
|
|
739
|
+
this.scopedServices ??= Context.add(this.services!, Scope.Scope, scope),
|
|
740
|
+
sessionId
|
|
741
|
+
).initializeCompiledSync()
|
|
742
|
+
)
|
|
743
|
+
: startLogicInternal(logic, startOptions)
|
|
744
|
+
const guarded = start.pipe(
|
|
745
|
+
Effect.onExit((exit) => {
|
|
746
|
+
if (Exit.isSuccess(exit)) return Effect.void
|
|
747
|
+
unregisterChild(this.registry, options.id, token)
|
|
748
|
+
return startedChild === undefined ? Effect.void : startedChild.stop
|
|
749
|
+
})
|
|
750
|
+
)
|
|
751
|
+
return (synchronous ? guarded : Scope.provide(guarded, scope)).pipe(Effect.asVoid)
|
|
752
|
+
})
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
stopAll(): Effect.Effect<void> {
|
|
756
|
+
return Effect.suspend(() => {
|
|
757
|
+
const effects: Array<Effect.Effect<void>> = []
|
|
758
|
+
for (const entry of this.registry.children.values()) {
|
|
759
|
+
if (entry.ownerActive) effects.push(this.stopEntry(entry))
|
|
760
|
+
}
|
|
761
|
+
return effects.length === 0
|
|
762
|
+
? Effect.void
|
|
763
|
+
: effects.length === 1
|
|
764
|
+
? effects[0]!
|
|
765
|
+
: Effect.all(effects, { concurrency: "unbounded", discard: true })
|
|
766
|
+
})
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
stopPaths(paths: ReadonlyArray<string>): Effect.Effect<void> | undefined {
|
|
770
|
+
if (paths.length === 0) return undefined
|
|
771
|
+
const pathSet = new Set(paths)
|
|
772
|
+
const effects: Array<Effect.Effect<void>> = []
|
|
773
|
+
for (const entry of this.registry.children.values()) {
|
|
774
|
+
if (entry.ownerActive && entry.ownerPath !== undefined && pathSet.has(entry.ownerPath)) {
|
|
775
|
+
effects.push(this.stopEntry(entry))
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return effects.length === 0
|
|
779
|
+
? undefined
|
|
780
|
+
: effects.length === 1
|
|
781
|
+
? effects[0]!
|
|
782
|
+
: Effect.all(effects, { concurrency: "unbounded", discard: true })
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const noChildChanges = Stream.succeed(Option.none()).pipe(Stream.concat(Stream.never))
|
|
787
|
+
const noParentSend = (_event: unknown): Effect.Effect<void, StoppedError> => Effect.void
|
|
788
|
+
|
|
789
|
+
const childlessRuntime: ChildRuntime = {
|
|
790
|
+
close: () => Effect.void,
|
|
791
|
+
spawn: (() => Effect.die(new Error("Childless machine logic cannot spawn a process"))) as ProcessSpawn,
|
|
792
|
+
get: () => Effect.succeed(Option.none()),
|
|
793
|
+
changes: () => noChildChanges,
|
|
794
|
+
sendTo: () => Effect.void,
|
|
795
|
+
stop: () => Effect.void,
|
|
796
|
+
owned: {
|
|
797
|
+
spawn: () => Effect.die(new Error("Childless machine logic cannot spawn an owned process")),
|
|
798
|
+
stopAll: () => Effect.void,
|
|
799
|
+
stopPaths: () => undefined
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const makeChildRuntimeSync = (
|
|
804
|
+
self: ProcessAddress<any>,
|
|
805
|
+
runtime: ProcessRuntime,
|
|
806
|
+
services?: Context.Context<any>
|
|
807
|
+
): ChildRuntime => {
|
|
808
|
+
// Child-registry decisions are synchronous and every access below runs in
|
|
809
|
+
// one Effect.sync / Effect.suspend step. Keep the unobserved representation
|
|
810
|
+
// compact; selector-specific handoffs are installed only while
|
|
811
|
+
// childChanges streams are running.
|
|
812
|
+
const registry: ChildRegistry = {
|
|
813
|
+
closed: false,
|
|
814
|
+
children: new Map(),
|
|
815
|
+
observers: undefined,
|
|
816
|
+
scope: undefined
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const close = <A, E>(exit: Exit.Exit<A, E>): Effect.Effect<void> =>
|
|
820
|
+
Effect.suspend(() => {
|
|
821
|
+
if (registry.closed) {
|
|
822
|
+
return Effect.void
|
|
823
|
+
}
|
|
824
|
+
registry.closed = true
|
|
825
|
+
if (registry.scope === undefined) {
|
|
826
|
+
return Effect.void
|
|
827
|
+
}
|
|
828
|
+
const finalizers = Scope.closeUnsafe(registry.scope, exit)
|
|
829
|
+
let first: Effect.Effect<void> | undefined
|
|
830
|
+
let rest: Array<Effect.Effect<void>> | undefined
|
|
831
|
+
for (const entry of registry.children.values()) {
|
|
832
|
+
if (entry._tag !== "Started") {
|
|
833
|
+
continue
|
|
834
|
+
}
|
|
835
|
+
if (first === undefined) {
|
|
836
|
+
first = entry.ref.stop
|
|
837
|
+
} else {
|
|
838
|
+
rest ??= [first]
|
|
839
|
+
rest.push(entry.ref.stop)
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
if (finalizers !== undefined) {
|
|
843
|
+
if (first === undefined) {
|
|
844
|
+
first = finalizers
|
|
845
|
+
} else {
|
|
846
|
+
rest ??= [first]
|
|
847
|
+
rest.push(finalizers)
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
const cleanup = rest ?? first
|
|
851
|
+
return cleanup === undefined
|
|
852
|
+
? Effect.void
|
|
853
|
+
: Array.isArray(cleanup)
|
|
854
|
+
? Effect.all(cleanup, { concurrency: "unbounded", discard: true })
|
|
855
|
+
: cleanup
|
|
856
|
+
})
|
|
857
|
+
|
|
858
|
+
const unregister = (
|
|
859
|
+
key: ChildKey,
|
|
860
|
+
token: symbol
|
|
861
|
+
): Effect.Effect<void> => Effect.sync(() => unregisterChild(registry, key, token))
|
|
862
|
+
|
|
863
|
+
const register = (
|
|
864
|
+
key: ChildKey,
|
|
865
|
+
token: symbol,
|
|
866
|
+
ref: MachineRef<any, any, any, any>,
|
|
867
|
+
descriptor: ChildDescriptor | undefined
|
|
868
|
+
): Effect.Effect<boolean> => Effect.sync(() => registerChild(registry, key, token, ref, descriptor))
|
|
869
|
+
|
|
870
|
+
const get: ChildRuntime["get"] = (child) => {
|
|
871
|
+
const id = typeof child === "string" ? child : child.id
|
|
872
|
+
return Effect.sync(() => {
|
|
873
|
+
if (registry.closed) {
|
|
874
|
+
return Option.none()
|
|
875
|
+
}
|
|
876
|
+
const entry = registry.children.get(id)
|
|
877
|
+
return entry !== undefined && matchesChild(entry, child)
|
|
878
|
+
? Option.some(entry.ref)
|
|
879
|
+
: Option.none()
|
|
880
|
+
})
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const changes: ChildRuntime["changes"] = (child) => {
|
|
884
|
+
const id = typeof child === "string" ? child : child.id
|
|
885
|
+
return Stream.fromChannel(
|
|
886
|
+
Channel.fromTransform((_, streamScope) =>
|
|
887
|
+
Effect.sync((): ChildObserver => ({ child, id, values: undefined, waiter: undefined })).pipe(
|
|
888
|
+
Effect.flatMap((observer) => {
|
|
889
|
+
const removeObserver = Effect.sync(() => {
|
|
890
|
+
if (registry.observers !== undefined) {
|
|
891
|
+
registry.observers.delete(observer)
|
|
892
|
+
if (registry.observers.size === 0) {
|
|
893
|
+
registry.observers = undefined
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
observer.values = undefined
|
|
897
|
+
observer.waiter = undefined
|
|
898
|
+
})
|
|
899
|
+
return Scope.addFinalizer(streamScope, removeObserver).pipe(
|
|
900
|
+
Effect.andThen(
|
|
901
|
+
Effect.sync(() => {
|
|
902
|
+
if (!registry.closed && streamScope.state._tag !== "Closed") {
|
|
903
|
+
registry.observers ??= new Set()
|
|
904
|
+
registry.observers.add(observer)
|
|
905
|
+
}
|
|
906
|
+
offerChildObservation(observer, selectRegistryChild(registry, id, child))
|
|
907
|
+
})
|
|
908
|
+
),
|
|
909
|
+
Effect.as(takeChildObservations(observer))
|
|
910
|
+
)
|
|
911
|
+
})
|
|
912
|
+
)
|
|
913
|
+
)
|
|
914
|
+
)
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
const sendTo = (child: ChildSelector, event: unknown): Effect.Effect<void, StoppedError> => {
|
|
918
|
+
const id = typeof child === "string" ? child : child.id
|
|
919
|
+
return Effect.suspend(() => {
|
|
920
|
+
if (registry.closed) {
|
|
921
|
+
return Effect.void
|
|
922
|
+
}
|
|
923
|
+
const entry = registry.children.get(id)
|
|
924
|
+
return entry !== undefined && matchesChild(entry, child)
|
|
925
|
+
? entry.ref.send(event)
|
|
926
|
+
: Effect.void
|
|
927
|
+
})
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const stop = (child: ChildSelector): Effect.Effect<void> => {
|
|
931
|
+
const id = typeof child === "string" ? child : child.id
|
|
932
|
+
return Effect.suspend(() => {
|
|
933
|
+
if (registry.closed) {
|
|
934
|
+
return Effect.void
|
|
935
|
+
}
|
|
936
|
+
const entry = registry.children.get(id)
|
|
937
|
+
return entry !== undefined && matchesChild(entry, child)
|
|
938
|
+
? entry.ref.stop
|
|
939
|
+
: Effect.void
|
|
940
|
+
})
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function spawn<ChildState, ChildEvent, ChildError, ChildRequirements, ChildOutput, ChildInitialError = never>(
|
|
944
|
+
logic: ProcessLogic<ChildState, ChildEvent, ChildError, ChildRequirements, ChildOutput, ChildInitialError>
|
|
945
|
+
): Effect.Effect<
|
|
946
|
+
MachineRef<ChildState, ChildEvent, ChildError, ChildOutput>,
|
|
947
|
+
ChildInitialError,
|
|
948
|
+
Exclude<ChildRequirements, Scope.Scope>
|
|
949
|
+
>
|
|
950
|
+
function spawn<ChildState, ChildEvent, ChildError, ChildRequirements, ChildOutput, ChildInitialError = never>(
|
|
951
|
+
logic: ProcessLogic<ChildState, ChildEvent, ChildError, ChildRequirements, ChildOutput, ChildInitialError>,
|
|
952
|
+
spawnOptions: {
|
|
953
|
+
readonly id: string
|
|
954
|
+
readonly descriptor?: ChildDescriptor
|
|
955
|
+
readonly onOutcome?: (
|
|
956
|
+
outcome: RuntimeOutcome<ChildState, ChildError, ChildOutput>
|
|
957
|
+
) => Effect.Effect<void>
|
|
958
|
+
readonly [activeSnapshotObserver]?: (
|
|
959
|
+
snapshot: Extract<RuntimeSnapshot<ChildState, ChildError, ChildOutput>, { readonly status: "active" }>
|
|
960
|
+
) => Effect.Effect<void>
|
|
961
|
+
readonly [sendParentOverride]?: (event: unknown) => Effect.Effect<void, StoppedError>
|
|
962
|
+
}
|
|
963
|
+
): Effect.Effect<
|
|
964
|
+
MachineRef<ChildState, ChildEvent, ChildError, ChildOutput>,
|
|
965
|
+
ChildAlreadyExistsError | ChildInitialError,
|
|
966
|
+
Exclude<ChildRequirements, Scope.Scope>
|
|
967
|
+
>
|
|
968
|
+
function spawn<ChildState, ChildEvent, ChildError, ChildRequirements, ChildOutput, ChildInitialError = never>(
|
|
969
|
+
logic: ProcessLogic<ChildState, ChildEvent, ChildError, ChildRequirements, ChildOutput, ChildInitialError>,
|
|
970
|
+
spawnOptions?: {
|
|
971
|
+
readonly id: string
|
|
972
|
+
readonly descriptor?: ChildDescriptor
|
|
973
|
+
readonly onOutcome?: (
|
|
974
|
+
outcome: RuntimeOutcome<ChildState, ChildError, ChildOutput>
|
|
975
|
+
) => Effect.Effect<void>
|
|
976
|
+
readonly [activeSnapshotObserver]?: (
|
|
977
|
+
snapshot: Extract<RuntimeSnapshot<ChildState, ChildError, ChildOutput>, { readonly status: "active" }>
|
|
978
|
+
) => Effect.Effect<void>
|
|
979
|
+
readonly [sendParentOverride]?: (event: unknown) => Effect.Effect<void, StoppedError>
|
|
980
|
+
}
|
|
981
|
+
): Effect.Effect<
|
|
982
|
+
MachineRef<ChildState, ChildEvent, ChildError, ChildOutput>,
|
|
983
|
+
ChildAlreadyExistsError | ChildInitialError,
|
|
984
|
+
Exclude<ChildRequirements, Scope.Scope>
|
|
985
|
+
> {
|
|
986
|
+
const token = Symbol()
|
|
987
|
+
const key = spawnOptions?.id ?? token
|
|
988
|
+
let startedChild: MachineRef<any, any, any, any> | undefined
|
|
989
|
+
return Effect.suspend((): Effect.Effect<
|
|
990
|
+
MachineRef<ChildState, ChildEvent, ChildError, ChildOutput>,
|
|
991
|
+
ChildAlreadyExistsError | ChildInitialError,
|
|
992
|
+
Exclude<ChildRequirements, Scope.Scope>
|
|
993
|
+
> => {
|
|
994
|
+
if (registry.closed) {
|
|
995
|
+
return Effect.interrupt
|
|
996
|
+
}
|
|
997
|
+
if (typeof key === "string" && registry.children.has(key)) {
|
|
998
|
+
return Effect.fail(new ChildAlreadyExistsError({ id: key }))
|
|
999
|
+
}
|
|
1000
|
+
registry.scope ??= Scope.makeUnsafe("parallel")
|
|
1001
|
+
registry.children.set(key, { _tag: "Starting", token })
|
|
1002
|
+
return startLogicInternal(logic, {
|
|
1003
|
+
detached: true,
|
|
1004
|
+
...(spawnOptions?.id === undefined ? undefined : { id: spawnOptions.id }),
|
|
1005
|
+
...(spawnOptions?.onOutcome === undefined ? undefined : { onOutcome: spawnOptions.onOutcome }),
|
|
1006
|
+
...(spawnOptions?.[activeSnapshotObserver] === undefined
|
|
1007
|
+
? undefined
|
|
1008
|
+
: { onSnapshot: spawnOptions[activeSnapshotObserver] }),
|
|
1009
|
+
...(spawnOptions?.[sendParentOverride] === undefined
|
|
1010
|
+
? undefined
|
|
1011
|
+
: { sendParent: spawnOptions[sendParentOverride] }),
|
|
1012
|
+
onReady: (child, requestChildStop) =>
|
|
1013
|
+
Effect.sync(() => {
|
|
1014
|
+
startedChild = child
|
|
1015
|
+
}).pipe(
|
|
1016
|
+
Effect.andThen(register(key, token, child, spawnOptions?.descriptor)),
|
|
1017
|
+
Effect.flatMap((registered) => registered ? Effect.void : requestChildStop)
|
|
1018
|
+
),
|
|
1019
|
+
onStop: unregister(key, token),
|
|
1020
|
+
parent: self,
|
|
1021
|
+
runtime
|
|
1022
|
+
}).pipe(
|
|
1023
|
+
Effect.onExit((exit) =>
|
|
1024
|
+
Exit.isFailure(exit)
|
|
1025
|
+
? unregister(key, token).pipe(
|
|
1026
|
+
Effect.andThen(startedChild === undefined ? Effect.void : startedChild.stop)
|
|
1027
|
+
)
|
|
1028
|
+
: Effect.void
|
|
1029
|
+
),
|
|
1030
|
+
Scope.provide(registry.scope)
|
|
1031
|
+
)
|
|
1032
|
+
})
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
return {
|
|
1036
|
+
close,
|
|
1037
|
+
spawn,
|
|
1038
|
+
get,
|
|
1039
|
+
changes,
|
|
1040
|
+
sendTo,
|
|
1041
|
+
stop,
|
|
1042
|
+
owned: new OwnedChildRuntimeImpl(registry, self, runtime, services)
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const makeChildRuntime = (
|
|
1047
|
+
self: ProcessAddress<any>,
|
|
1048
|
+
runtime: ProcessRuntime,
|
|
1049
|
+
services?: Context.Context<any>
|
|
1050
|
+
): Effect.Effect<ChildRuntime> => Effect.sync(() => makeChildRuntimeSync(self, runtime, services))
|
|
1051
|
+
|
|
1052
|
+
// `Machine.logic` permits an arbitrary Effect program, including programs that
|
|
1053
|
+
// suspend or supervise their own fibers. Keep its two-fiber worker/supervisor
|
|
1054
|
+
// protocol as the general contract rather than weakening it for statecharts.
|
|
1055
|
+
const startGenericInternal: <
|
|
1056
|
+
State,
|
|
1057
|
+
Event,
|
|
1058
|
+
Error = never,
|
|
1059
|
+
Requirements = never,
|
|
1060
|
+
Output = never,
|
|
1061
|
+
InitialError = never
|
|
1062
|
+
>(
|
|
1063
|
+
logic: ProcessLogic<State, Event, Error, Requirements, Output, InitialError>,
|
|
1064
|
+
options: StartInternalOptions
|
|
1065
|
+
) => Effect.Effect<
|
|
1066
|
+
MachineRef<State, Event, Error, Output>,
|
|
1067
|
+
InitialError,
|
|
1068
|
+
Requirements
|
|
1069
|
+
> = Effect.fnUntraced(function*<State, Event, Error, Requirements, Output, InitialError>(
|
|
1070
|
+
logic: ProcessLogic<State, Event, Error, Requirements, Output, InitialError>,
|
|
1071
|
+
options: StartInternalOptions
|
|
1072
|
+
) {
|
|
1073
|
+
const {
|
|
1074
|
+
detached,
|
|
1075
|
+
id: requestedId,
|
|
1076
|
+
onOutcome,
|
|
1077
|
+
onReady,
|
|
1078
|
+
onReadySync,
|
|
1079
|
+
onSnapshot,
|
|
1080
|
+
onStop,
|
|
1081
|
+
onStopSync,
|
|
1082
|
+
parent,
|
|
1083
|
+
runtime,
|
|
1084
|
+
sendParent: overrideSendParent
|
|
1085
|
+
} = options
|
|
1086
|
+
type ProcessTermination =
|
|
1087
|
+
| { readonly _tag: "Stopped" }
|
|
1088
|
+
| { readonly _tag: "Done"; readonly output: Output }
|
|
1089
|
+
| { readonly _tag: "Failure"; readonly cause: Cause.Cause<Error> }
|
|
1090
|
+
|
|
1091
|
+
const sessionId = yield* runtime.nextSessionId
|
|
1092
|
+
const id = requestedId ?? sessionId
|
|
1093
|
+
const queue = yield* Queue.unbounded<ProcessMessage<Event>>()
|
|
1094
|
+
const termination = yield* Deferred.make<ProcessTermination>()
|
|
1095
|
+
const done = yield* Deferred.make<Output, Error | StoppedError>()
|
|
1096
|
+
const awaitCompletion = Deferred.await(done).pipe(Effect.exit, Effect.asVoid)
|
|
1097
|
+
let initializing = true
|
|
1098
|
+
let inFlightMessage: ProcessMessage<Event> | undefined
|
|
1099
|
+
const requestStop = Deferred.succeed(termination, { _tag: "Stopped" }).pipe(Effect.asVoid)
|
|
1100
|
+
const self: ProcessAddress<Event> = {
|
|
1101
|
+
id,
|
|
1102
|
+
sessionId,
|
|
1103
|
+
// Initialization must finish constructing a state before a stopped
|
|
1104
|
+
// snapshot can be published. A stop requested there is therefore recorded
|
|
1105
|
+
// and returns so initialization can finish. Once running, the requesting
|
|
1106
|
+
// process waits forever and is interrupted by the supervisor after the
|
|
1107
|
+
// stop request wins, so execution never continues after `self.stop`.
|
|
1108
|
+
stop: Effect.suspend(() =>
|
|
1109
|
+
initializing
|
|
1110
|
+
? requestStop
|
|
1111
|
+
: requestStop.pipe(Effect.andThen(Effect.never))
|
|
1112
|
+
),
|
|
1113
|
+
send: (event: Event) =>
|
|
1114
|
+
Queue.offer(queue, event).pipe(
|
|
1115
|
+
Effect.flatMap((accepted) => accepted ? Effect.void : Effect.fail(new StoppedError()))
|
|
1116
|
+
)
|
|
1117
|
+
}
|
|
1118
|
+
const sendAcknowledged:
|
|
1119
|
+
| ((event: Event) => Effect.Effect<AcknowledgedDelivery<State>, Error | StoppedError>)
|
|
1120
|
+
| undefined = logic.execution?._tag !== "Compiled"
|
|
1121
|
+
? undefined
|
|
1122
|
+
: (event) =>
|
|
1123
|
+
Effect.uninterruptibleMask((restore) =>
|
|
1124
|
+
Deferred.make<AcknowledgedDelivery<unknown>, unknown>().pipe(
|
|
1125
|
+
Effect.flatMap((deferred) =>
|
|
1126
|
+
Queue.offer(queue, {
|
|
1127
|
+
[AcknowledgedMessageTypeId]: true as const,
|
|
1128
|
+
event,
|
|
1129
|
+
deferred
|
|
1130
|
+
}).pipe(
|
|
1131
|
+
Effect.flatMap((accepted) =>
|
|
1132
|
+
accepted
|
|
1133
|
+
? restore(Deferred.await(deferred))
|
|
1134
|
+
: Effect.fail(new StoppedError())
|
|
1135
|
+
)
|
|
1136
|
+
)
|
|
1137
|
+
),
|
|
1138
|
+
Effect.map((delivery) => delivery as AcknowledgedDelivery<State>)
|
|
1139
|
+
)
|
|
1140
|
+
) as Effect.Effect<AcknowledgedDelivery<State>, Error | StoppedError>
|
|
1141
|
+
|
|
1142
|
+
let {
|
|
1143
|
+
changes: childChanges,
|
|
1144
|
+
close: closeChildren,
|
|
1145
|
+
get: getChild,
|
|
1146
|
+
owned: ownedChildren,
|
|
1147
|
+
sendTo,
|
|
1148
|
+
spawn,
|
|
1149
|
+
stop: stopChild
|
|
1150
|
+
} = childlessRuntime
|
|
1151
|
+
if (!executionIsChildless(logic.execution)) {
|
|
1152
|
+
;({
|
|
1153
|
+
changes: childChanges,
|
|
1154
|
+
close: closeChildren,
|
|
1155
|
+
get: getChild,
|
|
1156
|
+
owned: ownedChildren,
|
|
1157
|
+
sendTo,
|
|
1158
|
+
spawn,
|
|
1159
|
+
stop: stopChild
|
|
1160
|
+
} = yield* makeChildRuntime(self, runtime))
|
|
1161
|
+
}
|
|
1162
|
+
const cleanupStartupFailure = <A, E>(exit: Exit.Exit<A, E>): Effect.Effect<void> =>
|
|
1163
|
+
Exit.isFailure(exit)
|
|
1164
|
+
? closeChildren(exit)
|
|
1165
|
+
: Effect.void
|
|
1166
|
+
const cleanup = onStopSync === undefined ? onStop ?? Effect.void : Effect.sync(onStopSync)
|
|
1167
|
+
const sendParent = overrideSendParent ?? (parent === undefined ? noParentSend : parent.send)
|
|
1168
|
+
|
|
1169
|
+
const scope: ProcessScope<Event> = {
|
|
1170
|
+
self,
|
|
1171
|
+
parent,
|
|
1172
|
+
spawn,
|
|
1173
|
+
sendParent,
|
|
1174
|
+
sendTo,
|
|
1175
|
+
stopChild,
|
|
1176
|
+
failCause: (cause) =>
|
|
1177
|
+
Deferred.succeed(termination, {
|
|
1178
|
+
_tag: "Failure",
|
|
1179
|
+
cause: cause as Cause.Cause<Error>
|
|
1180
|
+
}).pipe(Effect.asVoid)
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
const initial = yield* logic.initial(scope).pipe(
|
|
1184
|
+
Effect.onExit(cleanupStartupFailure),
|
|
1185
|
+
Effect.ensuring(Effect.sync(() => {
|
|
1186
|
+
initializing = false
|
|
1187
|
+
}))
|
|
1188
|
+
)
|
|
1189
|
+
const current = yield* SynchronizedRef.make<VersionedSnapshot<State, Error, Output>>({
|
|
1190
|
+
revision: 0,
|
|
1191
|
+
terminalizing: false,
|
|
1192
|
+
changes: undefined,
|
|
1193
|
+
snapshot: {
|
|
1194
|
+
status: "active",
|
|
1195
|
+
state: initial
|
|
1196
|
+
}
|
|
1197
|
+
})
|
|
1198
|
+
const publishSnapshot: (
|
|
1199
|
+
snapshot: VersionedSnapshot<State, Error, Output>
|
|
1200
|
+
) => Effect.Effect<VersionedSnapshot<State, Error, Output>> = onSnapshot === undefined
|
|
1201
|
+
? (snapshot) =>
|
|
1202
|
+
snapshot.changes === undefined
|
|
1203
|
+
? Effect.succeed(snapshot)
|
|
1204
|
+
: PubSub.publish(snapshot.changes, [snapshot] as const).pipe(Effect.as(snapshot))
|
|
1205
|
+
: (snapshot) => {
|
|
1206
|
+
const publish = snapshot.changes === undefined
|
|
1207
|
+
? Effect.succeed(snapshot)
|
|
1208
|
+
: PubSub.publish(snapshot.changes, [snapshot] as const).pipe(Effect.as(snapshot))
|
|
1209
|
+
const runtimeSnapshot = snapshot.snapshot
|
|
1210
|
+
return runtimeSnapshot.status !== "active"
|
|
1211
|
+
? publish
|
|
1212
|
+
: publish.pipe(Effect.tap(() => notifyActiveSnapshot(onSnapshot, runtimeSnapshot)))
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
const completeChanges = (
|
|
1216
|
+
snapshot: VersionedSnapshot<State, Error, Output>
|
|
1217
|
+
): Effect.Effect<void> =>
|
|
1218
|
+
snapshot.changes === undefined
|
|
1219
|
+
? Effect.void
|
|
1220
|
+
: PubSub.publish(snapshot.changes, Exit.succeed<void>(undefined)).pipe(Effect.asVoid)
|
|
1221
|
+
|
|
1222
|
+
const completeIfTerminal = (
|
|
1223
|
+
snapshot: VersionedSnapshot<State, Error, Output>
|
|
1224
|
+
): Effect.Effect<VersionedSnapshot<State, Error, Output>> => {
|
|
1225
|
+
if (snapshot.snapshot.status === "active") {
|
|
1226
|
+
return Effect.succeed(snapshot)
|
|
1227
|
+
}
|
|
1228
|
+
return completeChanges(snapshot).pipe(Effect.as(snapshot))
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
const publishIfCurrent = (
|
|
1232
|
+
snapshot: VersionedSnapshot<State, Error, Output>
|
|
1233
|
+
): Effect.Effect<VersionedSnapshot<State, Error, Output> | undefined> =>
|
|
1234
|
+
SynchronizedRef.get(current).pipe(
|
|
1235
|
+
Effect.flatMap((
|
|
1236
|
+
currentSnapshot
|
|
1237
|
+
): Effect.Effect<VersionedSnapshot<State, Error, Output> | undefined> =>
|
|
1238
|
+
currentSnapshot.revision === snapshot.revision
|
|
1239
|
+
? publishSnapshot(snapshot).pipe(Effect.flatMap(completeIfTerminal))
|
|
1240
|
+
: Effect.succeed(undefined)
|
|
1241
|
+
)
|
|
1242
|
+
)
|
|
1243
|
+
|
|
1244
|
+
type SnapshotModification = readonly [
|
|
1245
|
+
VersionedSnapshot<State, Error, Output> | undefined,
|
|
1246
|
+
VersionedSnapshot<State, Error, Output>
|
|
1247
|
+
]
|
|
1248
|
+
|
|
1249
|
+
const updateSnapshot = <E2, R2>(
|
|
1250
|
+
f: (
|
|
1251
|
+
snapshot: RuntimeSnapshot<State, Error, Output>
|
|
1252
|
+
) => Effect.Effect<RuntimeSnapshot<State, Error, Output> | undefined, E2, R2>
|
|
1253
|
+
): Effect.Effect<RuntimeSnapshot<State, Error, Output> | undefined, E2, R2> =>
|
|
1254
|
+
SynchronizedRef.modifyEffect(
|
|
1255
|
+
current,
|
|
1256
|
+
(current) =>
|
|
1257
|
+
current.terminalizing
|
|
1258
|
+
? Effect.succeed([undefined, current] as const)
|
|
1259
|
+
: Effect.map(
|
|
1260
|
+
f(current.snapshot),
|
|
1261
|
+
(next) => {
|
|
1262
|
+
if (next === undefined) {
|
|
1263
|
+
return [undefined, current] as const
|
|
1264
|
+
}
|
|
1265
|
+
const versioned = {
|
|
1266
|
+
revision: current.revision + 1,
|
|
1267
|
+
snapshot: next,
|
|
1268
|
+
terminalizing: false,
|
|
1269
|
+
changes: current.changes
|
|
1270
|
+
}
|
|
1271
|
+
return [versioned, versioned] as const
|
|
1272
|
+
}
|
|
1273
|
+
)
|
|
1274
|
+
).pipe(
|
|
1275
|
+
Effect.flatMap((versioned) => versioned === undefined ? Effect.succeed(undefined) : publishIfCurrent(versioned)),
|
|
1276
|
+
Effect.map((published) => published?.snapshot)
|
|
1277
|
+
)
|
|
1278
|
+
|
|
1279
|
+
const reserveTerminalSnapshot = (
|
|
1280
|
+
f: (
|
|
1281
|
+
snapshot: Extract<RuntimeSnapshot<State, Error, Output>, { readonly status: "active" }>
|
|
1282
|
+
) => RuntimeSnapshot<State, Error, Output>
|
|
1283
|
+
): Effect.Effect<RuntimeSnapshot<State, Error, Output> | undefined> =>
|
|
1284
|
+
SynchronizedRef.modify(
|
|
1285
|
+
current,
|
|
1286
|
+
(current): SnapshotModification => {
|
|
1287
|
+
if (current.terminalizing || current.snapshot.status !== "active") {
|
|
1288
|
+
return [undefined, current]
|
|
1289
|
+
}
|
|
1290
|
+
return [
|
|
1291
|
+
{
|
|
1292
|
+
revision: current.revision + 1,
|
|
1293
|
+
snapshot: f(current.snapshot),
|
|
1294
|
+
terminalizing: true,
|
|
1295
|
+
changes: current.changes
|
|
1296
|
+
},
|
|
1297
|
+
{ ...current, terminalizing: true }
|
|
1298
|
+
]
|
|
1299
|
+
}
|
|
1300
|
+
).pipe(Effect.map((versioned) => versioned?.snapshot))
|
|
1301
|
+
|
|
1302
|
+
const setAndPublishSnapshot = (
|
|
1303
|
+
snapshot: RuntimeSnapshot<State, Error, Output>
|
|
1304
|
+
): Effect.Effect<void> =>
|
|
1305
|
+
SynchronizedRef.updateAndGet(current, (current) => ({
|
|
1306
|
+
revision: current.revision + 1,
|
|
1307
|
+
snapshot,
|
|
1308
|
+
terminalizing: true,
|
|
1309
|
+
changes: current.changes
|
|
1310
|
+
})).pipe(
|
|
1311
|
+
Effect.flatMap(publishSnapshot),
|
|
1312
|
+
Effect.flatMap(completeIfTerminal),
|
|
1313
|
+
Effect.asVoid
|
|
1314
|
+
)
|
|
1315
|
+
|
|
1316
|
+
const setActiveState = (state: State) =>
|
|
1317
|
+
updateSnapshot((snapshot) =>
|
|
1318
|
+
Effect.succeed(
|
|
1319
|
+
snapshot.status === "active"
|
|
1320
|
+
? {
|
|
1321
|
+
status: "active",
|
|
1322
|
+
state
|
|
1323
|
+
}
|
|
1324
|
+
: undefined
|
|
1325
|
+
)
|
|
1326
|
+
).pipe(Effect.asVoid)
|
|
1327
|
+
|
|
1328
|
+
const terminalizeWith = (
|
|
1329
|
+
snapshot: RuntimeSnapshot<State, Error, Output>,
|
|
1330
|
+
exit: Exit.Exit<unknown, unknown>,
|
|
1331
|
+
completeDone: Effect.Effect<void>
|
|
1332
|
+
): Effect.Effect<void> => {
|
|
1333
|
+
const notifyOutcome =
|
|
1334
|
+
onOutcome === undefined || (snapshot.status === "stopped" && options.skipStoppedOutcome === true)
|
|
1335
|
+
? Effect.void
|
|
1336
|
+
: Effect.suspend(() => onOutcome(classifyOutcome(snapshot)!)).pipe(
|
|
1337
|
+
Effect.exit,
|
|
1338
|
+
Effect.asVoid
|
|
1339
|
+
)
|
|
1340
|
+
return Effect.uninterruptible(
|
|
1341
|
+
Effect.sync(() => {
|
|
1342
|
+
while (true) {
|
|
1343
|
+
const pending = Queue.takeUnsafe(queue)
|
|
1344
|
+
if (pending === undefined || Exit.isFailure(pending)) break
|
|
1345
|
+
stopAcknowledgedMessage(pending.value)
|
|
1346
|
+
}
|
|
1347
|
+
}).pipe(
|
|
1348
|
+
Effect.andThen(Queue.shutdown(queue)),
|
|
1349
|
+
Effect.andThen(closeChildren(exit)),
|
|
1350
|
+
Effect.andThen(setAndPublishSnapshot(snapshot)),
|
|
1351
|
+
Effect.andThen(Effect.sync(() => {
|
|
1352
|
+
if (Exit.isFailure(exit)) {
|
|
1353
|
+
failAcknowledgedMessage(inFlightMessage, exit.cause)
|
|
1354
|
+
} else {
|
|
1355
|
+
stopAcknowledgedMessage(inFlightMessage)
|
|
1356
|
+
}
|
|
1357
|
+
inFlightMessage = undefined
|
|
1358
|
+
})),
|
|
1359
|
+
Effect.andThen(notifyOutcome),
|
|
1360
|
+
Effect.andThen(cleanup),
|
|
1361
|
+
Effect.andThen(completeDone)
|
|
1362
|
+
)
|
|
1363
|
+
)
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
const reserveStoppedSnapshot = reserveTerminalSnapshot((snapshot) => ({
|
|
1367
|
+
status: "stopped",
|
|
1368
|
+
state: snapshot.state
|
|
1369
|
+
}))
|
|
1370
|
+
|
|
1371
|
+
const reserveFailureSnapshot = (cause: Cause.Cause<Error>) =>
|
|
1372
|
+
reserveTerminalSnapshot((snapshot) => ({
|
|
1373
|
+
status: "error",
|
|
1374
|
+
state: snapshot.state,
|
|
1375
|
+
cause
|
|
1376
|
+
}))
|
|
1377
|
+
|
|
1378
|
+
const reserveSuccessSnapshot = (output: Output) =>
|
|
1379
|
+
reserveTerminalSnapshot((snapshot) => ({
|
|
1380
|
+
status: "done",
|
|
1381
|
+
state: snapshot.state,
|
|
1382
|
+
output
|
|
1383
|
+
}))
|
|
1384
|
+
|
|
1385
|
+
const terminalizeReservedStop = (
|
|
1386
|
+
snapshot: RuntimeSnapshot<State, Error, Output>
|
|
1387
|
+
): Effect.Effect<void> => {
|
|
1388
|
+
const exit = Exit.void
|
|
1389
|
+
return terminalizeWith(
|
|
1390
|
+
snapshot,
|
|
1391
|
+
exit,
|
|
1392
|
+
Deferred.fail(done, new StoppedError())
|
|
1393
|
+
)
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
const terminalizeReservedFailure = (
|
|
1397
|
+
snapshot: RuntimeSnapshot<State, Error, Output>,
|
|
1398
|
+
cause: Cause.Cause<Error>
|
|
1399
|
+
): Effect.Effect<void> => {
|
|
1400
|
+
const exit = Exit.failCause(cause)
|
|
1401
|
+
return terminalizeWith(snapshot, exit, Deferred.failCause(done, cause))
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
const terminalizeReservedSuccess = (
|
|
1405
|
+
snapshot: RuntimeSnapshot<State, Error, Output>,
|
|
1406
|
+
output: Output
|
|
1407
|
+
): Effect.Effect<void> => {
|
|
1408
|
+
const exit = Exit.succeed(output)
|
|
1409
|
+
return terminalizeWith(snapshot, exit, Deferred.succeed(done, output))
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
const stop: Effect.Effect<void> = Effect.uninterruptible(
|
|
1413
|
+
requestStop.pipe(Effect.andThen(awaitCompletion))
|
|
1414
|
+
)
|
|
1415
|
+
|
|
1416
|
+
const acknowledgedContext:
|
|
1417
|
+
| Pick<
|
|
1418
|
+
ProcessContext<State, Event>,
|
|
1419
|
+
"receiveMessage" | "pollMessage" | "completeMessage"
|
|
1420
|
+
>
|
|
1421
|
+
| undefined = logic.execution?._tag !== "Compiled" ? undefined : {
|
|
1422
|
+
receiveMessage: Queue.take(queue).pipe(
|
|
1423
|
+
Effect.tap((message) =>
|
|
1424
|
+
Effect.sync(() => {
|
|
1425
|
+
inFlightMessage = isAcknowledgedMessage(message) ? message : undefined
|
|
1426
|
+
})
|
|
1427
|
+
)
|
|
1428
|
+
),
|
|
1429
|
+
pollMessage: Queue.poll(queue).pipe(
|
|
1430
|
+
Effect.tap((message) =>
|
|
1431
|
+
Effect.sync(() => {
|
|
1432
|
+
if (Option.isSome(message)) {
|
|
1433
|
+
inFlightMessage = isAcknowledgedMessage(message.value) ? message.value : undefined
|
|
1434
|
+
}
|
|
1435
|
+
})
|
|
1436
|
+
)
|
|
1437
|
+
),
|
|
1438
|
+
completeMessage: (delivery) => {
|
|
1439
|
+
succeedAcknowledgedMessage(inFlightMessage, delivery)
|
|
1440
|
+
inFlightMessage = undefined
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
const context: ProcessContext<State, Event> = {
|
|
1444
|
+
...scope,
|
|
1445
|
+
...(logic.execution?._tag === "Compiled" && !logic.execution.childless ? { ownedChildren } : undefined),
|
|
1446
|
+
...acknowledgedContext,
|
|
1447
|
+
receive: Queue.take(queue).pipe(Effect.map(messageEvent)),
|
|
1448
|
+
poll: Queue.poll(queue).pipe(Effect.map(Option.map(messageEvent))),
|
|
1449
|
+
state: SynchronizedRef.get(current).pipe(Effect.map((current) => current.snapshot.state)),
|
|
1450
|
+
setState: setActiveState,
|
|
1451
|
+
updateState: (f) =>
|
|
1452
|
+
updateSnapshot((snapshot) =>
|
|
1453
|
+
snapshot.status === "active"
|
|
1454
|
+
? f(snapshot.state).pipe(
|
|
1455
|
+
Effect.map((state) => ({
|
|
1456
|
+
status: "active" as const,
|
|
1457
|
+
state
|
|
1458
|
+
}))
|
|
1459
|
+
)
|
|
1460
|
+
: Effect.succeed(undefined)
|
|
1461
|
+
).pipe(Effect.asVoid)
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
const getOrCreateChanges = SynchronizedRef.modifyEffect(
|
|
1465
|
+
current,
|
|
1466
|
+
(current) => {
|
|
1467
|
+
if (current.snapshot.status !== "active") {
|
|
1468
|
+
return Effect.succeed([undefined, current] as const)
|
|
1469
|
+
}
|
|
1470
|
+
if (current.changes !== undefined) {
|
|
1471
|
+
return Effect.succeed([current.changes, current] as const)
|
|
1472
|
+
}
|
|
1473
|
+
return PubSub.unbounded<Take.Take<VersionedSnapshot<State, Error, Output>>>({ replay: 1 }).pipe(
|
|
1474
|
+
Effect.map((changes) => [changes, { ...current, changes }] as const)
|
|
1475
|
+
)
|
|
1476
|
+
}
|
|
1477
|
+
)
|
|
1478
|
+
|
|
1479
|
+
const changesStream: Stream.Stream<RuntimeSnapshot<State, Error, Output>> = Stream.unwrap(
|
|
1480
|
+
Effect.gen(function*() {
|
|
1481
|
+
const changes = yield* getOrCreateChanges
|
|
1482
|
+
if (changes === undefined) {
|
|
1483
|
+
return Stream.succeed((yield* SynchronizedRef.get(current)).snapshot)
|
|
1484
|
+
}
|
|
1485
|
+
const subscription = yield* PubSub.subscribe(changes)
|
|
1486
|
+
const captured = yield* SynchronizedRef.get(current)
|
|
1487
|
+
if (captured.snapshot.status !== "active") {
|
|
1488
|
+
return Stream.succeed(captured.snapshot)
|
|
1489
|
+
}
|
|
1490
|
+
return Stream.succeed(captured.snapshot).pipe(
|
|
1491
|
+
Stream.concat(
|
|
1492
|
+
Stream.fromChannel(Channel.fromEffectTake(PubSub.take(subscription))).pipe(
|
|
1493
|
+
Stream.filter((next) => next.revision > captured.revision),
|
|
1494
|
+
Stream.map((next) => next.snapshot)
|
|
1495
|
+
)
|
|
1496
|
+
)
|
|
1497
|
+
)
|
|
1498
|
+
})
|
|
1499
|
+
)
|
|
1500
|
+
|
|
1501
|
+
const ref: MachineRef<State, Event, Error, Output> = {
|
|
1502
|
+
id,
|
|
1503
|
+
sessionId,
|
|
1504
|
+
state: SynchronizedRef.get(current).pipe(Effect.map((current) => current.snapshot.state)),
|
|
1505
|
+
snapshot: SynchronizedRef.get(current).pipe(Effect.map((current) => current.snapshot)),
|
|
1506
|
+
changes: changesStream,
|
|
1507
|
+
join: Deferred.await(done),
|
|
1508
|
+
stop,
|
|
1509
|
+
send: self.send,
|
|
1510
|
+
...(sendAcknowledged === undefined ? undefined : { [acknowledgedSend]: sendAcknowledged }),
|
|
1511
|
+
child: getChild,
|
|
1512
|
+
childChanges
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
if (onReadySync !== undefined && !onReadySync(ref)) {
|
|
1516
|
+
yield* requestStop
|
|
1517
|
+
} else if (onReady !== undefined) {
|
|
1518
|
+
yield* onReady(ref, requestStop)
|
|
1519
|
+
}
|
|
1520
|
+
if (onSnapshot !== undefined) {
|
|
1521
|
+
yield* notifyActiveSnapshot(onSnapshot, { status: "active", state: initial })
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
const reserveTermination = (termination: ProcessTermination) => {
|
|
1525
|
+
switch (termination._tag) {
|
|
1526
|
+
case "Stopped":
|
|
1527
|
+
return reserveStoppedSnapshot
|
|
1528
|
+
case "Done":
|
|
1529
|
+
return reserveSuccessSnapshot(termination.output)
|
|
1530
|
+
case "Failure":
|
|
1531
|
+
return reserveFailureSnapshot(termination.cause)
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
const completeTermination = (
|
|
1536
|
+
termination: ProcessTermination,
|
|
1537
|
+
snapshot: RuntimeSnapshot<State, Error, Output>
|
|
1538
|
+
) => {
|
|
1539
|
+
switch (termination._tag) {
|
|
1540
|
+
case "Stopped":
|
|
1541
|
+
return terminalizeReservedStop(snapshot)
|
|
1542
|
+
case "Done":
|
|
1543
|
+
return terminalizeReservedSuccess(snapshot, termination.output)
|
|
1544
|
+
case "Failure":
|
|
1545
|
+
return terminalizeReservedFailure(snapshot, termination.cause)
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
const forkRuntime = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
|
1550
|
+
detached === true
|
|
1551
|
+
? Effect.forkDetach(effect)
|
|
1552
|
+
: Effect.forkChild(effect)
|
|
1553
|
+
|
|
1554
|
+
const pendingTermination = yield* Deferred.poll(termination)
|
|
1555
|
+
const worker = Option.isNone(pendingTermination)
|
|
1556
|
+
? yield* Effect.uninterruptibleMask((restore) =>
|
|
1557
|
+
restore(Effect.suspend(() => logic.run(context))).pipe(
|
|
1558
|
+
Effect.exit,
|
|
1559
|
+
Effect.flatMap((exit) =>
|
|
1560
|
+
Deferred.succeed(
|
|
1561
|
+
termination,
|
|
1562
|
+
Exit.isFailure(exit)
|
|
1563
|
+
? { _tag: "Failure", cause: exit.cause }
|
|
1564
|
+
: { _tag: "Done", output: exit.value }
|
|
1565
|
+
)
|
|
1566
|
+
)
|
|
1567
|
+
)
|
|
1568
|
+
).pipe(forkRuntime)
|
|
1569
|
+
: undefined
|
|
1570
|
+
|
|
1571
|
+
// One Deferred arbitrates all terminal causes. The supervisor reserves the
|
|
1572
|
+
// terminal snapshot before interrupting the worker, so worker finalizers
|
|
1573
|
+
// cannot mutate the frozen state. It then waits for those finalizers before
|
|
1574
|
+
// publishing and completing `join` / `stop`.
|
|
1575
|
+
const runFiber: Effect.Effect<void, never, Requirements> = Effect.uninterruptibleMask((restore) =>
|
|
1576
|
+
Effect.gen(function*() {
|
|
1577
|
+
const requested = Option.isSome(pendingTermination)
|
|
1578
|
+
? yield* pendingTermination.value
|
|
1579
|
+
: yield* restore(Deferred.await(termination))
|
|
1580
|
+
|
|
1581
|
+
const snapshot = yield* reserveTermination(requested)
|
|
1582
|
+
if (worker !== undefined) {
|
|
1583
|
+
yield* Fiber.interrupt(worker)
|
|
1584
|
+
}
|
|
1585
|
+
if (snapshot === undefined) {
|
|
1586
|
+
return yield* awaitCompletion
|
|
1587
|
+
}
|
|
1588
|
+
return yield* completeTermination(requested, snapshot)
|
|
1589
|
+
})
|
|
1590
|
+
)
|
|
1591
|
+
|
|
1592
|
+
yield* forkRuntime(runFiber)
|
|
1593
|
+
yield* Effect.yieldNow
|
|
1594
|
+
|
|
1595
|
+
return ref
|
|
1596
|
+
})
|
|
1597
|
+
|
|
1598
|
+
type CompiledTermination =
|
|
1599
|
+
| { readonly _tag: "Stopped" }
|
|
1600
|
+
| { readonly _tag: "Done"; readonly output: unknown }
|
|
1601
|
+
| { readonly _tag: "Failure"; readonly cause: Cause.Cause<unknown> }
|
|
1602
|
+
|
|
1603
|
+
type CompiledInitialized = CompiledProcessInitial<unknown, unknown>
|
|
1604
|
+
|
|
1605
|
+
// Stopping is commonly used only for resource cleanup. Keep that path free of
|
|
1606
|
+
// Error stack capture and materialize the typed join failure only if observed.
|
|
1607
|
+
const CompiledStoppedCompletion: unique symbol = Symbol("effect/Machine/CompiledStoppedCompletion")
|
|
1608
|
+
|
|
1609
|
+
type CompiledCompletion =
|
|
1610
|
+
| Effect.Effect<unknown, unknown>
|
|
1611
|
+
| typeof CompiledStoppedCompletion
|
|
1612
|
+
|
|
1613
|
+
type CompiledLifecycle = "Active" | "TerminationRequested" | "Completed"
|
|
1614
|
+
type CompiledRunState = "Initializing" | "Idle" | "Draining"
|
|
1615
|
+
|
|
1616
|
+
/**
|
|
1617
|
+
* Compact runtime for compiled statecharts.
|
|
1618
|
+
*
|
|
1619
|
+
* All long-lived state is stored directly on this object. Operations are
|
|
1620
|
+
* implemented by shared prototype methods, and public Effect / Stream values
|
|
1621
|
+
* are materialized only when accessed. Arbitrary process logic continues to
|
|
1622
|
+
* use the general runtime above.
|
|
1623
|
+
*/
|
|
1624
|
+
class CompiledProcess implements MachineRef<any, any, any, any> {
|
|
1625
|
+
readonly id: string
|
|
1626
|
+
readonly sessionId: string
|
|
1627
|
+
readonly send: (event: unknown) => Effect.Effect<void, StoppedError>;
|
|
1628
|
+
[acknowledgedSend](
|
|
1629
|
+
event: unknown
|
|
1630
|
+
): Effect.Effect<AcknowledgedDelivery<unknown>, unknown | StoppedError> {
|
|
1631
|
+
return this.sendAcknowledgedEffect(event)
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
private readonly mailbox: CompactProcessMailbox<unknown> = {
|
|
1635
|
+
items: undefined,
|
|
1636
|
+
index: 0,
|
|
1637
|
+
closed: false
|
|
1638
|
+
}
|
|
1639
|
+
private readonly address: ProcessAddress<unknown>
|
|
1640
|
+
private childRuntime: ChildRuntime = childlessRuntime
|
|
1641
|
+
private processScope!: ProcessScope<unknown>
|
|
1642
|
+
private processContext: ProcessContext<unknown, unknown> | undefined
|
|
1643
|
+
private compiledContext: CompiledProcessContext<unknown, unknown> | undefined
|
|
1644
|
+
private current!: VersionedSnapshot<unknown, unknown, unknown>
|
|
1645
|
+
/**
|
|
1646
|
+
* `Active` has no terminal payload, `TerminationRequested` owns
|
|
1647
|
+
* `termination` and its optional reserved snapshot, and `Completed` owns
|
|
1648
|
+
* `completion`. `runState` independently tracks worker activity.
|
|
1649
|
+
*/
|
|
1650
|
+
private lifecycle: CompiledLifecycle = "Active"
|
|
1651
|
+
private runState: CompiledRunState = "Initializing"
|
|
1652
|
+
private completion: CompiledCompletion | undefined
|
|
1653
|
+
private waiter: Deferred.Deferred<unknown, unknown> | undefined
|
|
1654
|
+
private termination: CompiledTermination | undefined
|
|
1655
|
+
private terminationSnapshot: RuntimeSnapshot<unknown, unknown, unknown> | undefined
|
|
1656
|
+
private worker: Fiber.Fiber<any, never> | undefined
|
|
1657
|
+
private interruptRequested = false
|
|
1658
|
+
private offerRevision = 0
|
|
1659
|
+
private inFlightMessage: ProcessMessage<unknown> | undefined
|
|
1660
|
+
|
|
1661
|
+
constructor(
|
|
1662
|
+
private readonly logic: ProcessLogic<any, any, any, any, any, any>,
|
|
1663
|
+
private readonly options: StartInternalOptions,
|
|
1664
|
+
private readonly services: Context.Context<any>,
|
|
1665
|
+
sessionId: string
|
|
1666
|
+
) {
|
|
1667
|
+
this.sessionId = sessionId
|
|
1668
|
+
this.id = options.id ?? sessionId
|
|
1669
|
+
this.send = (event) => this.offerMessage(event)
|
|
1670
|
+
this.address = {
|
|
1671
|
+
id: this.id,
|
|
1672
|
+
sessionId,
|
|
1673
|
+
stop: Effect.suspend(() => this.stopFromProcess()),
|
|
1674
|
+
send: this.send
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
private get execution(): CompiledProcessExecution<any, any, any, any, any, any> {
|
|
1679
|
+
return this.logic.execution as CompiledProcessExecution<any, any, any, any, any, any>
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
initializeCompiledSync(): Effect.Effect<MachineRef<any, any, any, any>, unknown> {
|
|
1683
|
+
if (!this.execution.childless) {
|
|
1684
|
+
this.childRuntime = makeChildRuntimeSync(this.address, this.options.runtime, this.services)
|
|
1685
|
+
}
|
|
1686
|
+
const parent = this.options.parent
|
|
1687
|
+
const sendParent = this.options.sendParent ?? (parent === undefined ? noParentSend : parent.send)
|
|
1688
|
+
this.processScope = {
|
|
1689
|
+
self: this.address,
|
|
1690
|
+
parent,
|
|
1691
|
+
spawn: this.childRuntime.spawn,
|
|
1692
|
+
sendParent,
|
|
1693
|
+
sendTo: this.childRuntime.sendTo,
|
|
1694
|
+
stopChild: this.childRuntime.stop,
|
|
1695
|
+
failCause: (cause: Cause.Cause<unknown>) => this.failCause(cause)
|
|
1696
|
+
}
|
|
1697
|
+
const compiledInitial = this.execution.initialSync!
|
|
1698
|
+
let initialized: CompiledInitialized
|
|
1699
|
+
try {
|
|
1700
|
+
initialized = compiledInitial(this.processScope)
|
|
1701
|
+
} catch (error) {
|
|
1702
|
+
this.runState = "Idle"
|
|
1703
|
+
return Effect.fail(error)
|
|
1704
|
+
}
|
|
1705
|
+
this.runState = "Idle"
|
|
1706
|
+
this.current = {
|
|
1707
|
+
revision: 0,
|
|
1708
|
+
terminalizing: false,
|
|
1709
|
+
changes: undefined,
|
|
1710
|
+
snapshot: { status: "active", state: initialized.state }
|
|
1711
|
+
}
|
|
1712
|
+
this.compiledContext = new CompiledProcessContextImpl(this.processScope, this.childRuntime.owned, this)
|
|
1713
|
+
if ("executionState" in initialized) {
|
|
1714
|
+
this.compiledContext.executionState = initialized.executionState
|
|
1715
|
+
}
|
|
1716
|
+
if (this.options.onReadySync !== undefined && !this.options.onReadySync(this)) {
|
|
1717
|
+
this.requestTerminationSync({ _tag: "Stopped" })
|
|
1718
|
+
}
|
|
1719
|
+
if (initialized.done === true && this.lifecycle === "Active") {
|
|
1720
|
+
this.requestTerminationSync({ _tag: "Done", output: initialized.output })
|
|
1721
|
+
}
|
|
1722
|
+
if (
|
|
1723
|
+
initialized.done === false && this.execution.childless &&
|
|
1724
|
+
this.lifecycle === "Active" &&
|
|
1725
|
+
this.mailbox.items === undefined
|
|
1726
|
+
) {
|
|
1727
|
+
return Effect.succeed(this)
|
|
1728
|
+
}
|
|
1729
|
+
this.runState = "Draining"
|
|
1730
|
+
return Effect.provideContext(this.drainRuntime(), this.services).pipe(Effect.as(this))
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
initialize(): Effect.Effect<MachineRef<any, any, any, any>, unknown, any> {
|
|
1734
|
+
const self = this
|
|
1735
|
+
return Effect.gen(function*() {
|
|
1736
|
+
if (!self.execution.childless) {
|
|
1737
|
+
self.childRuntime = yield* makeChildRuntime(self.address, self.options.runtime, self.services)
|
|
1738
|
+
}
|
|
1739
|
+
const parent = self.options.parent
|
|
1740
|
+
const sendParent = self.options.sendParent ?? (parent === undefined ? noParentSend : parent.send)
|
|
1741
|
+
self.processScope = {
|
|
1742
|
+
self: self.address,
|
|
1743
|
+
parent,
|
|
1744
|
+
spawn: self.childRuntime.spawn,
|
|
1745
|
+
sendParent,
|
|
1746
|
+
sendTo: self.childRuntime.sendTo,
|
|
1747
|
+
stopChild: self.childRuntime.stop,
|
|
1748
|
+
failCause: (cause: Cause.Cause<unknown>) => self.failCause(cause)
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
const cleanupStartupFailure = <A, E>(exit: Exit.Exit<A, E>): Effect.Effect<void> =>
|
|
1752
|
+
Exit.isFailure(exit) ? self.childRuntime.close(exit) : Effect.void
|
|
1753
|
+
const compiledInitial = self.execution.initial
|
|
1754
|
+
const initializeEffect: Effect.Effect<
|
|
1755
|
+
{
|
|
1756
|
+
readonly state: unknown
|
|
1757
|
+
readonly done: boolean | undefined
|
|
1758
|
+
readonly output: unknown
|
|
1759
|
+
},
|
|
1760
|
+
unknown,
|
|
1761
|
+
any
|
|
1762
|
+
> = compiledInitial === undefined
|
|
1763
|
+
? self.logic.initial(self.processScope).pipe(
|
|
1764
|
+
Effect.map((state) => ({ state, done: undefined, output: undefined } as const))
|
|
1765
|
+
)
|
|
1766
|
+
: compiledInitial(self.processScope)
|
|
1767
|
+
const initialized = yield* initializeEffect.pipe(
|
|
1768
|
+
Effect.onExit(cleanupStartupFailure),
|
|
1769
|
+
Effect.ensuring(Effect.sync(() => {
|
|
1770
|
+
self.runState = "Idle"
|
|
1771
|
+
}))
|
|
1772
|
+
)
|
|
1773
|
+
const initial = initialized.state
|
|
1774
|
+
self.current = {
|
|
1775
|
+
revision: 0,
|
|
1776
|
+
terminalizing: false,
|
|
1777
|
+
changes: undefined,
|
|
1778
|
+
snapshot: { status: "active", state: initial }
|
|
1779
|
+
}
|
|
1780
|
+
if (self.execution.drain._tag === "Process") {
|
|
1781
|
+
self.processContext = {
|
|
1782
|
+
...self.processScope,
|
|
1783
|
+
receive: Effect.never,
|
|
1784
|
+
poll: Effect.sync(() => Option.map(pollCompactMailbox(self.mailbox), messageEvent)),
|
|
1785
|
+
receiveMessage: Effect.never,
|
|
1786
|
+
pollMessage: Effect.sync(() => self.pollCompiledMessage()),
|
|
1787
|
+
completeMessage: (delivery) => self.completeCompiledMessage(delivery),
|
|
1788
|
+
state: Effect.sync(() => self.current.snapshot.state),
|
|
1789
|
+
setState: (state: unknown) => self.setActiveState(state),
|
|
1790
|
+
updateState: (f) => self.updateState(f)
|
|
1791
|
+
}
|
|
1792
|
+
} else {
|
|
1793
|
+
self.compiledContext = new CompiledProcessContextImpl(self.processScope, self.childRuntime.owned, self)
|
|
1794
|
+
if ("executionState" in initialized) {
|
|
1795
|
+
self.compiledContext.executionState = initialized.executionState
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
if (self.options.onReadySync !== undefined && !self.options.onReadySync(self)) {
|
|
1800
|
+
yield* self.requestTermination({ _tag: "Stopped" })
|
|
1801
|
+
} else if (self.options.onReady !== undefined) {
|
|
1802
|
+
yield* self.options.onReady(self, self.requestTermination({ _tag: "Stopped" }).pipe(Effect.asVoid))
|
|
1803
|
+
}
|
|
1804
|
+
if (self.options.onSnapshot !== undefined) {
|
|
1805
|
+
yield* notifyActiveSnapshot(self.options.onSnapshot, { status: "active", state: initial })
|
|
1806
|
+
}
|
|
1807
|
+
if (initialized.done === true && self.lifecycle === "Active") {
|
|
1808
|
+
yield* self.requestTermination({ _tag: "Done", output: initialized.output })
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// A compiled machine startup plan has already settled entry actions,
|
|
1812
|
+
// raised events, and eventless transitions. If it is known active and
|
|
1813
|
+
// neither startup hooks nor emitted work queued an event, there is no
|
|
1814
|
+
// first drain to perform. Future sends observe an idle run state and
|
|
1815
|
+
// schedule the ordinary compiled worker.
|
|
1816
|
+
if (
|
|
1817
|
+
initialized.done === false && self.execution.childless &&
|
|
1818
|
+
self.lifecycle === "Active" && self.mailbox.items === undefined
|
|
1819
|
+
) {
|
|
1820
|
+
return self
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
self.runState = "Draining"
|
|
1824
|
+
yield* self.drainRuntime()
|
|
1825
|
+
return self
|
|
1826
|
+
})
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
get state(): Effect.Effect<unknown> {
|
|
1830
|
+
return Effect.sync(() => this.current.snapshot.state)
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
get snapshot(): Effect.Effect<RuntimeSnapshot<unknown, unknown, unknown>> {
|
|
1834
|
+
return Effect.sync(() => this.current.snapshot)
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
get changes(): Stream.Stream<RuntimeSnapshot<unknown, unknown, unknown>> {
|
|
1838
|
+
return this.changesStream()
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
get join(): Effect.Effect<unknown, unknown> {
|
|
1842
|
+
return Effect.suspend(() => {
|
|
1843
|
+
if (this.lifecycle === "Completed") {
|
|
1844
|
+
return this.resolveCompletion(this.completion!)
|
|
1845
|
+
}
|
|
1846
|
+
this.waiter ??= Deferred.makeUnsafe<unknown, unknown>()
|
|
1847
|
+
return Deferred.await(this.waiter)
|
|
1848
|
+
})
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
get stop(): Effect.Effect<void> {
|
|
1852
|
+
return Effect.uninterruptible(this.stopEffect())
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
child(child: ChildSelector): Effect.Effect<Option.Option<any>> {
|
|
1856
|
+
return this.childRuntime.get(child)
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
childChanges(child: ChildSelector): Stream.Stream<Option.Option<any>> {
|
|
1860
|
+
return this.childRuntime.changes(child)
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
private requestTermination(requested: CompiledTermination): Effect.Effect<boolean> {
|
|
1864
|
+
return Effect.sync(() => this.requestTerminationSync(requested))
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
private hasTerminationRequest(): boolean {
|
|
1868
|
+
return this.lifecycle === "TerminationRequested"
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
private requestTerminationSync(requested: CompiledTermination): boolean {
|
|
1872
|
+
if (this.lifecycle !== "Active") {
|
|
1873
|
+
return false
|
|
1874
|
+
}
|
|
1875
|
+
this.lifecycle = "TerminationRequested"
|
|
1876
|
+
this.termination = requested
|
|
1877
|
+
this.terminationSnapshot = this.reserveTermination(requested)
|
|
1878
|
+
return true
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
private reserveTermination(
|
|
1882
|
+
requested: CompiledTermination
|
|
1883
|
+
): RuntimeSnapshot<unknown, unknown, unknown> | undefined {
|
|
1884
|
+
const latest = this.current
|
|
1885
|
+
if (latest === undefined || latest.terminalizing || latest.snapshot.status !== "active") {
|
|
1886
|
+
return undefined
|
|
1887
|
+
}
|
|
1888
|
+
const snapshot: RuntimeSnapshot<unknown, unknown, unknown> = requested._tag === "Stopped"
|
|
1889
|
+
? { status: "stopped", state: latest.snapshot.state }
|
|
1890
|
+
: requested._tag === "Done"
|
|
1891
|
+
? { status: "done", state: latest.snapshot.state, output: requested.output }
|
|
1892
|
+
: { status: "error", state: latest.snapshot.state, cause: requested.cause }
|
|
1893
|
+
this.current = { ...latest, terminalizing: true }
|
|
1894
|
+
return snapshot
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
private stopFromProcess(): Effect.Effect<void> {
|
|
1898
|
+
const request = this.requestTermination({ _tag: "Stopped" }).pipe(Effect.asVoid)
|
|
1899
|
+
return this.runState === "Initializing" ? request : request.pipe(Effect.andThen(Effect.interrupt))
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
private failCause(cause: Cause.Cause<unknown>): Effect.Effect<void> {
|
|
1903
|
+
const requested = { _tag: "Failure", cause } as const
|
|
1904
|
+
return this.requestTermination(requested).pipe(
|
|
1905
|
+
Effect.flatMap((accepted) =>
|
|
1906
|
+
accepted
|
|
1907
|
+
? Effect.forkDetach(this.settleRequestedTermination()).pipe(Effect.asVoid)
|
|
1908
|
+
: Effect.void
|
|
1909
|
+
)
|
|
1910
|
+
)
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
private sendAcknowledgedEffect(
|
|
1914
|
+
event: unknown
|
|
1915
|
+
): Effect.Effect<AcknowledgedDelivery<unknown>, unknown | StoppedError> {
|
|
1916
|
+
return Effect.uninterruptibleMask((restore) =>
|
|
1917
|
+
Deferred.make<AcknowledgedDelivery<unknown>, unknown>().pipe(
|
|
1918
|
+
Effect.flatMap((deferred) =>
|
|
1919
|
+
this.offerMessage({
|
|
1920
|
+
[AcknowledgedMessageTypeId]: true as const,
|
|
1921
|
+
event,
|
|
1922
|
+
deferred
|
|
1923
|
+
}).pipe(
|
|
1924
|
+
Effect.andThen(restore(Deferred.await(deferred)))
|
|
1925
|
+
)
|
|
1926
|
+
)
|
|
1927
|
+
)
|
|
1928
|
+
)
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
private offerMessage(message: ProcessMessage<unknown>): Effect.Effect<void, StoppedError> {
|
|
1932
|
+
return Effect.uninterruptible(
|
|
1933
|
+
Effect.suspend(() => {
|
|
1934
|
+
if (this.mailbox.closed || this.lifecycle !== "Active") {
|
|
1935
|
+
return Effect.fail(new StoppedError())
|
|
1936
|
+
}
|
|
1937
|
+
offerCompactMailbox(this.mailbox, message)
|
|
1938
|
+
this.offerRevision += 1
|
|
1939
|
+
if (this.runState === "Draining") {
|
|
1940
|
+
return Effect.void
|
|
1941
|
+
}
|
|
1942
|
+
this.runState = "Draining"
|
|
1943
|
+
const scheduled = Effect.yieldNow.pipe(
|
|
1944
|
+
Effect.andThen(Effect.provideContext(this.drainRuntime(), this.services))
|
|
1945
|
+
)
|
|
1946
|
+
const fork = this.options.detached === true
|
|
1947
|
+
? Effect.forkDetach(scheduled, { startImmediately: true })
|
|
1948
|
+
: Effect.forkChild(scheduled, { startImmediately: true })
|
|
1949
|
+
return fork.pipe(
|
|
1950
|
+
Effect.flatMap((fiber) =>
|
|
1951
|
+
Effect.sync(() => {
|
|
1952
|
+
this.worker = fiber
|
|
1953
|
+
if (!this.interruptRequested) {
|
|
1954
|
+
return false
|
|
1955
|
+
}
|
|
1956
|
+
this.interruptRequested = false
|
|
1957
|
+
return true
|
|
1958
|
+
}).pipe(
|
|
1959
|
+
Effect.flatMap((interrupt) => interrupt ? this.interruptAndFinish(fiber) : Effect.void)
|
|
1960
|
+
)
|
|
1961
|
+
),
|
|
1962
|
+
Effect.asVoid
|
|
1963
|
+
)
|
|
1964
|
+
})
|
|
1965
|
+
)
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
private stopEffect(): Effect.Effect<void> {
|
|
1969
|
+
return Effect.suspend(() => {
|
|
1970
|
+
if (this.lifecycle === "Completed") {
|
|
1971
|
+
return Effect.void
|
|
1972
|
+
}
|
|
1973
|
+
if (this.finishIdleChildlessStop()) {
|
|
1974
|
+
return Effect.void
|
|
1975
|
+
}
|
|
1976
|
+
const requested = { _tag: "Stopped" } as const
|
|
1977
|
+
return this.requestTermination(requested).pipe(
|
|
1978
|
+
Effect.flatMap((accepted) =>
|
|
1979
|
+
accepted
|
|
1980
|
+
? this.settleRequestedTermination()
|
|
1981
|
+
: this.awaitCompletion()
|
|
1982
|
+
)
|
|
1983
|
+
)
|
|
1984
|
+
})
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
private finishIdleChildlessStop(): boolean {
|
|
1988
|
+
if (
|
|
1989
|
+
!this.execution.childless || this.runState !== "Idle" || this.worker !== undefined ||
|
|
1990
|
+
this.lifecycle !== "Active" ||
|
|
1991
|
+
(this.options.onOutcome !== undefined && this.options.skipStoppedOutcome !== true) ||
|
|
1992
|
+
this.options.onStop !== undefined ||
|
|
1993
|
+
this.current.changes !== undefined ||
|
|
1994
|
+
this.current.terminalizing || this.current.snapshot.status !== "active"
|
|
1995
|
+
) {
|
|
1996
|
+
return false
|
|
1997
|
+
}
|
|
1998
|
+
const snapshot = { status: "stopped" as const, state: this.current.snapshot.state }
|
|
1999
|
+
this.lifecycle = "TerminationRequested"
|
|
2000
|
+
this.termination = { _tag: "Stopped" }
|
|
2001
|
+
this.terminationSnapshot = snapshot
|
|
2002
|
+
closeCompactMailbox(this.mailbox)
|
|
2003
|
+
this.current = {
|
|
2004
|
+
revision: this.current.revision + 1,
|
|
2005
|
+
terminalizing: true,
|
|
2006
|
+
changes: undefined,
|
|
2007
|
+
snapshot
|
|
2008
|
+
}
|
|
2009
|
+
stopAcknowledgedMessage(this.inFlightMessage)
|
|
2010
|
+
this.inFlightMessage = undefined
|
|
2011
|
+
this.interruptRequested = false
|
|
2012
|
+
if (this.compiledContext !== undefined) {
|
|
2013
|
+
this.compiledContext.executionState = undefined
|
|
2014
|
+
}
|
|
2015
|
+
this.options.onStopSync?.()
|
|
2016
|
+
this.completion = CompiledStoppedCompletion
|
|
2017
|
+
this.lifecycle = "Completed"
|
|
2018
|
+
if (this.waiter !== undefined) {
|
|
2019
|
+
Deferred.doneUnsafe(this.waiter, this.resolveCompletion(CompiledStoppedCompletion))
|
|
2020
|
+
this.waiter = undefined
|
|
2021
|
+
}
|
|
2022
|
+
return true
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
private settleRequestedTermination(): Effect.Effect<void> {
|
|
2026
|
+
return Effect.suspend(() => {
|
|
2027
|
+
if (this.runState !== "Draining") {
|
|
2028
|
+
return this.finishRequestedTermination()
|
|
2029
|
+
}
|
|
2030
|
+
if (this.worker === undefined) {
|
|
2031
|
+
this.interruptRequested = true
|
|
2032
|
+
return this.awaitCompletion()
|
|
2033
|
+
}
|
|
2034
|
+
return this.interruptAndFinish(this.worker).pipe(
|
|
2035
|
+
Effect.andThen(this.awaitCompletion())
|
|
2036
|
+
)
|
|
2037
|
+
})
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
private interruptAndFinish(worker: Fiber.Fiber<any, never>): Effect.Effect<void> {
|
|
2041
|
+
return Fiber.interrupt(worker).pipe(
|
|
2042
|
+
Effect.andThen(
|
|
2043
|
+
Effect.suspend(() =>
|
|
2044
|
+
this.lifecycle !== "Completed"
|
|
2045
|
+
? this.finishRequestedTermination()
|
|
2046
|
+
: Effect.void
|
|
2047
|
+
)
|
|
2048
|
+
)
|
|
2049
|
+
)
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
private awaitCompletion(): Effect.Effect<void> {
|
|
2053
|
+
return this.lifecycle !== "Completed"
|
|
2054
|
+
? this.join.pipe(Effect.exit, Effect.asVoid)
|
|
2055
|
+
: Effect.void
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
private resolveCompletion(completion: CompiledCompletion): Effect.Effect<unknown, unknown> {
|
|
2059
|
+
if (completion !== CompiledStoppedCompletion) {
|
|
2060
|
+
return completion
|
|
2061
|
+
}
|
|
2062
|
+
const stopped = Effect.fail(new StoppedError())
|
|
2063
|
+
this.completion = stopped
|
|
2064
|
+
return stopped
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
private drainRuntime(): Effect.Effect<void, never, any> {
|
|
2068
|
+
const self = this
|
|
2069
|
+
return Effect.uninterruptibleMask((restore) =>
|
|
2070
|
+
Effect.gen(function*() {
|
|
2071
|
+
let observedRevision = self.offerRevision
|
|
2072
|
+
while (true) {
|
|
2073
|
+
if (self.hasTerminationRequest()) {
|
|
2074
|
+
return yield* self.finishRequestedTermination()
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
const exit = yield* restore(
|
|
2078
|
+
Effect.suspend(() => {
|
|
2079
|
+
const drain = self.execution.drain
|
|
2080
|
+
return drain._tag === "Process"
|
|
2081
|
+
? drain.run(self.processContext!)
|
|
2082
|
+
: drain.run(self.compiledContext!)
|
|
2083
|
+
})
|
|
2084
|
+
).pipe(Effect.exit)
|
|
2085
|
+
self.flushPendingChanges()
|
|
2086
|
+
if (Exit.isFailure(exit)) {
|
|
2087
|
+
if (self.lifecycle === "Active") {
|
|
2088
|
+
yield* self.requestTermination({ _tag: "Failure", cause: exit.cause })
|
|
2089
|
+
}
|
|
2090
|
+
return yield* self.finishRequestedTermination()
|
|
2091
|
+
}
|
|
2092
|
+
if (Option.isSome(exit.value)) {
|
|
2093
|
+
yield* self.requestTermination({ _tag: "Done", output: exit.value.value })
|
|
2094
|
+
return yield* self.finishRequestedTermination()
|
|
2095
|
+
}
|
|
2096
|
+
if (self.hasTerminationRequest()) {
|
|
2097
|
+
return yield* self.finishRequestedTermination()
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
if (self.offerRevision !== observedRevision) {
|
|
2101
|
+
observedRevision = self.offerRevision
|
|
2102
|
+
continue
|
|
2103
|
+
}
|
|
2104
|
+
self.runState = "Idle"
|
|
2105
|
+
self.worker = undefined
|
|
2106
|
+
return
|
|
2107
|
+
}
|
|
2108
|
+
})
|
|
2109
|
+
)
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
private finishRequestedTermination(): Effect.Effect<void> {
|
|
2113
|
+
return Effect.suspend(() => {
|
|
2114
|
+
if (this.lifecycle !== "TerminationRequested") {
|
|
2115
|
+
return Effect.void
|
|
2116
|
+
}
|
|
2117
|
+
const requested = this.termination
|
|
2118
|
+
if (requested === undefined) {
|
|
2119
|
+
return Effect.void
|
|
2120
|
+
}
|
|
2121
|
+
const snapshot = this.terminationSnapshot ?? this.reserveTermination(requested)
|
|
2122
|
+
if (snapshot === undefined) {
|
|
2123
|
+
return this.awaitCompletion()
|
|
2124
|
+
}
|
|
2125
|
+
const exit = requested._tag === "Stopped"
|
|
2126
|
+
? Exit.void
|
|
2127
|
+
: requested._tag === "Done"
|
|
2128
|
+
? Exit.succeed(requested.output)
|
|
2129
|
+
: Exit.failCause(requested.cause)
|
|
2130
|
+
const completion: CompiledCompletion = requested._tag === "Stopped"
|
|
2131
|
+
? CompiledStoppedCompletion
|
|
2132
|
+
: requested._tag === "Done"
|
|
2133
|
+
? Effect.succeed(requested.output)
|
|
2134
|
+
: Effect.failCause(requested.cause)
|
|
2135
|
+
const notifyOutcome = this.options.onOutcome === undefined ||
|
|
2136
|
+
(requested._tag === "Stopped" && this.options.skipStoppedOutcome === true)
|
|
2137
|
+
? Effect.void
|
|
2138
|
+
: Effect.suspend(() => this.options.onOutcome!(classifyOutcome(snapshot)!)).pipe(
|
|
2139
|
+
Effect.exit,
|
|
2140
|
+
Effect.asVoid
|
|
2141
|
+
)
|
|
2142
|
+
return Effect.uninterruptible(
|
|
2143
|
+
Effect.sync(() => {
|
|
2144
|
+
closeCompactMailbox(this.mailbox)
|
|
2145
|
+
}).pipe(
|
|
2146
|
+
Effect.andThen(this.childRuntime.close(exit)),
|
|
2147
|
+
Effect.andThen(this.setAndPublishSnapshot(snapshot)),
|
|
2148
|
+
Effect.andThen(Effect.sync(() => {
|
|
2149
|
+
if (requested._tag === "Failure") {
|
|
2150
|
+
failAcknowledgedMessage(this.inFlightMessage, requested.cause)
|
|
2151
|
+
} else {
|
|
2152
|
+
stopAcknowledgedMessage(this.inFlightMessage)
|
|
2153
|
+
}
|
|
2154
|
+
this.inFlightMessage = undefined
|
|
2155
|
+
})),
|
|
2156
|
+
Effect.andThen(notifyOutcome),
|
|
2157
|
+
Effect.andThen(this.options.onStop ?? Effect.void),
|
|
2158
|
+
Effect.andThen(Effect.sync(() => {
|
|
2159
|
+
this.options.onStopSync?.()
|
|
2160
|
+
this.runState = "Idle"
|
|
2161
|
+
this.worker = undefined
|
|
2162
|
+
this.interruptRequested = false
|
|
2163
|
+
if (this.compiledContext !== undefined) {
|
|
2164
|
+
this.compiledContext.executionState = undefined
|
|
2165
|
+
}
|
|
2166
|
+
this.completion = completion
|
|
2167
|
+
this.lifecycle = "Completed"
|
|
2168
|
+
if (this.waiter !== undefined) {
|
|
2169
|
+
Deferred.doneUnsafe(this.waiter, this.resolveCompletion(completion))
|
|
2170
|
+
this.waiter = undefined
|
|
2171
|
+
}
|
|
2172
|
+
}))
|
|
2173
|
+
)
|
|
2174
|
+
)
|
|
2175
|
+
})
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
private publishSnapshot(
|
|
2179
|
+
snapshot: VersionedSnapshot<unknown, unknown, unknown>
|
|
2180
|
+
): Effect.Effect<VersionedSnapshot<unknown, unknown, unknown>> {
|
|
2181
|
+
const publish = snapshot.changes === undefined
|
|
2182
|
+
? Effect.succeed(snapshot)
|
|
2183
|
+
: PubSub.publish(snapshot.changes, [snapshot] as const).pipe(Effect.as(snapshot))
|
|
2184
|
+
const current = snapshot.snapshot
|
|
2185
|
+
return this.options.onSnapshot === undefined || current.status !== "active"
|
|
2186
|
+
? publish
|
|
2187
|
+
: publish.pipe(Effect.tap(() => notifyActiveSnapshot(this.options.onSnapshot!, current)))
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
private completeChanges(snapshot: VersionedSnapshot<unknown, unknown, unknown>): Effect.Effect<void> {
|
|
2191
|
+
return snapshot.changes === undefined
|
|
2192
|
+
? Effect.void
|
|
2193
|
+
: PubSub.publish(snapshot.changes, Exit.succeed<void>(undefined)).pipe(Effect.asVoid)
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
private setAndPublishSnapshot(snapshot: RuntimeSnapshot<unknown, unknown, unknown>): Effect.Effect<void> {
|
|
2197
|
+
return Effect.suspend(() => {
|
|
2198
|
+
this.flushPendingChanges()
|
|
2199
|
+
const versioned = {
|
|
2200
|
+
revision: this.current.revision + 1,
|
|
2201
|
+
snapshot,
|
|
2202
|
+
terminalizing: true,
|
|
2203
|
+
changes: this.current.changes
|
|
2204
|
+
}
|
|
2205
|
+
this.current = versioned
|
|
2206
|
+
return this.publishSnapshot(versioned).pipe(
|
|
2207
|
+
Effect.flatMap((published) => this.completeChanges(published)),
|
|
2208
|
+
Effect.asVoid
|
|
2209
|
+
)
|
|
2210
|
+
})
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
private setActiveState(state: unknown): Effect.Effect<void> {
|
|
2214
|
+
return Effect.suspend(() => this.commitActiveState(state) ?? Effect.void)
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
pollCompiledMessage(): Option.Option<ProcessMessage<unknown>> {
|
|
2218
|
+
const message = pollCompactMailbox(this.mailbox)
|
|
2219
|
+
if (Option.isSome(message)) {
|
|
2220
|
+
this.inFlightMessage = isAcknowledgedMessage(message.value) ? message.value : undefined
|
|
2221
|
+
}
|
|
2222
|
+
return message
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
completeCompiledMessage(delivery: AcknowledgedDelivery<unknown>): void {
|
|
2226
|
+
succeedAcknowledgedMessage(this.inFlightMessage, delivery)
|
|
2227
|
+
this.inFlightMessage = undefined
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
compiledState(): unknown {
|
|
2231
|
+
return this.current.snapshot.state
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
commitCompiledState(state: unknown): Effect.Effect<void> | undefined {
|
|
2235
|
+
return this.commitActiveState(state, true)
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
private commitActiveState(state: unknown, batchChanges = false): Effect.Effect<void> | undefined {
|
|
2239
|
+
const latest = this.current
|
|
2240
|
+
if (latest.terminalizing || latest.snapshot.status !== "active") {
|
|
2241
|
+
return undefined
|
|
2242
|
+
}
|
|
2243
|
+
const pendingChanges = latest.pendingChanges
|
|
2244
|
+
if (pendingChanges !== undefined) {
|
|
2245
|
+
latest.pendingChanges = undefined
|
|
2246
|
+
}
|
|
2247
|
+
const activeSnapshot = { status: "active" as const, state }
|
|
2248
|
+
const versioned = {
|
|
2249
|
+
revision: latest.revision + 1,
|
|
2250
|
+
snapshot: activeSnapshot,
|
|
2251
|
+
terminalizing: false,
|
|
2252
|
+
changes: latest.changes
|
|
2253
|
+
} as VersionedSnapshot<unknown, unknown, unknown>
|
|
2254
|
+
this.current = versioned
|
|
2255
|
+
if (versioned.changes !== undefined) {
|
|
2256
|
+
if (pendingChanges === undefined) {
|
|
2257
|
+
if (batchChanges) {
|
|
2258
|
+
versioned.pendingChanges = [versioned]
|
|
2259
|
+
} else {
|
|
2260
|
+
PubSub.publishUnsafe(versioned.changes, [versioned] as const)
|
|
2261
|
+
}
|
|
2262
|
+
} else {
|
|
2263
|
+
pendingChanges.push(versioned)
|
|
2264
|
+
if (batchChanges) {
|
|
2265
|
+
versioned.pendingChanges = pendingChanges
|
|
2266
|
+
} else {
|
|
2267
|
+
PubSub.publishUnsafe(versioned.changes, pendingChanges)
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
return this.options.onSnapshot === undefined
|
|
2272
|
+
? undefined
|
|
2273
|
+
: notifyActiveSnapshot(this.options.onSnapshot, activeSnapshot)
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
flushPendingChanges(): void {
|
|
2277
|
+
const current = this.current
|
|
2278
|
+
const pendingChanges = current?.pendingChanges
|
|
2279
|
+
if (pendingChanges === undefined || current.changes === undefined) {
|
|
2280
|
+
return
|
|
2281
|
+
}
|
|
2282
|
+
current.pendingChanges = undefined
|
|
2283
|
+
PubSub.publishUnsafe(current.changes, pendingChanges)
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
private updateState<E, R>(
|
|
2287
|
+
f: (state: unknown) => Effect.Effect<unknown, E, R>
|
|
2288
|
+
): Effect.Effect<void, E, R> {
|
|
2289
|
+
return Effect.suspend(() => {
|
|
2290
|
+
const observed = this.current
|
|
2291
|
+
if (observed.terminalizing || observed.snapshot.status !== "active") {
|
|
2292
|
+
return Effect.void
|
|
2293
|
+
}
|
|
2294
|
+
return f(observed.snapshot.state).pipe(
|
|
2295
|
+
Effect.flatMap((state) => {
|
|
2296
|
+
const latest = this.current
|
|
2297
|
+
return latest.terminalizing || latest.revision !== observed.revision
|
|
2298
|
+
? Effect.void
|
|
2299
|
+
: this.setActiveState(state)
|
|
2300
|
+
})
|
|
2301
|
+
)
|
|
2302
|
+
})
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
private getOrCreateChanges(): Effect.Effect<
|
|
2306
|
+
PubSub.PubSub<Take.Take<VersionedSnapshot<unknown, unknown, unknown>>> | undefined
|
|
2307
|
+
> {
|
|
2308
|
+
return Effect.suspend(() => {
|
|
2309
|
+
const observed = this.current
|
|
2310
|
+
if (observed.snapshot.status !== "active") {
|
|
2311
|
+
return Effect.succeed(undefined)
|
|
2312
|
+
}
|
|
2313
|
+
if (observed.changes !== undefined) {
|
|
2314
|
+
return Effect.succeed(observed.changes)
|
|
2315
|
+
}
|
|
2316
|
+
return PubSub.unbounded<Take.Take<VersionedSnapshot<unknown, unknown, unknown>>>({ replay: 1 }).pipe(
|
|
2317
|
+
Effect.flatMap((candidate) =>
|
|
2318
|
+
Effect.sync(() => {
|
|
2319
|
+
const latest = this.current
|
|
2320
|
+
if (latest.snapshot.status !== "active") {
|
|
2321
|
+
return [undefined, true] as const
|
|
2322
|
+
}
|
|
2323
|
+
if (latest.changes !== undefined) {
|
|
2324
|
+
return [latest.changes, true] as const
|
|
2325
|
+
}
|
|
2326
|
+
this.current = { ...latest, changes: candidate }
|
|
2327
|
+
return [candidate, false] as const
|
|
2328
|
+
}).pipe(
|
|
2329
|
+
Effect.flatMap(([changes, discard]) =>
|
|
2330
|
+
discard ? PubSub.shutdown(candidate).pipe(Effect.as(changes)) : Effect.succeed(changes)
|
|
2331
|
+
)
|
|
2332
|
+
)
|
|
2333
|
+
)
|
|
2334
|
+
)
|
|
2335
|
+
})
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
private changesStream(): Stream.Stream<RuntimeSnapshot<unknown, unknown, unknown>> {
|
|
2339
|
+
const self = this
|
|
2340
|
+
return Stream.unwrap(
|
|
2341
|
+
Effect.gen(function*() {
|
|
2342
|
+
const changes = yield* self.getOrCreateChanges()
|
|
2343
|
+
if (changes === undefined) {
|
|
2344
|
+
return Stream.succeed(self.current.snapshot)
|
|
2345
|
+
}
|
|
2346
|
+
const subscription = yield* PubSub.subscribe(changes)
|
|
2347
|
+
const captured = self.current
|
|
2348
|
+
if (captured.snapshot.status !== "active") {
|
|
2349
|
+
return Stream.succeed(captured.snapshot)
|
|
2350
|
+
}
|
|
2351
|
+
return Stream.succeed(captured.snapshot).pipe(
|
|
2352
|
+
Stream.concat(
|
|
2353
|
+
Stream.fromChannel(Channel.fromEffectTake(PubSub.take(subscription))).pipe(
|
|
2354
|
+
Stream.filter((next) => next.revision > captured.revision),
|
|
2355
|
+
Stream.map((next) => next.snapshot)
|
|
2356
|
+
)
|
|
2357
|
+
)
|
|
2358
|
+
)
|
|
2359
|
+
})
|
|
2360
|
+
)
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
class CompiledProcessContextImpl implements CompiledProcessContext<unknown, unknown> {
|
|
2365
|
+
executionState: unknown
|
|
2366
|
+
|
|
2367
|
+
constructor(
|
|
2368
|
+
readonly scope: ProcessScope<unknown>,
|
|
2369
|
+
readonly ownedChildren: OwnedChildRuntime,
|
|
2370
|
+
private readonly process: CompiledProcess
|
|
2371
|
+
) {}
|
|
2372
|
+
|
|
2373
|
+
poll(): Option.Option<unknown> {
|
|
2374
|
+
return Option.map(this.process.pollCompiledMessage(), messageEvent)
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
pollMessage(): Option.Option<ProcessMessage<unknown>> {
|
|
2378
|
+
return this.process.pollCompiledMessage()
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
state(): unknown {
|
|
2382
|
+
return this.process.compiledState()
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
completeMessage(delivery: AcknowledgedDelivery<unknown>): void {
|
|
2386
|
+
this.process.completeCompiledMessage(delivery)
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
commit(state: unknown): Effect.Effect<void> | undefined {
|
|
2390
|
+
return this.process.commitCompiledState(state)
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
runAfterChanges<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> {
|
|
2394
|
+
this.process.flushPendingChanges()
|
|
2395
|
+
return effect
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
const startCompactCompiledInternal: typeof startGenericInternal = Effect.fnUntraced(function*(
|
|
2400
|
+
logic: ProcessLogic<any, any, any, any, any, any>,
|
|
2401
|
+
options: StartInternalOptions
|
|
2402
|
+
) {
|
|
2403
|
+
const sessionId = yield* options.runtime.nextSessionId
|
|
2404
|
+
const services = yield* Effect.context<any>()
|
|
2405
|
+
const execution = logic.execution as CompiledProcessExecution<any, any, any, any, any, any>
|
|
2406
|
+
const process = new CompiledProcess(logic, options, services, sessionId)
|
|
2407
|
+
// A compiled initializer is synchronous by construction. Only startup
|
|
2408
|
+
// callbacks that themselves return Effects need the generic initialization
|
|
2409
|
+
// program; the compiled drain is still provided the complete service context.
|
|
2410
|
+
return yield* execution.initialSync !== undefined &&
|
|
2411
|
+
options.onReady === undefined && options.onSnapshot === undefined
|
|
2412
|
+
? process.initializeCompiledSync()
|
|
2413
|
+
: process.initialize()
|
|
2414
|
+
}) as typeof startGenericInternal
|
|
2415
|
+
|
|
2416
|
+
const startLogicInternal: typeof startGenericInternal = ((
|
|
2417
|
+
logic: ProcessLogic<any, any, any, any, any, any>,
|
|
2418
|
+
options: StartInternalOptions
|
|
2419
|
+
) =>
|
|
2420
|
+
logic.execution?._tag === "Compiled"
|
|
2421
|
+
? startCompactCompiledInternal(logic, options)
|
|
2422
|
+
: startGenericInternal(logic, options)) as typeof startGenericInternal
|
|
2423
|
+
|
|
2424
|
+
export type ProcessRuntimeStrategy = "generic" | "compiled" | "auto"
|
|
2425
|
+
|
|
2426
|
+
const startProcessWithStrategy = Effect.fnUntraced(function*(
|
|
2427
|
+
logic: ProcessLogic<any, any, any, any, any, any>,
|
|
2428
|
+
strategy: ProcessRuntimeStrategy,
|
|
2429
|
+
options?: {
|
|
2430
|
+
readonly id?: string
|
|
2431
|
+
}
|
|
2432
|
+
) {
|
|
2433
|
+
const runtime = yield* makeProcessRuntime
|
|
2434
|
+
const internalOptions: StartInternalOptions = options === undefined
|
|
2435
|
+
? {
|
|
2436
|
+
detached: true,
|
|
2437
|
+
runtime
|
|
2438
|
+
}
|
|
2439
|
+
: {
|
|
2440
|
+
...options,
|
|
2441
|
+
detached: true,
|
|
2442
|
+
runtime
|
|
2443
|
+
}
|
|
2444
|
+
if (strategy === "generic") {
|
|
2445
|
+
return yield* startGenericInternal(logic, internalOptions)
|
|
2446
|
+
}
|
|
2447
|
+
if (strategy === "compiled") {
|
|
2448
|
+
if (logic.execution?._tag !== "Compiled") {
|
|
2449
|
+
return yield* Effect.die(new Error("Machine cannot force the compiled runtime for generic process logic"))
|
|
2450
|
+
}
|
|
2451
|
+
return yield* startCompactCompiledInternal(logic, internalOptions)
|
|
2452
|
+
}
|
|
2453
|
+
return yield* startLogicInternal(logic, internalOptions)
|
|
2454
|
+
})
|
|
2455
|
+
|
|
2456
|
+
/** @internal Test-only startup strategy selection. */
|
|
2457
|
+
export const startProcessWithStrategyForTesting = <
|
|
2458
|
+
State,
|
|
2459
|
+
Event,
|
|
2460
|
+
Error = never,
|
|
2461
|
+
Requirements = never,
|
|
2462
|
+
Output = never,
|
|
2463
|
+
InitialError = never
|
|
2464
|
+
>(
|
|
2465
|
+
logic: ProcessLogic<State, Event, Error, Requirements, Output, InitialError>,
|
|
2466
|
+
strategy: ProcessRuntimeStrategy,
|
|
2467
|
+
options?: {
|
|
2468
|
+
readonly id?: string
|
|
2469
|
+
}
|
|
2470
|
+
): Effect.Effect<
|
|
2471
|
+
MachineRef<State, Event, Error, Output>,
|
|
2472
|
+
InitialError,
|
|
2473
|
+
Requirements
|
|
2474
|
+
> => startProcessWithStrategy(logic, strategy, options) as any
|
|
2475
|
+
|
|
2476
|
+
export const startProcess: <
|
|
2477
|
+
State,
|
|
2478
|
+
Event,
|
|
2479
|
+
Error = never,
|
|
2480
|
+
Requirements = never,
|
|
2481
|
+
Output = never,
|
|
2482
|
+
InitialError = never
|
|
2483
|
+
>(
|
|
2484
|
+
logic: ProcessLogic<State, Event, Error, Requirements, Output, InitialError>,
|
|
2485
|
+
options?: {
|
|
2486
|
+
readonly id?: string
|
|
2487
|
+
}
|
|
2488
|
+
) => Effect.Effect<
|
|
2489
|
+
MachineRef<State, Event, Error, Output>,
|
|
2490
|
+
InitialError,
|
|
2491
|
+
Requirements
|
|
2492
|
+
> = Effect.fnUntraced(function*<State, Event, Error, Requirements, Output, InitialError>(
|
|
2493
|
+
logic: ProcessLogic<State, Event, Error, Requirements, Output, InitialError>,
|
|
2494
|
+
options?: {
|
|
2495
|
+
readonly id?: string
|
|
2496
|
+
}
|
|
2497
|
+
) {
|
|
2498
|
+
const runtime = yield* makeProcessRuntime
|
|
2499
|
+
return yield* startLogicInternal(
|
|
2500
|
+
logic,
|
|
2501
|
+
options === undefined
|
|
2502
|
+
? {
|
|
2503
|
+
detached: true,
|
|
2504
|
+
runtime
|
|
2505
|
+
}
|
|
2506
|
+
: {
|
|
2507
|
+
...options,
|
|
2508
|
+
detached: true,
|
|
2509
|
+
runtime
|
|
2510
|
+
}
|
|
2511
|
+
)
|
|
2512
|
+
})
|