@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.
- package/README.md +4 -3
- package/dist/Machine.d.ts +24 -6
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js.map +1 -1
- package/dist/internal/machine/atom.d.ts +5 -0
- package/dist/internal/machine/atom.d.ts.map +1 -1
- package/dist/internal/machine/atom.js +6 -3
- package/dist/internal/machine/atom.js.map +1 -1
- package/dist/internal/machine/machine.d.ts.map +1 -1
- package/dist/internal/machine/machine.js +1 -1
- package/dist/internal/machine/machine.js.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.d.ts +26 -0
- package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.js +23 -0
- package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
- package/docs/agent-guide.md +14 -0
- package/package.json +5 -5
- package/src/Machine.ts +6909 -0
- package/src/index.ts +1 -0
- package/src/internal/machine/activities.ts +108 -0
- package/src/internal/machine/atom.ts +684 -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 +1750 -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 +696 -0
- package/src/unstable/reactivity/index.ts +1 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema"
|
|
2
|
+
import * as SchemaAST from "effect/SchemaAST"
|
|
3
|
+
|
|
4
|
+
export const StateNodePropertyPolicy = {
|
|
5
|
+
atomic: ["schema", "type"],
|
|
6
|
+
final: ["schema", "type", "output"],
|
|
7
|
+
compound: ["schema", "type", "initial", "states"],
|
|
8
|
+
parallel: ["schema", "type", "output", "states"],
|
|
9
|
+
history: ["type", "history", "annotations"],
|
|
10
|
+
choice: ["type", "annotations"]
|
|
11
|
+
} as const
|
|
12
|
+
|
|
13
|
+
export const PseudoStateAnnotationProperties = ["title", "description", "documentation"] as const
|
|
14
|
+
|
|
15
|
+
export type StateNodeKind = keyof typeof StateNodePropertyPolicy
|
|
16
|
+
|
|
17
|
+
export type AllowedStateNodeProperty<Kind extends StateNodeKind> = (typeof StateNodePropertyPolicy)[Kind][number]
|
|
18
|
+
|
|
19
|
+
export type AllowedPseudoStateAnnotationProperty = (typeof PseudoStateAnnotationProperties)[number]
|
|
20
|
+
|
|
21
|
+
export type StateDefinitionBoundary = "Machine.defineStates" | "Machine.make"
|
|
22
|
+
|
|
23
|
+
const hasOwn = (value: object, key: PropertyKey): boolean => Object.prototype.hasOwnProperty.call(value, key)
|
|
24
|
+
|
|
25
|
+
const displayPath = (parent: string, key: PropertyKey): string => {
|
|
26
|
+
const segment = typeof key === "symbol" ? `[${String(key)}]` : key === "" ? "<empty>" : String(key)
|
|
27
|
+
return parent === "" ? segment : `${parent}.${segment}`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const fail = (boundary: StateDefinitionBoundary, path: string, message: string): never => {
|
|
31
|
+
throw new Error(`${boundary} invalid state definition at "${path}": ${message}`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const isNumericForm = (key: string): boolean => Number.isFinite(Number(key))
|
|
35
|
+
|
|
36
|
+
type TaggedSchemaEvidence = "tagged" | "untagged" | "opaque"
|
|
37
|
+
|
|
38
|
+
const unionEvidence = (evidence: ReadonlyArray<TaggedSchemaEvidence>): TaggedSchemaEvidence =>
|
|
39
|
+
evidence.length === 0 || evidence.some((result) => result === "untagged")
|
|
40
|
+
? "untagged"
|
|
41
|
+
: evidence.every((result) => result === "tagged")
|
|
42
|
+
? "tagged"
|
|
43
|
+
: "opaque"
|
|
44
|
+
|
|
45
|
+
const propertyKeyEvidence = (
|
|
46
|
+
ast: SchemaAST.AST,
|
|
47
|
+
seen: ReadonlySet<SchemaAST.AST>
|
|
48
|
+
): TaggedSchemaEvidence => {
|
|
49
|
+
if (seen.has(ast)) return "opaque"
|
|
50
|
+
if (SchemaAST.isString(ast) || SchemaAST.isNumber(ast) || SchemaAST.isSymbol(ast)) return "tagged"
|
|
51
|
+
if (SchemaAST.isTemplateLiteral(ast) || SchemaAST.isUniqueSymbol(ast) || SchemaAST.isEnum(ast)) return "tagged"
|
|
52
|
+
if (SchemaAST.isLiteral(ast)) {
|
|
53
|
+
return typeof ast.literal === "string" || typeof ast.literal === "number" ? "tagged" : "untagged"
|
|
54
|
+
}
|
|
55
|
+
const nextSeen = new Set(seen).add(ast)
|
|
56
|
+
if (SchemaAST.isUnion(ast)) return unionEvidence(ast.types.map((member) => propertyKeyEvidence(member, nextSeen)))
|
|
57
|
+
if (SchemaAST.isSuspend(ast)) return propertyKeyEvidence(ast.thunk(), nextSeen)
|
|
58
|
+
if (SchemaAST.isDeclaration(ast)) return "opaque"
|
|
59
|
+
return "untagged"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const taggedSchemaEvidence = (
|
|
63
|
+
ast: SchemaAST.AST,
|
|
64
|
+
seen: ReadonlySet<SchemaAST.AST> = new Set()
|
|
65
|
+
): TaggedSchemaEvidence => {
|
|
66
|
+
if (seen.has(ast)) return "opaque"
|
|
67
|
+
if (SchemaAST.isObjects(ast)) {
|
|
68
|
+
const tag = ast.propertySignatures.find(({ name }) => name === "_tag")?.type
|
|
69
|
+
if (tag === undefined || SchemaAST.isOptional(tag)) return "untagged"
|
|
70
|
+
return propertyKeyEvidence(tag, new Set(seen).add(ast))
|
|
71
|
+
}
|
|
72
|
+
const nextSeen = new Set(seen).add(ast)
|
|
73
|
+
if (SchemaAST.isUnion(ast)) return unionEvidence(ast.types.map((member) => taggedSchemaEvidence(member, nextSeen)))
|
|
74
|
+
if (SchemaAST.isSuspend(ast)) return taggedSchemaEvidence(ast.thunk(), nextSeen)
|
|
75
|
+
// A declaration's decoded Type is intentionally opaque at runtime. Its type
|
|
76
|
+
// parameters describe values contained by the declaration, not its outer
|
|
77
|
+
// shape, so neither they nor private sentinel metadata can prove the public
|
|
78
|
+
// structural TaggedSchema contract without rejecting valid Schema.declare
|
|
79
|
+
// schemas or relying on an undocumented Effect export.
|
|
80
|
+
if (SchemaAST.isDeclaration(ast)) return "opaque"
|
|
81
|
+
return "untagged"
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Rejects schemas whose decoded AST proves they cannot satisfy the structural
|
|
86
|
+
* `_tag: PropertyKey` contract of `Machine.TaggedSchema`. Declarations without
|
|
87
|
+
* sentinel metadata stay opaque: their Type is unavailable at runtime, so
|
|
88
|
+
* rejecting them would reject valid `Schema.declare` schemas. Consequently an
|
|
89
|
+
* unsafe cast or JavaScript caller can pass an opaque untagged declaration;
|
|
90
|
+
* this boundary only promises to reject schemas whose mismatch is provable.
|
|
91
|
+
*/
|
|
92
|
+
const isTaggedSchema = (value: unknown): value is Schema.Top =>
|
|
93
|
+
Schema.isSchema(value) && taggedSchemaEvidence(SchemaAST.toType(value.ast)) !== "untagged"
|
|
94
|
+
|
|
95
|
+
const validateStateKey = (
|
|
96
|
+
boundary: StateDefinitionBoundary,
|
|
97
|
+
parent: string,
|
|
98
|
+
key: PropertyKey
|
|
99
|
+
): string => {
|
|
100
|
+
const path = displayPath(parent, key)
|
|
101
|
+
if (typeof key === "symbol") {
|
|
102
|
+
return fail(boundary, path, "state keys must be strings, not symbols")
|
|
103
|
+
}
|
|
104
|
+
if (typeof key === "number") {
|
|
105
|
+
return fail(boundary, path, "state keys cannot use numeric forms")
|
|
106
|
+
}
|
|
107
|
+
if (key === "") {
|
|
108
|
+
return fail(boundary, path, "state keys cannot be empty")
|
|
109
|
+
}
|
|
110
|
+
if (key === "__proto__") {
|
|
111
|
+
return fail(boundary, path, "the state key \"__proto__\" is not allowed")
|
|
112
|
+
}
|
|
113
|
+
if (key.includes(".")) {
|
|
114
|
+
return fail(boundary, path, "state keys cannot contain \".\"")
|
|
115
|
+
}
|
|
116
|
+
if (isNumericForm(key)) {
|
|
117
|
+
return fail(boundary, path, "state keys cannot use numeric forms")
|
|
118
|
+
}
|
|
119
|
+
return path
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const assertPlainRecord: (
|
|
123
|
+
boundary: StateDefinitionBoundary,
|
|
124
|
+
path: string,
|
|
125
|
+
value: unknown,
|
|
126
|
+
description: string
|
|
127
|
+
) => asserts value is Readonly<Record<PropertyKey, unknown>> = (boundary, path, value, description) => {
|
|
128
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
129
|
+
fail(boundary, path, `${description} must be a plain record`)
|
|
130
|
+
}
|
|
131
|
+
const prototype = Object.getPrototypeOf(value)
|
|
132
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
133
|
+
fail(
|
|
134
|
+
boundary,
|
|
135
|
+
displayPath(path === "<root>" ? "" : path, "__proto__"),
|
|
136
|
+
`${description} must not declare an implicit "__proto__" state key`
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const assertAllowedProperties = (
|
|
142
|
+
boundary: StateDefinitionBoundary,
|
|
143
|
+
path: string,
|
|
144
|
+
node: Readonly<Record<PropertyKey, unknown>>,
|
|
145
|
+
kind: StateNodeKind
|
|
146
|
+
): void => {
|
|
147
|
+
const allowed = StateNodePropertyPolicy[kind] as ReadonlyArray<PropertyKey>
|
|
148
|
+
for (const property of Reflect.ownKeys(node)) {
|
|
149
|
+
if (!allowed.includes(property)) {
|
|
150
|
+
fail(boundary, path, `${kind} states cannot declare property "${String(property)}"`)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const validatePseudoAnnotations = (
|
|
156
|
+
boundary: StateDefinitionBoundary,
|
|
157
|
+
path: string,
|
|
158
|
+
annotations: unknown
|
|
159
|
+
): void => {
|
|
160
|
+
assertPlainRecord(boundary, `${path}.annotations`, annotations, "pseudo-state annotations")
|
|
161
|
+
for (const property of Reflect.ownKeys(annotations)) {
|
|
162
|
+
if (!(PseudoStateAnnotationProperties as ReadonlyArray<PropertyKey>).includes(property)) {
|
|
163
|
+
fail(boundary, `${path}.annotations`, `pseudo-state annotations cannot declare property "${String(property)}"`)
|
|
164
|
+
}
|
|
165
|
+
if (typeof annotations[property] !== "string") {
|
|
166
|
+
fail(boundary, `${path}.annotations.${String(property)}`, "pseudo-state annotation values must be strings")
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const validateStateTree = (
|
|
172
|
+
boundary: StateDefinitionBoundary,
|
|
173
|
+
states: unknown,
|
|
174
|
+
parent: string,
|
|
175
|
+
allowPseudoStates: boolean
|
|
176
|
+
): void => {
|
|
177
|
+
const treePath = parent === "" ? "<root>" : parent
|
|
178
|
+
assertPlainRecord(boundary, treePath, states, "state trees")
|
|
179
|
+
|
|
180
|
+
for (const key of Reflect.ownKeys(states)) {
|
|
181
|
+
const path = validateStateKey(boundary, parent, key)
|
|
182
|
+
const node = states[key]
|
|
183
|
+
|
|
184
|
+
// Effect schemas have their own runtime properties. They are complete
|
|
185
|
+
// atomic node definitions and must be recognized before config validation.
|
|
186
|
+
if (Schema.isSchema(node)) {
|
|
187
|
+
if (!isTaggedSchema(node)) {
|
|
188
|
+
fail(boundary, path, "state schemas must decode to an object with a required PropertyKey _tag")
|
|
189
|
+
}
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
192
|
+
assertPlainRecord(boundary, path, node, "state nodes")
|
|
193
|
+
|
|
194
|
+
const type = node.type
|
|
195
|
+
const kind: StateNodeKind = type === "history" ?
|
|
196
|
+
"history"
|
|
197
|
+
: type === "choice" ?
|
|
198
|
+
"choice"
|
|
199
|
+
: type === "parallel" ?
|
|
200
|
+
"parallel"
|
|
201
|
+
: type === "final" ?
|
|
202
|
+
"final"
|
|
203
|
+
: hasOwn(node, "states") ?
|
|
204
|
+
"compound"
|
|
205
|
+
: "atomic"
|
|
206
|
+
|
|
207
|
+
assertAllowedProperties(boundary, path, node, kind)
|
|
208
|
+
|
|
209
|
+
if (kind === "history" || kind === "choice") {
|
|
210
|
+
if (!allowPseudoStates) {
|
|
211
|
+
fail(boundary, path, `${kind} states must be declared below an active parent state`)
|
|
212
|
+
}
|
|
213
|
+
if (hasOwn(node, "annotations")) {
|
|
214
|
+
validatePseudoAnnotations(boundary, path, node.annotations)
|
|
215
|
+
}
|
|
216
|
+
if (kind === "history" && hasOwn(node, "history") && node.history !== "shallow" && node.history !== "deep") {
|
|
217
|
+
fail(boundary, `${path}.history`, "history must be \"shallow\" or \"deep\"")
|
|
218
|
+
}
|
|
219
|
+
continue
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!hasOwn(node, "schema") || !isTaggedSchema(node.schema)) {
|
|
223
|
+
fail(boundary, `${path}.schema`, "active states must declare an Effect Schema with a required PropertyKey _tag")
|
|
224
|
+
}
|
|
225
|
+
if (kind === "atomic" && hasOwn(node, "type") && node.type !== "active") {
|
|
226
|
+
fail(boundary, `${path}.type`, "atomic state type must be \"active\"")
|
|
227
|
+
}
|
|
228
|
+
if (kind === "final" && node.type !== "final") {
|
|
229
|
+
fail(boundary, `${path}.type`, "final state type must be \"final\"")
|
|
230
|
+
}
|
|
231
|
+
if (kind === "compound" && hasOwn(node, "type") && node.type !== "active") {
|
|
232
|
+
fail(boundary, `${path}.type`, "compound state type must be \"active\"")
|
|
233
|
+
}
|
|
234
|
+
if (kind === "parallel" && node.type !== "parallel") {
|
|
235
|
+
fail(boundary, `${path}.type`, "parallel state type must be \"parallel\"")
|
|
236
|
+
}
|
|
237
|
+
if ((kind === "final" || kind === "parallel") && hasOwn(node, "output") && !Schema.isSchema(node.output)) {
|
|
238
|
+
fail(boundary, `${path}.output`, "state output must be an Effect Schema")
|
|
239
|
+
}
|
|
240
|
+
if (kind === "compound") {
|
|
241
|
+
if (typeof node.initial !== "string") {
|
|
242
|
+
fail(boundary, `${path}.initial`, "compound states must declare an initial child key")
|
|
243
|
+
}
|
|
244
|
+
const initialKey = node.initial as string
|
|
245
|
+
validateStateTree(boundary, node.states, path, true)
|
|
246
|
+
if (!hasOwn(node.states as object, initialKey)) {
|
|
247
|
+
fail(boundary, `${path}.initial`, `compound initial child "${initialKey}" does not exist`)
|
|
248
|
+
}
|
|
249
|
+
const initial = (node.states as Readonly<Record<PropertyKey, unknown>>)[initialKey]
|
|
250
|
+
if (
|
|
251
|
+
!Schema.isSchema(initial) && typeof initial === "object" && initial !== null &&
|
|
252
|
+
(initial as Readonly<Record<PropertyKey, unknown>>).type === "history"
|
|
253
|
+
) {
|
|
254
|
+
fail(boundary, `${path}.initial`, "compound initial children cannot be history states")
|
|
255
|
+
}
|
|
256
|
+
continue
|
|
257
|
+
}
|
|
258
|
+
if (kind === "parallel") {
|
|
259
|
+
if (!hasOwn(node, "states")) {
|
|
260
|
+
fail(boundary, `${path}.states`, "parallel states must declare child regions")
|
|
261
|
+
}
|
|
262
|
+
validateStateTree(boundary, node.states, path, true)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export const validateStateDefinitions = (
|
|
268
|
+
states: unknown,
|
|
269
|
+
boundary: StateDefinitionBoundary
|
|
270
|
+
): void => validateStateTree(boundary, states, "", false)
|