@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,479 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal machine topology and target helpers.
|
|
3
|
+
*
|
|
4
|
+
* @since 0.4.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as Option from "effect/Option"
|
|
8
|
+
import { hasProperty } from "effect/Predicate"
|
|
9
|
+
import * as Schema from "effect/Schema"
|
|
10
|
+
import type { Machine } from "../../Machine.js"
|
|
11
|
+
|
|
12
|
+
export const TargetTypeId = "~effect/Machine/Target"
|
|
13
|
+
|
|
14
|
+
export const TargetSnapshotTypeId: unique symbol = Symbol("effect/Machine/TargetSnapshot")
|
|
15
|
+
|
|
16
|
+
export const StateInputTypeId: unique symbol = Symbol("effect/Machine/StateInput")
|
|
17
|
+
|
|
18
|
+
export const StateConstructionTypeId: unique symbol = Symbol("effect/Machine/StateConstruction")
|
|
19
|
+
|
|
20
|
+
export const HistoryTargetTypeId: unique symbol = Symbol("effect/Machine/HistoryTarget")
|
|
21
|
+
|
|
22
|
+
export const ChoiceTargetTypeId: unique symbol = Symbol("effect/Machine/ChoiceTarget")
|
|
23
|
+
|
|
24
|
+
interface StateInput {
|
|
25
|
+
readonly [StateInputTypeId]: typeof StateInputTypeId
|
|
26
|
+
readonly input: unknown
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Internal target produced by the history target builder. History nodes are
|
|
30
|
+
* routing instructions and are never part of an active configuration. */
|
|
31
|
+
export interface HistoryTarget {
|
|
32
|
+
readonly [HistoryTargetTypeId]: typeof HistoryTargetTypeId
|
|
33
|
+
readonly path: string
|
|
34
|
+
readonly parent: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Internal target produced by a choice target builder. */
|
|
38
|
+
export interface ChoiceTarget {
|
|
39
|
+
readonly [ChoiceTargetTypeId]: typeof ChoiceTargetTypeId
|
|
40
|
+
readonly path: string
|
|
41
|
+
readonly parent: string
|
|
42
|
+
readonly values?: Readonly<Record<string, unknown>>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const makeHistoryTarget = (path: string, parent: string): HistoryTarget => ({
|
|
46
|
+
[HistoryTargetTypeId]: HistoryTargetTypeId,
|
|
47
|
+
path,
|
|
48
|
+
parent
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
export const isHistoryTarget = (u: unknown): u is HistoryTarget => hasProperty(u, HistoryTargetTypeId)
|
|
52
|
+
|
|
53
|
+
export const makeChoiceTarget = (
|
|
54
|
+
path: string,
|
|
55
|
+
parent: string,
|
|
56
|
+
values?: Readonly<Record<string, unknown>>
|
|
57
|
+
): ChoiceTarget => ({
|
|
58
|
+
[ChoiceTargetTypeId]: ChoiceTargetTypeId,
|
|
59
|
+
path,
|
|
60
|
+
parent,
|
|
61
|
+
...(values === undefined ? {} : { values })
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
export const isChoiceTarget = (u: unknown): u is ChoiceTarget => hasProperty(u, ChoiceTargetTypeId)
|
|
65
|
+
|
|
66
|
+
interface NormalizedStateNodeDefinitionBase {
|
|
67
|
+
readonly annotations: Readonly<Machine.StateNodeAnnotations> | undefined
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type NormalizedStateNodeDefinition =
|
|
71
|
+
| (NormalizedStateNodeDefinitionBase & {
|
|
72
|
+
readonly type: "atomic"
|
|
73
|
+
readonly schema: Machine.TaggedSchema
|
|
74
|
+
readonly output: undefined
|
|
75
|
+
readonly history: undefined
|
|
76
|
+
readonly initial: undefined
|
|
77
|
+
readonly states: undefined
|
|
78
|
+
})
|
|
79
|
+
| (NormalizedStateNodeDefinitionBase & {
|
|
80
|
+
readonly type: "compound"
|
|
81
|
+
readonly schema: Machine.TaggedSchema
|
|
82
|
+
readonly output: undefined
|
|
83
|
+
readonly history: undefined
|
|
84
|
+
readonly initial: string
|
|
85
|
+
readonly states: Machine.StateTree
|
|
86
|
+
})
|
|
87
|
+
| (NormalizedStateNodeDefinitionBase & {
|
|
88
|
+
readonly type: "parallel"
|
|
89
|
+
readonly schema: Machine.TaggedSchema
|
|
90
|
+
readonly output: Schema.Top | undefined
|
|
91
|
+
readonly history: undefined
|
|
92
|
+
readonly initial: undefined
|
|
93
|
+
readonly states: Machine.StateTree
|
|
94
|
+
})
|
|
95
|
+
| (NormalizedStateNodeDefinitionBase & {
|
|
96
|
+
readonly type: "final"
|
|
97
|
+
readonly schema: Machine.TaggedSchema
|
|
98
|
+
readonly output: Schema.Top | undefined
|
|
99
|
+
readonly history: undefined
|
|
100
|
+
readonly initial: undefined
|
|
101
|
+
readonly states: undefined
|
|
102
|
+
})
|
|
103
|
+
| (NormalizedStateNodeDefinitionBase & {
|
|
104
|
+
readonly type: "history"
|
|
105
|
+
readonly schema: undefined
|
|
106
|
+
readonly output: undefined
|
|
107
|
+
readonly history: "shallow" | "deep"
|
|
108
|
+
readonly initial: undefined
|
|
109
|
+
readonly states: undefined
|
|
110
|
+
})
|
|
111
|
+
| (NormalizedStateNodeDefinitionBase & {
|
|
112
|
+
readonly type: "choice"
|
|
113
|
+
readonly schema: undefined
|
|
114
|
+
readonly output: undefined
|
|
115
|
+
readonly history: undefined
|
|
116
|
+
readonly initial: undefined
|
|
117
|
+
readonly states: undefined
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
export const getStateNodeDefinition = (
|
|
121
|
+
path: string,
|
|
122
|
+
definition: Machine.TaggedSchema | Machine.StateNodeConfig
|
|
123
|
+
): NormalizedStateNodeDefinition => {
|
|
124
|
+
if (!Schema.isSchema(definition) && definition.type === "history") {
|
|
125
|
+
return {
|
|
126
|
+
schema: undefined,
|
|
127
|
+
output: undefined,
|
|
128
|
+
annotations: definition.annotations,
|
|
129
|
+
type: "history",
|
|
130
|
+
history: definition.history === "deep" ? "deep" : "shallow",
|
|
131
|
+
initial: undefined,
|
|
132
|
+
states: undefined
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (!Schema.isSchema(definition) && definition.type === "choice") {
|
|
136
|
+
return {
|
|
137
|
+
schema: undefined,
|
|
138
|
+
output: undefined,
|
|
139
|
+
annotations: definition.annotations,
|
|
140
|
+
type: "choice",
|
|
141
|
+
history: undefined,
|
|
142
|
+
initial: undefined,
|
|
143
|
+
states: undefined
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (Schema.isSchema(definition)) {
|
|
147
|
+
return {
|
|
148
|
+
schema: definition,
|
|
149
|
+
output: undefined,
|
|
150
|
+
annotations: Schema.resolveAnnotations(definition),
|
|
151
|
+
type: "atomic",
|
|
152
|
+
history: undefined,
|
|
153
|
+
initial: undefined,
|
|
154
|
+
states: undefined
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (!hasProperty(definition, "schema") || !Schema.isSchema(definition.schema)) {
|
|
158
|
+
throw new Error(`Machine.make expected state "${path}" to be a tagged schema or state node config`)
|
|
159
|
+
}
|
|
160
|
+
if (definition.type === "parallel" && !hasProperty(definition, "states")) {
|
|
161
|
+
throw new Error(`Machine.make expected parallel state "${path}" to declare child regions`)
|
|
162
|
+
}
|
|
163
|
+
if (hasProperty(definition, "states")) {
|
|
164
|
+
const type: unknown = definition.type
|
|
165
|
+
if (type === "final") {
|
|
166
|
+
throw new Error(`Machine.make expected compound state "${path}" to be active`)
|
|
167
|
+
}
|
|
168
|
+
if (definition.type === "parallel") {
|
|
169
|
+
return {
|
|
170
|
+
schema: definition.schema,
|
|
171
|
+
output: Schema.isSchema(definition.output) ? definition.output : undefined,
|
|
172
|
+
annotations: Schema.resolveAnnotations(definition.schema),
|
|
173
|
+
type: "parallel",
|
|
174
|
+
history: undefined,
|
|
175
|
+
initial: undefined,
|
|
176
|
+
states: definition.states
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (typeof definition.initial !== "string") {
|
|
180
|
+
throw new Error(`Machine.make expected compound state "${path}" to declare an initial child`)
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
schema: definition.schema,
|
|
184
|
+
output: undefined,
|
|
185
|
+
annotations: Schema.resolveAnnotations(definition.schema),
|
|
186
|
+
type: "compound",
|
|
187
|
+
history: undefined,
|
|
188
|
+
initial: definition.initial,
|
|
189
|
+
states: definition.states
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const output = Schema.isSchema(definition.output) ? definition.output : undefined
|
|
193
|
+
return definition.type === "final"
|
|
194
|
+
? {
|
|
195
|
+
schema: definition.schema,
|
|
196
|
+
output,
|
|
197
|
+
annotations: Schema.resolveAnnotations(definition.schema),
|
|
198
|
+
type: "final",
|
|
199
|
+
history: undefined,
|
|
200
|
+
initial: undefined,
|
|
201
|
+
states: undefined
|
|
202
|
+
}
|
|
203
|
+
: {
|
|
204
|
+
schema: definition.schema,
|
|
205
|
+
output: undefined,
|
|
206
|
+
annotations: Schema.resolveAnnotations(definition.schema),
|
|
207
|
+
type: "atomic",
|
|
208
|
+
history: undefined,
|
|
209
|
+
initial: undefined,
|
|
210
|
+
states: undefined
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export const compileStateNodes = (states: Machine.StateSchemas): Machine.StateNodes => {
|
|
215
|
+
const byPath = new Map<string, Machine.StateNode>()
|
|
216
|
+
let order = 0
|
|
217
|
+
|
|
218
|
+
const compile = (tree: Machine.StateTree, parent: string | undefined): ReadonlyArray<string> => {
|
|
219
|
+
const paths: Array<string> = []
|
|
220
|
+
for (const key of Object.keys(tree)) {
|
|
221
|
+
if (key.includes(".")) {
|
|
222
|
+
throw new Error(`Machine state keys cannot contain ".": "${key}"`)
|
|
223
|
+
}
|
|
224
|
+
const path = parent === undefined ? key : `${parent}.${key}`
|
|
225
|
+
const definition = getStateNodeDefinition(path, tree[key]!)
|
|
226
|
+
let node: Machine.StateNode
|
|
227
|
+
let childStates: Machine.StateTree | undefined
|
|
228
|
+
const base = { path, key, annotations: definition.annotations, order }
|
|
229
|
+
switch (definition.type) {
|
|
230
|
+
case "atomic":
|
|
231
|
+
node = {
|
|
232
|
+
...base,
|
|
233
|
+
type: "atomic",
|
|
234
|
+
schema: definition.schema,
|
|
235
|
+
output: undefined,
|
|
236
|
+
history: undefined,
|
|
237
|
+
parent,
|
|
238
|
+
children: [],
|
|
239
|
+
initial: undefined
|
|
240
|
+
}
|
|
241
|
+
break
|
|
242
|
+
case "compound":
|
|
243
|
+
node = {
|
|
244
|
+
...base,
|
|
245
|
+
type: "compound",
|
|
246
|
+
schema: definition.schema,
|
|
247
|
+
output: undefined,
|
|
248
|
+
history: undefined,
|
|
249
|
+
parent,
|
|
250
|
+
children: [],
|
|
251
|
+
initial: `${path}.${definition.initial}`
|
|
252
|
+
}
|
|
253
|
+
childStates = definition.states
|
|
254
|
+
break
|
|
255
|
+
case "parallel":
|
|
256
|
+
node = {
|
|
257
|
+
...base,
|
|
258
|
+
type: "parallel",
|
|
259
|
+
schema: definition.schema,
|
|
260
|
+
output: definition.output,
|
|
261
|
+
history: undefined,
|
|
262
|
+
parent,
|
|
263
|
+
children: [],
|
|
264
|
+
initial: undefined
|
|
265
|
+
}
|
|
266
|
+
childStates = definition.states
|
|
267
|
+
break
|
|
268
|
+
case "final":
|
|
269
|
+
node = {
|
|
270
|
+
...base,
|
|
271
|
+
type: "final",
|
|
272
|
+
schema: definition.schema,
|
|
273
|
+
output: definition.output,
|
|
274
|
+
history: undefined,
|
|
275
|
+
parent,
|
|
276
|
+
children: [],
|
|
277
|
+
initial: undefined
|
|
278
|
+
}
|
|
279
|
+
break
|
|
280
|
+
case "history":
|
|
281
|
+
if (parent === undefined) {
|
|
282
|
+
throw new Error(`Machine history state "${path}" must belong to a parent state`)
|
|
283
|
+
}
|
|
284
|
+
node = {
|
|
285
|
+
...base,
|
|
286
|
+
type: "history",
|
|
287
|
+
schema: undefined,
|
|
288
|
+
output: undefined,
|
|
289
|
+
history: definition.history,
|
|
290
|
+
parent,
|
|
291
|
+
children: [],
|
|
292
|
+
initial: undefined
|
|
293
|
+
}
|
|
294
|
+
break
|
|
295
|
+
case "choice":
|
|
296
|
+
if (parent === undefined) {
|
|
297
|
+
throw new Error(`Machine choice state "${path}" must belong to a parent state`)
|
|
298
|
+
}
|
|
299
|
+
node = {
|
|
300
|
+
...base,
|
|
301
|
+
type: "choice",
|
|
302
|
+
schema: undefined,
|
|
303
|
+
output: undefined,
|
|
304
|
+
history: undefined,
|
|
305
|
+
parent,
|
|
306
|
+
children: [],
|
|
307
|
+
initial: undefined
|
|
308
|
+
}
|
|
309
|
+
break
|
|
310
|
+
}
|
|
311
|
+
byPath.set(path, node)
|
|
312
|
+
order += 1
|
|
313
|
+
if (definition.type === "history" || definition.type === "choice") {
|
|
314
|
+
continue
|
|
315
|
+
}
|
|
316
|
+
paths.push(path)
|
|
317
|
+
if (childStates !== undefined) {
|
|
318
|
+
const children = compile(childStates, path)
|
|
319
|
+
if (node.type === "compound") {
|
|
320
|
+
if (!children.includes(node.initial) && byPath.get(node.initial)?.type !== "choice") {
|
|
321
|
+
throw new Error(`Machine.make expected compound state "${path}" initial child to exist`)
|
|
322
|
+
}
|
|
323
|
+
node = { ...node, children }
|
|
324
|
+
} else if (node.type === "parallel") {
|
|
325
|
+
node = { ...node, children }
|
|
326
|
+
} else {
|
|
327
|
+
throw new Error(`Machine state "${path}" cannot declare child states`)
|
|
328
|
+
}
|
|
329
|
+
byPath.set(path, node)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return paths
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
byPath,
|
|
337
|
+
roots: compile(states, undefined)
|
|
338
|
+
} as Machine.StateNodes
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const dynamicTransitionTargets = { type: "dynamic" } as const
|
|
342
|
+
|
|
343
|
+
const transitionTargets = (handler: unknown): Machine.TransitionTargets =>
|
|
344
|
+
typeof handler === "object" && handler !== null && "targets" in handler && handler.targets !== undefined
|
|
345
|
+
? { type: "declared", paths: Array.from(handler.targets as ReadonlyArray<string>) }
|
|
346
|
+
: dynamicTransitionTargets
|
|
347
|
+
|
|
348
|
+
export const transitionDefinitions = (
|
|
349
|
+
machine: Machine.Any
|
|
350
|
+
): ReadonlyArray<Machine.TransitionDefinition> => {
|
|
351
|
+
const definitions: Array<Machine.TransitionDefinition> = []
|
|
352
|
+
for (const node of machine.stateNodes.byPath.values()) {
|
|
353
|
+
const config = machine.handlers[node.path] as Machine.AnyStateConfig | undefined
|
|
354
|
+
if (config === undefined) {
|
|
355
|
+
continue
|
|
356
|
+
}
|
|
357
|
+
if (node.type === "choice") {
|
|
358
|
+
const choice = (config as any).choice
|
|
359
|
+
if (choice !== undefined) {
|
|
360
|
+
definitions.push({
|
|
361
|
+
source: node.path,
|
|
362
|
+
trigger: { type: "choice" },
|
|
363
|
+
reenter: false,
|
|
364
|
+
targets: transitionTargets(choice)
|
|
365
|
+
})
|
|
366
|
+
}
|
|
367
|
+
continue
|
|
368
|
+
}
|
|
369
|
+
for (const event of Reflect.ownKeys(config.on ?? {})) {
|
|
370
|
+
const handler = config.on?.[event]
|
|
371
|
+
definitions.push({
|
|
372
|
+
source: node.path,
|
|
373
|
+
trigger: { type: "event", event },
|
|
374
|
+
reenter: typeof handler === "object" && handler !== null && handler.reenter === true,
|
|
375
|
+
targets: transitionTargets(handler)
|
|
376
|
+
})
|
|
377
|
+
}
|
|
378
|
+
if (config.always !== undefined) {
|
|
379
|
+
definitions.push({
|
|
380
|
+
source: node.path,
|
|
381
|
+
trigger: { type: "always" },
|
|
382
|
+
reenter: false,
|
|
383
|
+
targets: transitionTargets(config.always)
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
if (config.onDone !== undefined) {
|
|
387
|
+
definitions.push({
|
|
388
|
+
source: node.path,
|
|
389
|
+
trigger: { type: "done" },
|
|
390
|
+
reenter: false,
|
|
391
|
+
targets: transitionTargets(config.onDone)
|
|
392
|
+
})
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return definitions
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export const makeTarget = <
|
|
399
|
+
const States extends Machine.StateSchemas,
|
|
400
|
+
const StateId extends Machine.StateIdentifier<States>
|
|
401
|
+
>(
|
|
402
|
+
path: StateId,
|
|
403
|
+
value: Machine.StateByIdentifier<States, StateId>,
|
|
404
|
+
options?: {
|
|
405
|
+
readonly snapshot?: Machine.SnapshotByIdentifier<States, StateId>
|
|
406
|
+
readonly values?: Partial<
|
|
407
|
+
{
|
|
408
|
+
readonly [AncestorStateId in Machine.StateIdentifier<States>]: Machine.StateByIdentifier<
|
|
409
|
+
States,
|
|
410
|
+
AncestorStateId
|
|
411
|
+
>
|
|
412
|
+
}
|
|
413
|
+
>
|
|
414
|
+
}
|
|
415
|
+
): Machine.Target<States, StateId> =>
|
|
416
|
+
({
|
|
417
|
+
[TargetTypeId]: TargetTypeId,
|
|
418
|
+
[TargetSnapshotTypeId]: options?.snapshot,
|
|
419
|
+
path,
|
|
420
|
+
value,
|
|
421
|
+
values: options?.values
|
|
422
|
+
}) as Machine.Target<States, StateId>
|
|
423
|
+
|
|
424
|
+
export const isTarget = (u: unknown): u is Machine.Target<any, any> => hasProperty(u, TargetTypeId)
|
|
425
|
+
|
|
426
|
+
export const makeStateInput = (input: unknown): StateInput => ({
|
|
427
|
+
[StateInputTypeId]: StateInputTypeId,
|
|
428
|
+
input
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
export const isStateInput = (u: unknown): u is StateInput => hasProperty(u, StateInputTypeId)
|
|
432
|
+
|
|
433
|
+
export const isSnapshot = (u: unknown): u is Machine.AtomicSnapshot<string, unknown> =>
|
|
434
|
+
hasProperty(u, "path") && hasProperty(u, "value")
|
|
435
|
+
|
|
436
|
+
export const getSnapshotByPath = (
|
|
437
|
+
snapshot: Machine.AtomicSnapshot<string, unknown>,
|
|
438
|
+
path: string,
|
|
439
|
+
parents?: Record<string, unknown>
|
|
440
|
+
): Option.Option<Machine.AtomicSnapshot<string, unknown>> => {
|
|
441
|
+
if (snapshot.path === path) {
|
|
442
|
+
return Option.some(snapshot)
|
|
443
|
+
}
|
|
444
|
+
if (!path.startsWith(`${snapshot.path}.`)) {
|
|
445
|
+
return Option.none()
|
|
446
|
+
}
|
|
447
|
+
if (parents !== undefined) {
|
|
448
|
+
parents[snapshot.path] = snapshot.value
|
|
449
|
+
}
|
|
450
|
+
if (hasProperty(snapshot, "state") && isSnapshot(snapshot.state)) {
|
|
451
|
+
return getSnapshotByPath(snapshot.state, path, parents)
|
|
452
|
+
}
|
|
453
|
+
if (hasProperty(snapshot, "states") && typeof snapshot.states === "object" && snapshot.states !== null) {
|
|
454
|
+
for (const child of Object.values(snapshot.states)) {
|
|
455
|
+
if (isSnapshot(child)) {
|
|
456
|
+
const result = getSnapshotByPath(child, path, parents)
|
|
457
|
+
if (Option.isSome(result)) {
|
|
458
|
+
return result
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return Option.none()
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export const getNode = (machine: Machine.Any, path: string): Machine.StateNode => {
|
|
467
|
+
const node = machine.stateNodes.byPath.get(path)
|
|
468
|
+
if (node === undefined) {
|
|
469
|
+
throw new Error(`Machine expected state path "${path}" to exist`)
|
|
470
|
+
}
|
|
471
|
+
return node
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export const getStateNodeSchema = (node: Machine.StateNode): Machine.TaggedSchema => {
|
|
475
|
+
if (node.schema === undefined) {
|
|
476
|
+
throw new Error(`Machine pseudo-state "${node.path}" has no active value schema`)
|
|
477
|
+
}
|
|
478
|
+
return node.schema
|
|
479
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema"
|
|
2
|
+
import * as SchemaAST from "effect/SchemaAST"
|
|
3
|
+
import { FastCheck } from "effect/testing"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Warning emitted when schema arbitrary generation must enforce an opaque
|
|
7
|
+
* filter through rejection sampling.
|
|
8
|
+
*
|
|
9
|
+
* @category models
|
|
10
|
+
* @since 0.4.0
|
|
11
|
+
*/
|
|
12
|
+
export interface SchemaArbitraryOpaqueFilterWarning {
|
|
13
|
+
readonly _tag: "OpaqueFilter"
|
|
14
|
+
readonly path: ReadonlyArray<PropertyKey>
|
|
15
|
+
readonly description?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Non-fatal diagnostic emitted while deriving a schema arbitrary.
|
|
20
|
+
*
|
|
21
|
+
* @category models
|
|
22
|
+
* @since 0.4.0
|
|
23
|
+
*/
|
|
24
|
+
export type SchemaArbitraryWarning = SchemaArbitraryOpaqueFilterWarning
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Diagnostics collected while deriving a schema arbitrary.
|
|
28
|
+
*
|
|
29
|
+
* @category models
|
|
30
|
+
* @since 0.4.0
|
|
31
|
+
*/
|
|
32
|
+
export interface SchemaArbitraryReport {
|
|
33
|
+
readonly warnings: ReadonlyArray<SchemaArbitraryWarning>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const reportChecks = (
|
|
37
|
+
warnings: Array<SchemaArbitraryWarning>,
|
|
38
|
+
checks: SchemaAST.Checks | undefined,
|
|
39
|
+
path: ReadonlyArray<PropertyKey>
|
|
40
|
+
): void => {
|
|
41
|
+
const visit = (check: SchemaAST.Check<unknown>, covered: boolean): void => {
|
|
42
|
+
const arbitrary = check.annotations?.arbitrary
|
|
43
|
+
const nextCovered = covered || arbitrary?.constraint !== undefined || arbitrary?.candidate !== undefined
|
|
44
|
+
if (check._tag !== "Filter") {
|
|
45
|
+
for (const child of check.checks) visit(child, nextCovered)
|
|
46
|
+
} else if (!nextCovered) {
|
|
47
|
+
const description = check.annotations?.representation?.id ?? check.annotations?.identifier ??
|
|
48
|
+
check.annotations?.expected
|
|
49
|
+
warnings.push({
|
|
50
|
+
_tag: "OpaqueFilter",
|
|
51
|
+
path,
|
|
52
|
+
...(description === undefined ? {} : { description })
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
checks?.forEach((check) => visit(check, false))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const reportFor = (ast: SchemaAST.AST): SchemaArbitraryReport => {
|
|
60
|
+
const warnings: Array<SchemaArbitraryWarning> = []
|
|
61
|
+
const stack = new WeakSet<SchemaAST.AST>()
|
|
62
|
+
const visit = (ast: SchemaAST.AST, path: ReadonlyArray<PropertyKey>): void => {
|
|
63
|
+
if (stack.has(ast)) return
|
|
64
|
+
stack.add(ast)
|
|
65
|
+
reportChecks(warnings, ast.checks, path)
|
|
66
|
+
switch (ast._tag) {
|
|
67
|
+
case "Declaration":
|
|
68
|
+
ast.typeParameters.forEach((typeParameter) => visit(typeParameter, path))
|
|
69
|
+
break
|
|
70
|
+
case "Arrays": {
|
|
71
|
+
const elements = [...ast.elements, ...ast.rest]
|
|
72
|
+
elements.forEach((type, index) => visit(type, [...path, index]))
|
|
73
|
+
break
|
|
74
|
+
}
|
|
75
|
+
case "Objects":
|
|
76
|
+
ast.propertySignatures.forEach((property) => visit(property.type, [...path, property.name]))
|
|
77
|
+
ast.indexSignatures.forEach((index) => {
|
|
78
|
+
visit(index.parameter, path)
|
|
79
|
+
visit(index.type, path)
|
|
80
|
+
})
|
|
81
|
+
break
|
|
82
|
+
case "Union":
|
|
83
|
+
ast.types.forEach((type) => visit(type, path))
|
|
84
|
+
break
|
|
85
|
+
case "TemplateLiteral":
|
|
86
|
+
ast.parts.forEach((part, index) => visit(SchemaAST.toEncoded(part), [...path, index]))
|
|
87
|
+
break
|
|
88
|
+
case "Suspend":
|
|
89
|
+
visit(ast.thunk(), path)
|
|
90
|
+
break
|
|
91
|
+
}
|
|
92
|
+
stack.delete(ast)
|
|
93
|
+
}
|
|
94
|
+
visit(ast, [])
|
|
95
|
+
return { warnings }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** @internal */
|
|
99
|
+
export const toArbitraryWithReport = <S extends Schema.Constraint>(schema: S) => ({
|
|
100
|
+
value: Schema.toArbitrary(schema)(FastCheck),
|
|
101
|
+
report: reportFor(schema.ast)
|
|
102
|
+
})
|