@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,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)
@@ -0,0 +1,2 @@
1
+ /** @internal */
2
+ export const InitialEventTypeId: unique symbol = Symbol("effect/Machine/InitialEvent")