@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.
Files changed (38) hide show
  1. package/package.json +5 -5
  2. package/src/Machine.ts +6873 -0
  3. package/src/index.ts +1 -0
  4. package/src/internal/machine/activities.ts +108 -0
  5. package/src/internal/machine/atom.ts +636 -0
  6. package/src/internal/machine/cluster.ts +394 -0
  7. package/src/internal/machine/command.ts +58 -0
  8. package/src/internal/machine/commandRuntime.ts +43 -0
  9. package/src/internal/machine/configuration.ts +1331 -0
  10. package/src/internal/machine/errors.ts +87 -0
  11. package/src/internal/machine/executionPlan.ts +996 -0
  12. package/src/internal/machine/invocation.ts +119 -0
  13. package/src/internal/machine/machine.ts +1747 -0
  14. package/src/internal/machine/planner.ts +1933 -0
  15. package/src/internal/machine/process.ts +906 -0
  16. package/src/internal/machine/protocol.ts +322 -0
  17. package/src/internal/machine/readiness.ts +10 -0
  18. package/src/internal/machine/runtime.ts +2512 -0
  19. package/src/internal/machine/serialization.ts +498 -0
  20. package/src/internal/machine/stateDefinition.ts +270 -0
  21. package/src/internal/machine/symbols.ts +2 -0
  22. package/src/internal/machine/topology.ts +479 -0
  23. package/src/internal/testing/machine/arbitrary.ts +102 -0
  24. package/src/internal/testing/machine/exploration.ts +331 -0
  25. package/src/internal/testing/machine/finiteModel.ts +1498 -0
  26. package/src/internal/testing/machine/invariant.ts +372 -0
  27. package/src/internal/testing/machine/probe.ts +79 -0
  28. package/src/internal/testing/machine/referenceModel.ts +1505 -0
  29. package/src/internal/testing/machine/runtime.ts +1710 -0
  30. package/src/internal/testing/machine/runtimeInvariant.ts +486 -0
  31. package/src/internal/testing/machine/trace.ts +150 -0
  32. package/src/internal/testing/machine/verification.ts +1890 -0
  33. package/src/testing/MachineTest.ts +2067 -0
  34. package/src/testing/index.ts +7 -0
  35. package/src/unstable/cluster/ClusterMachine.ts +390 -0
  36. package/src/unstable/cluster/index.ts +1 -0
  37. package/src/unstable/reactivity/AtomMachine.ts +649 -0
  38. 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
+ )