@typeonce/effect-machine 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +4 -3
  2. package/dist/Machine.d.ts +24 -6
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js.map +1 -1
  5. package/dist/internal/machine/atom.d.ts +5 -0
  6. package/dist/internal/machine/atom.d.ts.map +1 -1
  7. package/dist/internal/machine/atom.js +6 -3
  8. package/dist/internal/machine/atom.js.map +1 -1
  9. package/dist/internal/machine/machine.d.ts.map +1 -1
  10. package/dist/internal/machine/machine.js +1 -1
  11. package/dist/internal/machine/machine.js.map +1 -1
  12. package/dist/unstable/reactivity/AtomMachine.d.ts +26 -0
  13. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
  14. package/dist/unstable/reactivity/AtomMachine.js +23 -0
  15. package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
  16. package/docs/agent-guide.md +14 -0
  17. package/package.json +5 -5
  18. package/src/Machine.ts +6909 -0
  19. package/src/index.ts +1 -0
  20. package/src/internal/machine/activities.ts +108 -0
  21. package/src/internal/machine/atom.ts +684 -0
  22. package/src/internal/machine/cluster.ts +394 -0
  23. package/src/internal/machine/command.ts +58 -0
  24. package/src/internal/machine/commandRuntime.ts +43 -0
  25. package/src/internal/machine/configuration.ts +1331 -0
  26. package/src/internal/machine/errors.ts +87 -0
  27. package/src/internal/machine/executionPlan.ts +996 -0
  28. package/src/internal/machine/invocation.ts +119 -0
  29. package/src/internal/machine/machine.ts +1750 -0
  30. package/src/internal/machine/planner.ts +1933 -0
  31. package/src/internal/machine/process.ts +906 -0
  32. package/src/internal/machine/protocol.ts +322 -0
  33. package/src/internal/machine/readiness.ts +10 -0
  34. package/src/internal/machine/runtime.ts +2512 -0
  35. package/src/internal/machine/serialization.ts +498 -0
  36. package/src/internal/machine/stateDefinition.ts +270 -0
  37. package/src/internal/machine/symbols.ts +2 -0
  38. package/src/internal/machine/topology.ts +479 -0
  39. package/src/internal/testing/machine/arbitrary.ts +102 -0
  40. package/src/internal/testing/machine/exploration.ts +331 -0
  41. package/src/internal/testing/machine/finiteModel.ts +1498 -0
  42. package/src/internal/testing/machine/invariant.ts +372 -0
  43. package/src/internal/testing/machine/probe.ts +79 -0
  44. package/src/internal/testing/machine/referenceModel.ts +1505 -0
  45. package/src/internal/testing/machine/runtime.ts +1710 -0
  46. package/src/internal/testing/machine/runtimeInvariant.ts +486 -0
  47. package/src/internal/testing/machine/trace.ts +150 -0
  48. package/src/internal/testing/machine/verification.ts +1890 -0
  49. package/src/testing/MachineTest.ts +2067 -0
  50. package/src/testing/index.ts +7 -0
  51. package/src/unstable/cluster/ClusterMachine.ts +390 -0
  52. package/src/unstable/cluster/index.ts +1 -0
  53. package/src/unstable/reactivity/AtomMachine.ts +696 -0
  54. package/src/unstable/reactivity/index.ts +1 -0
@@ -0,0 +1,372 @@
1
+ /**
2
+ * User-defined semantic invariants over planner traces.
3
+ *
4
+ * @since 0.4.0
5
+ */
6
+
7
+ import * as Data from "effect/Data"
8
+ import * as Effect from "effect/Effect"
9
+ import * as Machine from "../../../Machine.js"
10
+ import type {
11
+ Invariant as MachineInvariant,
12
+ InvariantBuilder,
13
+ InvariantCheckResult,
14
+ InvariantOptions,
15
+ InvariantOutcome,
16
+ InvariantReport,
17
+ InvariantScope,
18
+ InvariantViolation,
19
+ StateInvariant,
20
+ StateInvariantContext,
21
+ StateInvariantOptions,
22
+ StateObservation,
23
+ StepInvariant,
24
+ StepInvariantContext,
25
+ Trace,
26
+ TraceInvariant,
27
+ TraceInvariantContext
28
+ } from "../../../testing/MachineTest.js"
29
+
30
+ type AnyMachine = Machine.Machine.Any
31
+
32
+ type StatePath<M extends AnyMachine> = Machine.Machine.StateIdentifier<Machine.Machine.States<M>>
33
+
34
+ interface Observation<M extends AnyMachine, Context> {
35
+ readonly context: Context
36
+ readonly observationIndex?: number
37
+ readonly eventIndex: number | undefined
38
+ readonly microstepIndex?: number
39
+ readonly phase?: StateObservation
40
+ readonly configuration?: ReadonlyArray<StatePath<M>>
41
+ readonly event?: Machine.Machine.Event<M> | Machine.InitialEvent
42
+ }
43
+
44
+ const validateName = (name: string): void => {
45
+ if (name.trim().length === 0) {
46
+ throw new Error("MachineTest.Invariant expected name to be a non-empty string")
47
+ }
48
+ }
49
+
50
+ const validateRequirement = <Context>(options: InvariantOptions<Context>): void => {
51
+ const minimum = options.require?.minObservations
52
+ if (minimum !== undefined && (!Number.isSafeInteger(minimum) || minimum < 0)) {
53
+ throw new Error("MachineTest.Invariant expected minObservations to be a non-negative safe integer")
54
+ }
55
+ }
56
+
57
+ export const stateInvariant = <M extends AnyMachine>(
58
+ name: string,
59
+ check: (context: StateInvariantContext<M>) => InvariantOutcome,
60
+ options: StateInvariantOptions<M> = {}
61
+ ): StateInvariant<M> => {
62
+ validateName(name)
63
+ validateRequirement(options)
64
+ return Object.freeze({
65
+ _tag: "StateInvariant" as const,
66
+ name,
67
+ observe: options.observe ?? "settled",
68
+ check,
69
+ ...(options.when === undefined ? {} : { when: options.when }),
70
+ ...(options.require === undefined ? {} : { require: Object.freeze({ ...options.require }) })
71
+ })
72
+ }
73
+
74
+ export const stepInvariant = <M extends AnyMachine>(
75
+ name: string,
76
+ check: (context: StepInvariantContext<M>) => InvariantOutcome,
77
+ options: InvariantOptions<StepInvariantContext<M>> = {}
78
+ ): StepInvariant<M> => {
79
+ validateName(name)
80
+ validateRequirement(options)
81
+ return Object.freeze({
82
+ _tag: "StepInvariant" as const,
83
+ name,
84
+ check,
85
+ ...(options.when === undefined ? {} : { when: options.when }),
86
+ ...(options.require === undefined ? {} : { require: Object.freeze({ ...options.require }) })
87
+ })
88
+ }
89
+
90
+ export const traceInvariant = <M extends AnyMachine>(
91
+ name: string,
92
+ check: (context: TraceInvariantContext<M>) => InvariantOutcome,
93
+ options: InvariantOptions<TraceInvariantContext<M>> = {}
94
+ ): TraceInvariant<M> => {
95
+ validateName(name)
96
+ validateRequirement(options)
97
+ return Object.freeze({
98
+ _tag: "TraceInvariant" as const,
99
+ name,
100
+ check,
101
+ ...(options.when === undefined ? {} : { when: options.when }),
102
+ ...(options.require === undefined ? {} : { require: Object.freeze({ ...options.require }) })
103
+ })
104
+ }
105
+
106
+ export const Invariant = Object.freeze({
107
+ state: stateInvariant,
108
+ step: stepInvariant,
109
+ trace: traceInvariant
110
+ })
111
+
112
+ export const invariants = <M extends AnyMachine>(_machine: M): InvariantBuilder<M> =>
113
+ Object.freeze({
114
+ state: (
115
+ name: string,
116
+ check: (context: StateInvariantContext<M>) => InvariantOutcome,
117
+ options?: StateInvariantOptions<M>
118
+ ) => stateInvariant(name, check, options),
119
+ step: (
120
+ name: string,
121
+ check: (context: StepInvariantContext<M>) => InvariantOutcome,
122
+ options?: InvariantOptions<StepInvariantContext<M>>
123
+ ) => stepInvariant(name, check, options),
124
+ trace: (
125
+ name: string,
126
+ check: (context: TraceInvariantContext<M>) => InvariantOutcome,
127
+ options?: InvariantOptions<TraceInvariantContext<M>>
128
+ ) => traceInvariant(name, check, options)
129
+ })
130
+
131
+ export class InvariantError<M extends AnyMachine = AnyMachine> extends Data.TaggedError(
132
+ "MachineTestInvariantError"
133
+ )<{
134
+ readonly trace: Trace<M>
135
+ readonly violations: ReadonlyArray<InvariantViolation<M>>
136
+ readonly report: InvariantReport
137
+ }> {}
138
+
139
+ const configuration = <M extends AnyMachine>(
140
+ machine: M,
141
+ snapshot: Machine.Machine.Snapshot<Machine.Machine.States<M>>
142
+ ): ReadonlyArray<StatePath<M>> => Machine.configuration(machine, snapshot).map(({ path }) => path)
143
+
144
+ const stateObservations = <M extends AnyMachine>(
145
+ machine: M,
146
+ trace: Trace<M>,
147
+ observe: StateInvariant<M>["observe"]
148
+ ): ReadonlyArray<Observation<M, StateInvariantContext<M>>> => {
149
+ const observations: Array<Observation<M, StateInvariantContext<M>>> = []
150
+ let observationIndex = 0
151
+ const add = (
152
+ snapshot: Machine.Machine.Snapshot<Machine.Machine.States<M>>,
153
+ paths: ReadonlyArray<StatePath<M>>,
154
+ phase: StateObservation,
155
+ eventIndex: number | undefined,
156
+ microstepIndex: number | undefined,
157
+ event: Machine.Machine.Event<M> | Machine.InitialEvent | undefined
158
+ ): void => {
159
+ const context: StateInvariantContext<M> = {
160
+ machine,
161
+ trace,
162
+ snapshot,
163
+ configuration: paths,
164
+ observationIndex,
165
+ phase,
166
+ eventIndex,
167
+ microstepIndex,
168
+ event
169
+ }
170
+ observations.push({
171
+ context,
172
+ observationIndex,
173
+ eventIndex,
174
+ ...(microstepIndex === undefined ? {} : { microstepIndex }),
175
+ phase,
176
+ configuration: paths,
177
+ ...(event === undefined ? {} : { event })
178
+ })
179
+ observationIndex += 1
180
+ }
181
+
182
+ if (observe === "final") {
183
+ add(trace.final, trace.finalConfiguration, "final", undefined, undefined, undefined)
184
+ return observations
185
+ }
186
+
187
+ if (observe === "settled" || observe === "all") {
188
+ add(
189
+ trace.initial.plan.state,
190
+ trace.initial.configuration,
191
+ "initial",
192
+ undefined,
193
+ undefined,
194
+ undefined
195
+ )
196
+ }
197
+
198
+ if (observe === "microsteps" || observe === "all") {
199
+ trace.initial.plan.microsteps.forEach((microstep, microstepIndex) => {
200
+ add(
201
+ microstep.next,
202
+ configuration(machine, microstep.next),
203
+ "microstep",
204
+ undefined,
205
+ microstepIndex,
206
+ microstep.event
207
+ )
208
+ })
209
+ }
210
+
211
+ for (const step of trace.steps) {
212
+ if (observe === "microsteps" || observe === "all") {
213
+ step.plan.microsteps.forEach((microstep, microstepIndex) => {
214
+ add(
215
+ microstep.next,
216
+ configuration(machine, microstep.next),
217
+ "microstep",
218
+ step.index,
219
+ microstepIndex,
220
+ microstep.event
221
+ )
222
+ })
223
+ }
224
+ if (observe === "settled" || observe === "all") {
225
+ add(
226
+ step.after,
227
+ step.afterConfiguration,
228
+ "event",
229
+ step.index,
230
+ undefined,
231
+ step.event
232
+ )
233
+ }
234
+ }
235
+
236
+ return observations
237
+ }
238
+
239
+ const stepObservations = <M extends AnyMachine>(
240
+ machine: M,
241
+ trace: Trace<M>
242
+ ): ReadonlyArray<Observation<M, StepInvariantContext<M>>> =>
243
+ trace.steps.map((step) => ({
244
+ context: {
245
+ machine,
246
+ trace,
247
+ step,
248
+ index: step.index,
249
+ before: step.before,
250
+ beforeConfiguration: step.beforeConfiguration,
251
+ event: step.event,
252
+ plan: step.plan,
253
+ after: step.after,
254
+ afterConfiguration: step.afterConfiguration
255
+ },
256
+ eventIndex: step.index,
257
+ event: step.event
258
+ }))
259
+
260
+ const traceObservations = <M extends AnyMachine>(
261
+ machine: M,
262
+ trace: Trace<M>
263
+ ): ReadonlyArray<Observation<M, TraceInvariantContext<M>>> => [{
264
+ context: { machine, trace },
265
+ eventIndex: undefined
266
+ }]
267
+
268
+ const scopeOf = <M extends AnyMachine>(invariant: MachineInvariant<M>): InvariantScope => {
269
+ switch (invariant._tag) {
270
+ case "StateInvariant":
271
+ return "state"
272
+ case "StepInvariant":
273
+ return "step"
274
+ case "TraceInvariant":
275
+ return "trace"
276
+ }
277
+ }
278
+
279
+ const messageOf = (outcome: InvariantOutcome): string | undefined =>
280
+ outcome === true ? undefined : typeof outcome === "string" ? outcome : "Invariant predicate returned false"
281
+
282
+ type EvaluatedInvariant<Context> = InvariantOptions<Context> & {
283
+ readonly name: string
284
+ readonly check: (context: Context) => InvariantOutcome
285
+ }
286
+
287
+ const evaluateInvariant = <M extends AnyMachine, Context>(
288
+ invariant: EvaluatedInvariant<Context>,
289
+ scope: InvariantScope,
290
+ observations: ReadonlyArray<Observation<M, Context>>
291
+ ): {
292
+ readonly check: InvariantCheckResult
293
+ readonly violations: ReadonlyArray<InvariantViolation<M>>
294
+ } => {
295
+ let observed = 0
296
+ let failures = 0
297
+ const violations: Array<InvariantViolation<M>> = []
298
+
299
+ for (const observation of observations) {
300
+ if (invariant.when !== undefined && !invariant.when(observation.context)) continue
301
+ observed += 1
302
+ const message = messageOf(invariant.check(observation.context))
303
+ if (message === undefined) continue
304
+ failures += 1
305
+ violations.push({
306
+ invariant: invariant.name,
307
+ scope,
308
+ kind: "predicate",
309
+ ...(observation.observationIndex === undefined ? {} : { observationIndex: observation.observationIndex }),
310
+ eventIndex: observation.eventIndex,
311
+ ...(observation.microstepIndex === undefined ? {} : { microstepIndex: observation.microstepIndex }),
312
+ ...(observation.phase === undefined ? {} : { phase: observation.phase }),
313
+ ...(observation.configuration === undefined ? {} : { configuration: observation.configuration }),
314
+ ...(observation.event === undefined ? {} : { event: observation.event }),
315
+ message
316
+ })
317
+ }
318
+
319
+ const minimum = invariant.require?.minObservations ?? 0
320
+ const insufficient = observed < minimum
321
+ if (insufficient) {
322
+ violations.push({
323
+ invariant: invariant.name,
324
+ scope,
325
+ kind: "observations",
326
+ eventIndex: undefined,
327
+ message: `Invariant required at least ${minimum} observation${minimum === 1 ? "" : "s"} but observed ${observed}`
328
+ })
329
+ }
330
+ return {
331
+ check: {
332
+ invariant: invariant.name,
333
+ scope,
334
+ status: failures > 0 ? "failed" : insufficient ? "insufficient" : observed === 0 ? "untested" : "passed",
335
+ observations: observed,
336
+ failures
337
+ },
338
+ violations
339
+ }
340
+ }
341
+
342
+ export const checkInvariants = <M extends AnyMachine>(
343
+ machine: M,
344
+ trace: Trace<M>,
345
+ invariants: ReadonlyArray<MachineInvariant<M>>
346
+ ): Effect.Effect<InvariantReport, InvariantError<M>> =>
347
+ Effect.suspend(() => {
348
+ const checks: Array<InvariantCheckResult> = []
349
+ const violations: Array<InvariantViolation<M>> = []
350
+
351
+ for (const invariant of invariants) {
352
+ const scope = scopeOf(invariant)
353
+ const result = invariant._tag === "StateInvariant"
354
+ ? evaluateInvariant(invariant, scope, stateObservations(machine, trace, invariant.observe))
355
+ : invariant._tag === "StepInvariant"
356
+ ? evaluateInvariant(invariant, scope, stepObservations(machine, trace))
357
+ : evaluateInvariant(invariant, scope, traceObservations(machine, trace))
358
+ checks.push(result.check)
359
+ violations.push(...result.violations)
360
+ }
361
+
362
+ const report: InvariantReport = { checks }
363
+ return violations.length === 0
364
+ ? Effect.succeed(report)
365
+ : Effect.fail(new InvariantError({ trace, violations, report }))
366
+ })
367
+
368
+ export const assertInvariants = <M extends AnyMachine>(
369
+ machine: M,
370
+ trace: Trace<M>,
371
+ invariants: ReadonlyArray<MachineInvariant<M>>
372
+ ): Effect.Effect<void, InvariantError<M>> => checkInvariants(machine, trace, invariants).pipe(Effect.asVoid)
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Causal testing access to a running statechart.
3
+ *
4
+ * @since 0.4.0
5
+ */
6
+
7
+ import * as Data from "effect/Data"
8
+ import * as Effect from "effect/Effect"
9
+ import type * as Machine from "../../../Machine.js"
10
+ import type { Probe, ProbePlan } from "../../../testing/MachineTest.js"
11
+ import * as Runtime from "../../machine/runtime.js"
12
+
13
+ type AnyMachine = Machine.Machine.Any
14
+
15
+ /**
16
+ * Raised when a reference was not created by the managed statechart runtime.
17
+ *
18
+ * @category errors
19
+ * @since 0.4.0
20
+ */
21
+ export class ProbeUnavailableError extends Data.TaggedError("MachineTestProbeUnavailableError")<{
22
+ readonly message: string
23
+ }> {}
24
+
25
+ /** @internal */
26
+ export const probe = <M extends AnyMachine, Error, Output>(
27
+ machine: M,
28
+ ref: Machine.MachineRef<
29
+ Machine.Machine.Snapshot<Machine.Machine.States<M>>,
30
+ Machine.Machine.InputEvent<M>,
31
+ Error,
32
+ Output
33
+ >
34
+ ): Effect.Effect<Probe<M, Error, Output>, ProbeUnavailableError> => {
35
+ const acknowledged = (ref as Runtime.MachineRef<
36
+ Machine.Machine.Snapshot<Machine.Machine.States<M>>,
37
+ Machine.Machine.InputEvent<M>,
38
+ Error,
39
+ Output
40
+ >)[Runtime.acknowledgedSend]
41
+ if (acknowledged === undefined) {
42
+ return Effect.fail(
43
+ new ProbeUnavailableError({
44
+ message: "MachineTest.probe requires a managed statechart reference created by Machine.start or Machine.resume"
45
+ })
46
+ )
47
+ }
48
+
49
+ return Effect.succeed(Object.freeze({
50
+ machine,
51
+ ref,
52
+ await: Object.freeze({
53
+ none: { _tag: "None" } as const,
54
+ until: (
55
+ predicate: (
56
+ snapshot: Machine.RuntimeSnapshot<
57
+ Machine.Machine.Snapshot<Machine.Machine.States<M>>,
58
+ Error,
59
+ Output
60
+ >
61
+ ) => boolean
62
+ ) => ({ _tag: "Until", predicate } as const)
63
+ }),
64
+ sendAndAwait: (event: Machine.Machine.InputEvent<M>) =>
65
+ acknowledged.call(ref, event).pipe(
66
+ Effect.map(({ after, before, plan }) => {
67
+ const eventPlan = plan as ProbePlan<M>
68
+ return Object.freeze({
69
+ event,
70
+ before,
71
+ plan: eventPlan,
72
+ after,
73
+ handled: eventPlan.microsteps.length > 0,
74
+ configurationChanged: eventPlan.microsteps.some((microstep) => microstep.changed)
75
+ })
76
+ })
77
+ )
78
+ }))
79
+ }