@typeonce/effect-machine 0.6.0 → 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/package.json +5 -5
- 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,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs Effect machines as persisted Cluster entities.
|
|
3
|
+
*
|
|
4
|
+
* @since 0.4.0
|
|
5
|
+
*/
|
|
6
|
+
import * as Cause from "effect/Cause"
|
|
7
|
+
import * as Context from "effect/Context"
|
|
8
|
+
import * as Effect from "effect/Effect"
|
|
9
|
+
import * as Layer from "effect/Layer"
|
|
10
|
+
import * as Option from "effect/Option"
|
|
11
|
+
import * as Schema from "effect/Schema"
|
|
12
|
+
import { ClusterError, ClusterSchema, Entity, EntityAddress, MessageStorage, Snowflake } from "effect/unstable/cluster"
|
|
13
|
+
import { Rpc } from "effect/unstable/rpc"
|
|
14
|
+
import type * as Machine from "../../Machine.js"
|
|
15
|
+
import type { Checkpoint, ClusterMachine, LoadResult } from "../../unstable/cluster/ClusterMachine.js"
|
|
16
|
+
import * as internalMachine from "./machine.js"
|
|
17
|
+
import type { EnsureExecutable } from "./readiness.js"
|
|
18
|
+
|
|
19
|
+
type EntityAddress = EntityAddress.EntityAddress
|
|
20
|
+
type PersistenceError = ClusterError.PersistenceError
|
|
21
|
+
type Snowflake = Snowflake.Snowflake
|
|
22
|
+
|
|
23
|
+
export type CommitResult = CommitResult.Committed | CommitResult.Duplicate
|
|
24
|
+
|
|
25
|
+
export const CommitResult = {
|
|
26
|
+
Committed: (): CommitResult.Committed => ({ _tag: "Committed" }),
|
|
27
|
+
Duplicate: (): CommitResult.Duplicate => ({ _tag: "Duplicate" })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export declare namespace CommitResult {
|
|
31
|
+
/**
|
|
32
|
+
* Indicates that the request id and checkpoint were committed atomically.
|
|
33
|
+
*
|
|
34
|
+
* @category models
|
|
35
|
+
* @since 0.4.0
|
|
36
|
+
*/
|
|
37
|
+
export interface Committed {
|
|
38
|
+
readonly _tag: "Committed"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Indicates that the request id was already committed.
|
|
43
|
+
*
|
|
44
|
+
* @category models
|
|
45
|
+
* @since 0.4.0
|
|
46
|
+
*/
|
|
47
|
+
export interface Duplicate {
|
|
48
|
+
readonly _tag: "Duplicate"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class Storage extends Context.Service<Storage, {
|
|
53
|
+
readonly load: (
|
|
54
|
+
address: EntityAddress,
|
|
55
|
+
requestId: Snowflake
|
|
56
|
+
) => Effect.Effect<LoadResult, PersistenceError>
|
|
57
|
+
readonly commit: (
|
|
58
|
+
address: EntityAddress,
|
|
59
|
+
checkpoint: Checkpoint
|
|
60
|
+
) => Effect.Effect<CommitResult, PersistenceError>
|
|
61
|
+
}>()("effect/cluster/ClusterMachine/Storage") {}
|
|
62
|
+
|
|
63
|
+
export class Accepted extends Schema.TaggedClass<Accepted>("effect/cluster/ClusterMachine/Accepted")(
|
|
64
|
+
"Accepted",
|
|
65
|
+
{}
|
|
66
|
+
) {}
|
|
67
|
+
|
|
68
|
+
export const RejectionReason = Schema.Literals([
|
|
69
|
+
"MachineIdMismatch",
|
|
70
|
+
"VersionMismatch",
|
|
71
|
+
"InvalidCheckpoint",
|
|
72
|
+
"UnsupportedProcessLocal",
|
|
73
|
+
"TransitionFailure",
|
|
74
|
+
"PersistenceFailure",
|
|
75
|
+
"EmissionFailure"
|
|
76
|
+
])
|
|
77
|
+
|
|
78
|
+
export type RejectionReason = typeof RejectionReason.Type
|
|
79
|
+
|
|
80
|
+
export class Rejected extends Schema.TaggedClass<Rejected>("effect/cluster/ClusterMachine/Rejected")(
|
|
81
|
+
"Rejected",
|
|
82
|
+
{
|
|
83
|
+
reason: RejectionReason,
|
|
84
|
+
message: Schema.String
|
|
85
|
+
}
|
|
86
|
+
) {}
|
|
87
|
+
|
|
88
|
+
export const SendResult = Schema.Union([Accepted, Rejected])
|
|
89
|
+
|
|
90
|
+
type SendRpc<Events extends ReadonlyArray<Machine.Machine.TaggedSchema>> = Rpc.Rpc<
|
|
91
|
+
"send",
|
|
92
|
+
Schema.Union<Events>,
|
|
93
|
+
typeof SendResult
|
|
94
|
+
>
|
|
95
|
+
|
|
96
|
+
type MachineEvents<M extends Machine.Machine.Any> = Machine.Machine.InputEvents<M>
|
|
97
|
+
|
|
98
|
+
type MachineEmits<M extends Machine.Machine.Any> = Machine.Machine.Emits<M>
|
|
99
|
+
|
|
100
|
+
type IsAny<A> = 0 extends (1 & A) ? true : false
|
|
101
|
+
|
|
102
|
+
type ExcludeCompatibleRuntime<Requirements, Events, Emits> = Requirements extends Machine.Runtime.Requirement<
|
|
103
|
+
infer RequiredEvents,
|
|
104
|
+
infer RequiredEmits
|
|
105
|
+
> ? IsAny<Requirements> extends true ? Requirements
|
|
106
|
+
: [RequiredEvents] extends [Events] ? [RequiredEmits] extends [Emits] ? never : Requirements
|
|
107
|
+
: Requirements
|
|
108
|
+
: Requirements
|
|
109
|
+
|
|
110
|
+
const hasInvokes = (machine: Machine.Machine.Any): boolean =>
|
|
111
|
+
Reflect.ownKeys(machine.handlers).some((key) => machine.handlers[key as string]?.invoke !== undefined)
|
|
112
|
+
|
|
113
|
+
const reject = (reason: RejectionReason, message: string): Rejected => new Rejected({ reason, message })
|
|
114
|
+
|
|
115
|
+
const fail = (reason: RejectionReason, message: string): Effect.Effect<never, Rejected> =>
|
|
116
|
+
Effect.fail(reject(reason, message))
|
|
117
|
+
|
|
118
|
+
const messageFromCause = (cause: Cause.Cause<unknown>): string => {
|
|
119
|
+
const squashed = Cause.squash(cause)
|
|
120
|
+
return squashed instanceof globalThis.Error ? squashed.message : String(squashed)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const rejectionFromCause = (cause: Cause.Cause<unknown>): Rejected => {
|
|
124
|
+
const error = Cause.findErrorOption(cause)
|
|
125
|
+
if (Option.isSome(error) && error.value instanceof Rejected) {
|
|
126
|
+
return error.value
|
|
127
|
+
}
|
|
128
|
+
if (Option.isSome(error) && error.value instanceof internalMachine.ProcessLocalError) {
|
|
129
|
+
return reject("UnsupportedProcessLocal", `${error.value.operation} is process-local and is not supported`)
|
|
130
|
+
}
|
|
131
|
+
return reject("TransitionFailure", messageFromCause(cause))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const addressKey = (address: EntityAddress): string => `${address.entityType}\u0000${address.entityId}`
|
|
135
|
+
|
|
136
|
+
export const makeMemory: Effect.Effect<Storage["Service"]> = Effect.sync(() => {
|
|
137
|
+
const entries = new Map<string, {
|
|
138
|
+
checkpoint: Checkpoint
|
|
139
|
+
readonly requests: Set<Snowflake>
|
|
140
|
+
}>()
|
|
141
|
+
return Storage.of({
|
|
142
|
+
load: (address, requestId) =>
|
|
143
|
+
Effect.sync(() => {
|
|
144
|
+
const entry = entries.get(addressKey(address))
|
|
145
|
+
return {
|
|
146
|
+
checkpoint: Option.fromNullishOr(entry?.checkpoint),
|
|
147
|
+
processed: entry?.requests.has(requestId) ?? false
|
|
148
|
+
}
|
|
149
|
+
}),
|
|
150
|
+
commit: (address, checkpoint) =>
|
|
151
|
+
Effect.sync(() => {
|
|
152
|
+
const key = addressKey(address)
|
|
153
|
+
const entry = entries.get(key)
|
|
154
|
+
if (entry?.requests.has(checkpoint.requestId)) {
|
|
155
|
+
return CommitResult.Duplicate()
|
|
156
|
+
}
|
|
157
|
+
if (entry === undefined) {
|
|
158
|
+
entries.set(key, {
|
|
159
|
+
checkpoint,
|
|
160
|
+
requests: new Set([checkpoint.requestId])
|
|
161
|
+
})
|
|
162
|
+
} else {
|
|
163
|
+
entry.checkpoint = checkpoint
|
|
164
|
+
entry.requests.add(checkpoint.requestId)
|
|
165
|
+
}
|
|
166
|
+
return CommitResult.Committed()
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
export const layerMemory: Layer.Layer<Storage> = Layer.effect(Storage, makeMemory)
|
|
172
|
+
|
|
173
|
+
export const make = <
|
|
174
|
+
const Type extends string,
|
|
175
|
+
States extends Machine.Machine.StateSchemas,
|
|
176
|
+
Events extends ReadonlyArray<Machine.Machine.TaggedSchema>,
|
|
177
|
+
Input extends Schema.Top,
|
|
178
|
+
UnhandledStates extends Machine.Machine.StateIdentifier<States>,
|
|
179
|
+
E,
|
|
180
|
+
R,
|
|
181
|
+
InitialE,
|
|
182
|
+
InitialR,
|
|
183
|
+
FinalStates extends Machine.Machine.StateIdentifier<States>,
|
|
184
|
+
Output,
|
|
185
|
+
Emits extends ReadonlyArray<Machine.Machine.TaggedSchema>,
|
|
186
|
+
OutputStates extends Machine.Machine.StateIdentifier<States>,
|
|
187
|
+
InputEvents extends ReadonlyArray<Machine.Machine.TaggedSchema> = Events
|
|
188
|
+
>(
|
|
189
|
+
type: Type,
|
|
190
|
+
machine:
|
|
191
|
+
& Machine.Machine<
|
|
192
|
+
States,
|
|
193
|
+
Events,
|
|
194
|
+
Input,
|
|
195
|
+
UnhandledStates,
|
|
196
|
+
E,
|
|
197
|
+
R,
|
|
198
|
+
InitialE,
|
|
199
|
+
InitialR,
|
|
200
|
+
FinalStates,
|
|
201
|
+
Output,
|
|
202
|
+
Emits,
|
|
203
|
+
OutputStates,
|
|
204
|
+
InputEvents
|
|
205
|
+
>
|
|
206
|
+
& EnsureExecutable<States, UnhandledStates, OutputStates>,
|
|
207
|
+
options: {
|
|
208
|
+
readonly version: string
|
|
209
|
+
},
|
|
210
|
+
...input: [...Machine.Machine.InputArgs<Input>]
|
|
211
|
+
): ClusterMachine<
|
|
212
|
+
Type,
|
|
213
|
+
Machine.Machine<
|
|
214
|
+
States,
|
|
215
|
+
Events,
|
|
216
|
+
Input,
|
|
217
|
+
UnhandledStates,
|
|
218
|
+
E,
|
|
219
|
+
R,
|
|
220
|
+
InitialE,
|
|
221
|
+
InitialR,
|
|
222
|
+
FinalStates,
|
|
223
|
+
Output,
|
|
224
|
+
Emits,
|
|
225
|
+
OutputStates,
|
|
226
|
+
InputEvents
|
|
227
|
+
>,
|
|
228
|
+
| ExcludeCompatibleRuntime<
|
|
229
|
+
Machine.ExecutionServices<R | InitialR>,
|
|
230
|
+
Machine.Machine.EventOf<Events>,
|
|
231
|
+
Machine.Machine.EmitOf<Emits>
|
|
232
|
+
>
|
|
233
|
+
| Machine.Machine.SnapshotDecodingServices<States>
|
|
234
|
+
| Machine.Machine.SnapshotEncodingServices<States>
|
|
235
|
+
> => {
|
|
236
|
+
type M = Machine.Machine<
|
|
237
|
+
States,
|
|
238
|
+
Events,
|
|
239
|
+
Input,
|
|
240
|
+
UnhandledStates,
|
|
241
|
+
E,
|
|
242
|
+
R,
|
|
243
|
+
InitialE,
|
|
244
|
+
InitialR,
|
|
245
|
+
FinalStates,
|
|
246
|
+
Output,
|
|
247
|
+
Emits,
|
|
248
|
+
OutputStates,
|
|
249
|
+
InputEvents
|
|
250
|
+
>
|
|
251
|
+
const eventSchema = Schema.Union(machine.events as MachineEvents<M>)
|
|
252
|
+
const rpc = Rpc.make("send", {
|
|
253
|
+
payload: eventSchema,
|
|
254
|
+
success: SendResult
|
|
255
|
+
})
|
|
256
|
+
.annotate(ClusterSchema.Persisted, true) as SendRpc<MachineEvents<M>>
|
|
257
|
+
const entity = Entity.make(type, [rpc])
|
|
258
|
+
const machineId = machine.id ?? type
|
|
259
|
+
|
|
260
|
+
const toLayer: ClusterMachine<
|
|
261
|
+
Type,
|
|
262
|
+
M,
|
|
263
|
+
| ExcludeCompatibleRuntime<
|
|
264
|
+
Machine.ExecutionServices<R | InitialR>,
|
|
265
|
+
Machine.Machine.EventOf<Events>,
|
|
266
|
+
Machine.Machine.EmitOf<Emits>
|
|
267
|
+
>
|
|
268
|
+
| Machine.Machine.SnapshotDecodingServices<States>
|
|
269
|
+
| Machine.Machine.SnapshotEncodingServices<States>
|
|
270
|
+
>["toLayer"] = (layerOptions) =>
|
|
271
|
+
entity.toLayer(
|
|
272
|
+
Effect.gen(function*() {
|
|
273
|
+
const storage = yield* Storage
|
|
274
|
+
const messageStorage = yield* MessageStorage.MessageStorage
|
|
275
|
+
|
|
276
|
+
const handle = Effect.fnUntraced(function*(request: Entity.Request<SendRpc<MachineEvents<M>>>) {
|
|
277
|
+
if (hasInvokes(machine)) {
|
|
278
|
+
return yield* fail(
|
|
279
|
+
"UnsupportedProcessLocal",
|
|
280
|
+
"Machine invoke configurations are process-local and cannot be restored"
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const loaded = yield* storage.load(request.address, request.requestId).pipe(
|
|
285
|
+
Effect.mapError((error) => reject("PersistenceFailure", String(error.cause)))
|
|
286
|
+
)
|
|
287
|
+
let current: Machine.Machine.Snapshot<States> | undefined
|
|
288
|
+
const emitted: Array<Machine.Machine.EmitOf<MachineEmits<M>>> = []
|
|
289
|
+
|
|
290
|
+
if (Option.isSome(loaded.checkpoint)) {
|
|
291
|
+
const checkpoint = loaded.checkpoint.value
|
|
292
|
+
if (loaded.processed) {
|
|
293
|
+
return new Accepted({})
|
|
294
|
+
}
|
|
295
|
+
if (checkpoint.machineId !== machineId) {
|
|
296
|
+
return yield* fail(
|
|
297
|
+
"MachineIdMismatch",
|
|
298
|
+
`Expected machine id ${machineId}, received ${checkpoint.machineId}`
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
if (checkpoint.version !== options.version) {
|
|
302
|
+
return yield* fail(
|
|
303
|
+
"VersionMismatch",
|
|
304
|
+
`Expected version ${options.version}, received ${checkpoint.version}`
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
current = yield* internalMachine.decodeSnapshot(machine, checkpoint.snapshot).pipe(
|
|
308
|
+
Effect.mapError((error) => reject("InvalidCheckpoint", String(error.cause)))
|
|
309
|
+
)
|
|
310
|
+
} else if (loaded.processed) {
|
|
311
|
+
return yield* fail("InvalidCheckpoint", "The request was recorded without a checkpoint")
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (current === undefined) {
|
|
315
|
+
const initial = yield* internalMachine.planInitial(machine, ...input as any)
|
|
316
|
+
if (initial.commands.length > 0) {
|
|
317
|
+
return yield* fail(
|
|
318
|
+
"UnsupportedProcessLocal",
|
|
319
|
+
"Machine actor commands require a managed local process"
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
current = initial.state
|
|
323
|
+
emitted.push(...initial.emittedEvents as any)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (!internalMachine.isFinal(machine, current)) {
|
|
327
|
+
const planned = yield* internalMachine.plan(machine, current, request.payload)
|
|
328
|
+
if (planned.commands.length > 0) {
|
|
329
|
+
return yield* fail(
|
|
330
|
+
"UnsupportedProcessLocal",
|
|
331
|
+
"Machine actor commands require a managed local process"
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
current = planned.next
|
|
335
|
+
emitted.push(...planned.emittedEvents as any)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const encoded = yield* internalMachine.encodeSnapshot(machine, current)
|
|
339
|
+
if (emitted.length > 0 && layerOptions?.enqueue === undefined) {
|
|
340
|
+
return yield* fail("EmissionFailure", "No durable enqueue handler was configured")
|
|
341
|
+
}
|
|
342
|
+
const committed = yield* storage.commit(request.address, {
|
|
343
|
+
machineId,
|
|
344
|
+
version: options.version,
|
|
345
|
+
requestId: request.requestId,
|
|
346
|
+
snapshot: encoded
|
|
347
|
+
}).pipe(
|
|
348
|
+
Effect.mapError((error) => reject("PersistenceFailure", String(error.cause)))
|
|
349
|
+
)
|
|
350
|
+
if (committed._tag === "Duplicate") {
|
|
351
|
+
return new Accepted({})
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (layerOptions?.enqueue !== undefined) {
|
|
355
|
+
yield* Effect.forEach(emitted, layerOptions.enqueue, { discard: true }).pipe(
|
|
356
|
+
Effect.mapError((error) => reject("EmissionFailure", String(error)))
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
return new Accepted({})
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
return entity.of({
|
|
363
|
+
send: (request) =>
|
|
364
|
+
messageStorage.withTransaction(
|
|
365
|
+
handle(request).pipe(
|
|
366
|
+
Effect.catchCause((cause) =>
|
|
367
|
+
Cause.hasInterrupts(cause)
|
|
368
|
+
? Effect.failCause(cause)
|
|
369
|
+
: Effect.fail(rejectionFromCause(cause))
|
|
370
|
+
)
|
|
371
|
+
)
|
|
372
|
+
).pipe(
|
|
373
|
+
Effect.catchCause((cause) => {
|
|
374
|
+
if (Cause.hasInterrupts(cause)) {
|
|
375
|
+
return Effect.failCause(cause)
|
|
376
|
+
}
|
|
377
|
+
const error = Cause.findErrorOption(cause)
|
|
378
|
+
return Effect.succeed(
|
|
379
|
+
Option.isSome(error) && error.value instanceof Rejected
|
|
380
|
+
? error.value
|
|
381
|
+
: reject("PersistenceFailure", messageFromCause(cause))
|
|
382
|
+
)
|
|
383
|
+
})
|
|
384
|
+
) as any
|
|
385
|
+
})
|
|
386
|
+
}) as any
|
|
387
|
+
) as any
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
machine,
|
|
391
|
+
entity,
|
|
392
|
+
toLayer
|
|
393
|
+
}
|
|
394
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal machine command collection.
|
|
3
|
+
*
|
|
4
|
+
* @since 0.4.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Command, Enqueue, Machine } from "../../Machine.js"
|
|
8
|
+
import { decodeEmitSync, decodeEventSync } from "./protocol.js"
|
|
9
|
+
|
|
10
|
+
export type RuntimeCommand = Command
|
|
11
|
+
|
|
12
|
+
export interface Collected<Event> {
|
|
13
|
+
readonly enqueue: Enqueue<Event, unknown>
|
|
14
|
+
readonly commands: Array<RuntimeCommand>
|
|
15
|
+
readonly raisedEvents: Array<Event>
|
|
16
|
+
readonly emittedEvents: Array<unknown>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const targetBuilderCache = new WeakMap<object, Map<string, unknown>>()
|
|
20
|
+
|
|
21
|
+
export const getTargetBuilder = (machine: Machine.Any, path: string): any => {
|
|
22
|
+
let byPath = targetBuilderCache.get(machine)
|
|
23
|
+
if (byPath === undefined) {
|
|
24
|
+
byPath = new Map()
|
|
25
|
+
targetBuilderCache.set(machine, byPath)
|
|
26
|
+
}
|
|
27
|
+
if (byPath.has(path)) {
|
|
28
|
+
return byPath.get(path)
|
|
29
|
+
}
|
|
30
|
+
const builder = machine.makeTargetBuilder(path as any)
|
|
31
|
+
byPath.set(path, builder)
|
|
32
|
+
return builder
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const makeCollector = <Event>(machine: Machine.Any): Collected<Event> => {
|
|
36
|
+
const commands: Array<RuntimeCommand> = []
|
|
37
|
+
const raisedEvents: Array<Event> = []
|
|
38
|
+
const emittedEvents: Array<unknown> = []
|
|
39
|
+
return {
|
|
40
|
+
commands,
|
|
41
|
+
raisedEvents,
|
|
42
|
+
emittedEvents,
|
|
43
|
+
enqueue: {
|
|
44
|
+
raise: (event) => {
|
|
45
|
+
raisedEvents.push(decodeEventSync(machine, event) as Event)
|
|
46
|
+
},
|
|
47
|
+
emit: (event) => {
|
|
48
|
+
emittedEvents.push(decodeEmitSync(machine, event))
|
|
49
|
+
},
|
|
50
|
+
sendTo: (child: unknown, event: unknown) => {
|
|
51
|
+
commands.push({ _tag: "SendTo", child: child as any, event })
|
|
52
|
+
},
|
|
53
|
+
stop: (child: unknown) => {
|
|
54
|
+
commands.push({ _tag: "Stop", child: child as any })
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal process-side machine command execution.
|
|
3
|
+
*
|
|
4
|
+
* @since 0.4.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as Effect from "effect/Effect"
|
|
8
|
+
import type { Machine, Runtime } from "../../Machine.js"
|
|
9
|
+
import type { RuntimeCommand } from "./command.js"
|
|
10
|
+
import { decodeEmit, decodeEvent } from "./protocol.js"
|
|
11
|
+
import type { ProcessScope } from "./runtime.js"
|
|
12
|
+
|
|
13
|
+
export const makeLiveRuntime = <Events, Emits>(
|
|
14
|
+
machine: Machine.Any,
|
|
15
|
+
scope: ProcessScope<Events>
|
|
16
|
+
): Runtime<Events, Emits> => ({
|
|
17
|
+
raise: (event) =>
|
|
18
|
+
decodeEvent(machine, event).pipe(
|
|
19
|
+
Effect.flatMap((event) => scope.self.send(event as Events))
|
|
20
|
+
),
|
|
21
|
+
sendParent: (event) =>
|
|
22
|
+
decodeEmit(machine, event).pipe(
|
|
23
|
+
Effect.flatMap((event) => scope.sendParent(event))
|
|
24
|
+
)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export const runCommands = <Event>(
|
|
28
|
+
commands: Iterable<RuntimeCommand>,
|
|
29
|
+
scope: ProcessScope<Event>
|
|
30
|
+
) =>
|
|
31
|
+
Effect.forEach(commands, (command) =>
|
|
32
|
+
command._tag === "SendTo"
|
|
33
|
+
? scope.sendTo(command.child as never, command.event)
|
|
34
|
+
: scope.stopChild(command.child as never), { discard: true })
|
|
35
|
+
|
|
36
|
+
export const runEmittedEvents = <Events, Emits>(
|
|
37
|
+
events: Iterable<Emits>,
|
|
38
|
+
runtime: Runtime<Events, Emits>
|
|
39
|
+
) =>
|
|
40
|
+
Effect.all(
|
|
41
|
+
Array.from(events, (event) => runtime.sendParent(event)),
|
|
42
|
+
{ discard: true }
|
|
43
|
+
)
|