@typeonce/effect-machine-devtools 0.24.0 → 0.26.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/NOTICE +4 -0
- package/README.md +13 -13
- package/dist/DevToolsProtocol.d.ts +66 -630
- package/dist/DevToolsProtocol.d.ts.map +1 -1
- package/dist/DevToolsProtocol.js +0 -190
- package/dist/DevToolsProtocol.js.map +1 -1
- package/dist/MachineDocument.d.ts +38 -18
- package/dist/MachineDocument.d.ts.map +1 -1
- package/dist/MachineDocument.js +14 -12
- package/dist/MachineDocument.js.map +1 -1
- package/dist/MachineRegistry.d.ts +22 -2
- package/dist/MachineRegistry.d.ts.map +1 -1
- package/dist/MachineWalkthrough.d.ts +171 -0
- package/dist/MachineWalkthrough.d.ts.map +1 -0
- package/dist/MachineWalkthrough.js +98 -0
- package/dist/MachineWalkthrough.js.map +1 -0
- package/dist/ProjectInspector.d.ts +0 -2
- package/dist/ProjectInspector.d.ts.map +1 -1
- package/dist/ProjectInspector.js.map +1 -1
- package/dist/client/assets/index-BsorPSDS.css +1 -0
- package/dist/client/assets/index-eiptehRd.js +37 -0
- package/dist/client/index.html +4 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/devServer.d.ts.map +1 -1
- package/dist/internal/devServer.js +4 -64
- package/dist/internal/devServer.js.map +1 -1
- package/dist/internal/evaluationWorker.d.ts.map +1 -1
- package/dist/internal/evaluationWorker.js +7 -263
- package/dist/internal/evaluationWorker.js.map +1 -1
- package/dist/internal/machineDocument.d.ts.map +1 -1
- package/dist/internal/machineDocument.js +3 -1
- package/dist/internal/machineDocument.js.map +1 -1
- package/dist/internal/machineWalkthrough.d.ts +26 -0
- package/dist/internal/machineWalkthrough.d.ts.map +1 -0
- package/dist/internal/machineWalkthrough.js +256 -0
- package/dist/internal/machineWalkthrough.js.map +1 -0
- package/dist/internal/projectInspector.d.ts.map +1 -1
- package/dist/internal/projectInspector.js +1 -11
- package/dist/internal/projectInspector.js.map +1 -1
- package/index.html +2 -2
- package/package.json +8 -7
- package/src/DevToolsProtocol.ts +0 -285
- package/src/MachineDocument.ts +21 -19
- package/src/MachineWalkthrough.ts +199 -0
- package/src/ProjectInspector.ts +0 -5
- package/src/index.ts +2 -2
- package/src/internal/browser/chart-layout-policy.ts +206 -0
- package/src/internal/browser/chart-layout.ts +686 -0
- package/src/internal/browser/chart-model.ts +244 -0
- package/src/internal/browser/chart-renderer.ts +774 -0
- package/src/internal/browser/example-machine.ts +15 -6
- package/src/internal/browser/input-form.ts +1 -418
- package/src/internal/browser/invoke-outcomes-example.ts +231 -0
- package/src/internal/browser/parallel-completion-example.ts +199 -0
- package/src/internal/browser/planner-example.ts +2 -0
- package/src/internal/browser/protocol-events-example.ts +200 -0
- package/src/internal/browser/styles.css +974 -342
- package/src/internal/browser/transition-semantics-example.ts +245 -0
- package/src/internal/browser/visualizer-analysis.ts +52 -0
- package/src/internal/browser/visualizer-app.ts +874 -706
- package/src/internal/browser/visualizer-model.ts +104 -0
- package/src/internal/devServer.ts +4 -82
- package/src/internal/evaluationWorker.ts +7 -371
- package/src/internal/machineDocument.ts +3 -1
- package/src/internal/machineWalkthrough.ts +355 -0
- package/src/internal/projectInspector.ts +3 -31
- package/dist/MachineSimulator.d.ts +0 -169
- package/dist/MachineSimulator.d.ts.map +0 -1
- package/dist/MachineSimulator.js +0 -98
- package/dist/MachineSimulator.js.map +0 -1
- package/dist/client/assets/index-B49Tjawc.js +0 -14
- package/dist/client/assets/index-BKH0cOG2.css +0 -1
- package/dist/internal/machineSimulator.d.ts +0 -5
- package/dist/internal/machineSimulator.d.ts.map +0 -1
- package/dist/internal/machineSimulator.js +0 -156
- package/dist/internal/machineSimulator.js.map +0 -1
- package/src/MachineSimulator.ts +0 -154
- package/src/internal/browser/simulation-client.ts +0 -20
- package/src/internal/machineSimulator.ts +0 -199
|
@@ -72,6 +72,110 @@ export const triggerLabel = (transition: VisualizationTransition): string => {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
const propertyAccess = (key: string): string => /^[$A-Z_a-z][$\w]*$/.test(key) ? `.${key}` : `[${JSON.stringify(key)}]`
|
|
76
|
+
|
|
77
|
+
const pathAccess = (path: string): string => path.split(".").map(propertyAccess).join("")
|
|
78
|
+
|
|
79
|
+
const nearestCompoundScope = (
|
|
80
|
+
document: VisualizationDocument,
|
|
81
|
+
source: string
|
|
82
|
+
): string | undefined => {
|
|
83
|
+
const states = new Map(document.states.map((state) => [state.path, state]))
|
|
84
|
+
let current = states.get(source)
|
|
85
|
+
while (current !== undefined) {
|
|
86
|
+
if (current.type === "compound") return current.path
|
|
87
|
+
current = current.parent === null ? undefined : states.get(current.parent)
|
|
88
|
+
}
|
|
89
|
+
return undefined
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const localPathAccess = (
|
|
93
|
+
document: VisualizationDocument,
|
|
94
|
+
source: string,
|
|
95
|
+
path: string
|
|
96
|
+
): string | undefined => {
|
|
97
|
+
const scope = nearestCompoundScope(document, source)
|
|
98
|
+
if (scope === undefined) return undefined
|
|
99
|
+
if (path === scope) return ""
|
|
100
|
+
const prefix = `${scope}.`
|
|
101
|
+
return path.startsWith(prefix) ? pathAccess(path.slice(prefix.length)) : undefined
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Canonical Effect Machine selector represented by one retained transition branch. */
|
|
105
|
+
export const branchTargetApi = (
|
|
106
|
+
document: VisualizationDocument,
|
|
107
|
+
source: string,
|
|
108
|
+
branch: VisualizationBranch
|
|
109
|
+
): string | undefined => {
|
|
110
|
+
const { selection } = branch
|
|
111
|
+
if (selection.kind === "none") return "to.none"
|
|
112
|
+
const path = selection.path
|
|
113
|
+
if (path === null || selection.scope === null) return undefined
|
|
114
|
+
|
|
115
|
+
let api: string | undefined
|
|
116
|
+
if (selection.kind === "history" && selection.scope === "full") {
|
|
117
|
+
api = `to.history${pathAccess(path)}`
|
|
118
|
+
} else if (selection.scope === "local") {
|
|
119
|
+
const local = localPathAccess(document, source, path)
|
|
120
|
+
if (local === undefined) return undefined
|
|
121
|
+
switch (selection.kind) {
|
|
122
|
+
case "state":
|
|
123
|
+
api = local === "" ? "to.local.with" : `to.local${local}()`
|
|
124
|
+
break
|
|
125
|
+
case "choice":
|
|
126
|
+
api = local === "" ? undefined : `to.local${local}()`
|
|
127
|
+
break
|
|
128
|
+
case "initial":
|
|
129
|
+
api = local === "" ? undefined : `to.local${local}.initial`
|
|
130
|
+
break
|
|
131
|
+
case "update":
|
|
132
|
+
api = local === "" ? "to.local.update" : undefined
|
|
133
|
+
break
|
|
134
|
+
case "history":
|
|
135
|
+
break
|
|
136
|
+
}
|
|
137
|
+
} else if (selection.scope === "branch") {
|
|
138
|
+
switch (selection.kind) {
|
|
139
|
+
case "state":
|
|
140
|
+
case "choice":
|
|
141
|
+
api = `to.branch${pathAccess(path)}()`
|
|
142
|
+
break
|
|
143
|
+
case "initial":
|
|
144
|
+
api = `to.branch${pathAccess(path)}.initial`
|
|
145
|
+
break
|
|
146
|
+
case "update":
|
|
147
|
+
api = `to.branch${pathAccess(path)}.update`
|
|
148
|
+
break
|
|
149
|
+
case "history":
|
|
150
|
+
break
|
|
151
|
+
}
|
|
152
|
+
} else if (selection.scope === "full") {
|
|
153
|
+
switch (selection.kind) {
|
|
154
|
+
case "state":
|
|
155
|
+
case "choice":
|
|
156
|
+
api = `to.full${pathAccess(path)}()`
|
|
157
|
+
break
|
|
158
|
+
case "initial":
|
|
159
|
+
api = `to.full${pathAccess(path)}.initial`
|
|
160
|
+
break
|
|
161
|
+
case "history":
|
|
162
|
+
api = `to.history${pathAccess(path)}`
|
|
163
|
+
break
|
|
164
|
+
case "update":
|
|
165
|
+
break
|
|
166
|
+
}
|
|
167
|
+
} else if (selection.scope === "initial") {
|
|
168
|
+
if (selection.kind === "state") api = `to${pathAccess(path)}()`
|
|
169
|
+
if (selection.kind === "initial") api = `to${pathAccess(path)}.initial`
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (api === undefined || selection.kind === "update") return api
|
|
173
|
+
return branch.updates.reduce(
|
|
174
|
+
(expression, owner) => `${expression}.updating(to.branch${pathAccess(owner)})`,
|
|
175
|
+
api
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
75
179
|
const buildInitialPaths = (document: VisualizationDocument): ReadonlySet<string> => {
|
|
76
180
|
const initial = new Set<string>([document.initial.target])
|
|
77
181
|
for (const state of document.states) {
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { type ChokidarOptions, type FSWatcher, watch } from "chokidar"
|
|
2
2
|
import * as Effect from "effect/Effect"
|
|
3
|
-
import * as Schema from "effect/Schema"
|
|
4
3
|
import * as Stream from "effect/Stream"
|
|
5
4
|
import type { IncomingMessage, ServerResponse } from "node:http"
|
|
6
5
|
import { isAbsolute, relative, resolve } from "node:path"
|
|
7
6
|
import { fileURLToPath } from "node:url"
|
|
8
7
|
import { createServer, type Plugin, type ViteDevServer } from "vite"
|
|
9
8
|
import type * as DevServer from "../DevServer.js"
|
|
10
|
-
import * as DevToolsProtocol from "../DevToolsProtocol.js"
|
|
11
9
|
import * as MachineRegistry from "../MachineRegistry.js"
|
|
12
10
|
import * as ProjectInspector from "../ProjectInspector.js"
|
|
13
11
|
|
|
@@ -51,51 +49,8 @@ const writeJson = (response: ServerResponse, value: unknown, status = 200): void
|
|
|
51
49
|
response.end(JSON.stringify(value))
|
|
52
50
|
}
|
|
53
51
|
|
|
54
|
-
const readJson = (request: IncomingMessage): Promise<unknown> =>
|
|
55
|
-
new Promise((resolveBody, reject) => {
|
|
56
|
-
const chunks: Array<Buffer> = []
|
|
57
|
-
let size = 0
|
|
58
|
-
request.on("data", (chunk: Buffer) => {
|
|
59
|
-
size += chunk.byteLength
|
|
60
|
-
if (size > 1_000_000) {
|
|
61
|
-
reject(new Error("Simulation requests are limited to 1 MB"))
|
|
62
|
-
request.destroy()
|
|
63
|
-
return
|
|
64
|
-
}
|
|
65
|
-
chunks.push(chunk)
|
|
66
|
-
})
|
|
67
|
-
request.on("end", () => {
|
|
68
|
-
try {
|
|
69
|
-
resolveBody(JSON.parse(Buffer.concat(chunks).toString("utf8")))
|
|
70
|
-
} catch (cause) {
|
|
71
|
-
reject(cause)
|
|
72
|
-
}
|
|
73
|
-
})
|
|
74
|
-
request.on("error", reject)
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
const staleSimulation = (
|
|
78
|
-
request: DevToolsProtocol.SimulationRequest,
|
|
79
|
-
message: string
|
|
80
|
-
): DevToolsProtocol.SimulationFailed => ({
|
|
81
|
-
_tag: "SimulationFailed",
|
|
82
|
-
protocolVersion: DevToolsProtocol.protocolVersion,
|
|
83
|
-
key: request.key,
|
|
84
|
-
revision: request.revision,
|
|
85
|
-
inputIssues: [],
|
|
86
|
-
diagnostics: [{
|
|
87
|
-
severity: "error",
|
|
88
|
-
code: "simulation-stale",
|
|
89
|
-
message,
|
|
90
|
-
location: { file: request.source.file, line: null, column: null },
|
|
91
|
-
statePath: null
|
|
92
|
-
}]
|
|
93
|
-
})
|
|
94
|
-
|
|
95
52
|
const apiPlugin = (
|
|
96
|
-
|
|
97
|
-
registry: MachineRegistry.MachineRegistry["Service"],
|
|
98
|
-
inspector: ProjectInspector.ProjectInspector["Service"]
|
|
53
|
+
registry: MachineRegistry.MachineRegistry["Service"]
|
|
99
54
|
): Plugin => ({
|
|
100
55
|
name: "effect-machine-devtools-api",
|
|
101
56
|
configureServer(server) {
|
|
@@ -110,37 +65,6 @@ const apiPlugin = (
|
|
|
110
65
|
)
|
|
111
66
|
return
|
|
112
67
|
}
|
|
113
|
-
if (request.method === "POST" && request.url === "/api/simulations") {
|
|
114
|
-
void Effect.runPromise(
|
|
115
|
-
Effect.gen(function*() {
|
|
116
|
-
const body = yield* Effect.tryPromise({
|
|
117
|
-
try: () => readJson(request),
|
|
118
|
-
catch: (cause) => cause
|
|
119
|
-
})
|
|
120
|
-
const simulationRequest = yield* Schema.decodeUnknownEffect(DevToolsProtocol.SimulationRequest)(body)
|
|
121
|
-
const snapshot = yield* registry.get
|
|
122
|
-
const current = snapshot.results.find((result) => result.key === simulationRequest.key)
|
|
123
|
-
if (current === undefined || current._tag === "Failed") {
|
|
124
|
-
return staleSimulation(simulationRequest, "The machine is no longer available; restart the simulation")
|
|
125
|
-
}
|
|
126
|
-
if (
|
|
127
|
-
current.document.revision !== simulationRequest.revision ||
|
|
128
|
-
current.document.source?.file !== simulationRequest.source.file ||
|
|
129
|
-
current.document.source.exportName !== simulationRequest.source.exportName
|
|
130
|
-
) {
|
|
131
|
-
return staleSimulation(
|
|
132
|
-
simulationRequest,
|
|
133
|
-
"The machine changed; restart the simulation from its latest revision"
|
|
134
|
-
)
|
|
135
|
-
}
|
|
136
|
-
return yield* inspector.simulate(simulationRequest, { root })
|
|
137
|
-
})
|
|
138
|
-
).then(
|
|
139
|
-
(result) => writeJson(response, result),
|
|
140
|
-
(cause) => writeJson(response, { message: String(cause) }, 400)
|
|
141
|
-
)
|
|
142
|
-
return
|
|
143
|
-
}
|
|
144
68
|
if (request.url !== "/api/events") {
|
|
145
69
|
next()
|
|
146
70
|
return
|
|
@@ -167,8 +91,7 @@ const apiPlugin = (
|
|
|
167
91
|
const acquire = (
|
|
168
92
|
ErrorType: DevServerErrorConstructor,
|
|
169
93
|
options: DevServer.Options,
|
|
170
|
-
registry: MachineRegistry.MachineRegistry["Service"]
|
|
171
|
-
inspector: ProjectInspector.ProjectInspector["Service"]
|
|
94
|
+
registry: MachineRegistry.MachineRegistry["Service"]
|
|
172
95
|
): Effect.Effect<ViteDevServer, DevServer.DevServerError, never> =>
|
|
173
96
|
Effect.tryPromise({
|
|
174
97
|
try: async () => {
|
|
@@ -176,7 +99,7 @@ const acquire = (
|
|
|
176
99
|
root: packageRoot,
|
|
177
100
|
appType: "spa",
|
|
178
101
|
logLevel: "error",
|
|
179
|
-
plugins: [apiPlugin(
|
|
102
|
+
plugins: [apiPlugin(registry)],
|
|
180
103
|
server: {
|
|
181
104
|
host: options.host,
|
|
182
105
|
port: options.port,
|
|
@@ -239,9 +162,8 @@ export const run = (
|
|
|
239
162
|
> =>
|
|
240
163
|
Effect.gen(function*() {
|
|
241
164
|
const registry = yield* MachineRegistry.MachineRegistry
|
|
242
|
-
const inspector = yield* ProjectInspector.ProjectInspector
|
|
243
165
|
const server = yield* Effect.acquireRelease(
|
|
244
|
-
acquire(ErrorType, options, registry
|
|
166
|
+
acquire(ErrorType, options, registry),
|
|
245
167
|
(server) => Effect.promise(() => server.close())
|
|
246
168
|
)
|
|
247
169
|
yield* Effect.acquireRelease(
|
|
@@ -2,9 +2,8 @@ import * as NodeWorkerRunner from "@effect/platform-node/NodeWorkerRunner"
|
|
|
2
2
|
import { Machine } from "@typeonce/effect-machine"
|
|
3
3
|
import * as Effect from "effect/Effect"
|
|
4
4
|
import * as Schema from "effect/Schema"
|
|
5
|
-
import * as SchemaIssue from "effect/SchemaIssue"
|
|
6
5
|
import * as WorkerRunner from "effect/unstable/workers/WorkerRunner"
|
|
7
|
-
import {
|
|
6
|
+
import { resolve } from "node:path"
|
|
8
7
|
import { pathToFileURL } from "node:url"
|
|
9
8
|
import type { ViteDevServer } from "vite"
|
|
10
9
|
import * as DevToolsProtocol from "../DevToolsProtocol.js"
|
|
@@ -23,14 +22,8 @@ interface EvaluationResponse {
|
|
|
23
22
|
readonly results: ReadonlyArray<DevToolsProtocol.MachineResult>
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
readonly root: string
|
|
29
|
-
readonly request: DevToolsProtocol.SimulationRequest
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
type WorkerRequest = EvaluationRequest | SimulationWorkerRequest
|
|
33
|
-
type WorkerResponse = EvaluationResponse | DevToolsProtocol.SimulationResult
|
|
25
|
+
type WorkerRequest = EvaluationRequest
|
|
26
|
+
type WorkerResponse = EvaluationResponse
|
|
34
27
|
|
|
35
28
|
const isMachine = (value: unknown): value is Machine.Machine.Any =>
|
|
36
29
|
typeof value === "object" &&
|
|
@@ -68,23 +61,7 @@ const messageOf = (cause: unknown): string => {
|
|
|
68
61
|
// Fall back to the runtime string representation below.
|
|
69
62
|
}
|
|
70
63
|
const rendered = String(cause)
|
|
71
|
-
return rendered.length > 0 ? rendered : "The
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const jsonValue = (value: unknown): Schema.Json => {
|
|
75
|
-
const seen = new WeakSet<object>()
|
|
76
|
-
const encoded = JSON.stringify(value, (_key, current: unknown) => {
|
|
77
|
-
if (typeof current === "bigint") return `${current}n`
|
|
78
|
-
if (typeof current === "function") return `[Function ${current.name || "anonymous"}]`
|
|
79
|
-
if (typeof current === "symbol") return String(current)
|
|
80
|
-
if (typeof current === "undefined") return null
|
|
81
|
-
if (typeof current === "object" && current !== null) {
|
|
82
|
-
if (seen.has(current)) return "[Circular]"
|
|
83
|
-
seen.add(current)
|
|
84
|
-
}
|
|
85
|
-
return current
|
|
86
|
-
})
|
|
87
|
-
return encoded === undefined ? null : JSON.parse(encoded) as Schema.Json
|
|
64
|
+
return rendered.length > 0 ? rendered : "The evaluator failed without a diagnostic message"
|
|
88
65
|
}
|
|
89
66
|
|
|
90
67
|
const failed = (
|
|
@@ -168,353 +145,12 @@ const handle = (server: ViteDevServer, request: EvaluationRequest): Effect.Effec
|
|
|
168
145
|
Effect.map((results) => ({ _tag: "InspectedMachines" as const, results: results.flat() }))
|
|
169
146
|
)
|
|
170
147
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
readonly event: unknown
|
|
174
|
-
readonly transitions: ReadonlyArray<Machine.Machine.RetainedTransition>
|
|
175
|
-
readonly commands: ReadonlyArray<Machine.Command>
|
|
176
|
-
readonly raisedEvents: ReadonlyArray<unknown>
|
|
177
|
-
readonly emittedEvents: ReadonlyArray<unknown>
|
|
178
|
-
readonly exitPaths: ReadonlyArray<string>
|
|
179
|
-
readonly entryPaths: ReadonlyArray<string>
|
|
180
|
-
readonly changed: boolean
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
interface DynamicPlan {
|
|
184
|
-
readonly startingState?: unknown
|
|
185
|
-
readonly state?: unknown
|
|
186
|
-
readonly next?: unknown
|
|
187
|
-
readonly commands: ReadonlyArray<Machine.Command>
|
|
188
|
-
readonly emittedEvents: ReadonlyArray<unknown>
|
|
189
|
-
readonly microsteps: ReadonlyArray<DynamicMicrostep>
|
|
190
|
-
readonly done: boolean
|
|
191
|
-
readonly output: unknown
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const planInitial = Machine.planInitial as unknown as (
|
|
195
|
-
machine: Machine.Machine.Any,
|
|
196
|
-
...input: ReadonlyArray<unknown>
|
|
197
|
-
) => Effect.Effect<DynamicPlan, unknown>
|
|
198
|
-
|
|
199
|
-
const plan = Machine.plan as (
|
|
200
|
-
machine: Machine.Machine.Any,
|
|
201
|
-
snapshot: unknown,
|
|
202
|
-
event: unknown
|
|
203
|
-
) => Effect.Effect<DynamicPlan, unknown>
|
|
204
|
-
|
|
205
|
-
const encodeSnapshot = Machine.encodeSnapshot as (
|
|
206
|
-
machine: Machine.Machine.Any,
|
|
207
|
-
snapshot: unknown
|
|
208
|
-
) => Effect.Effect<DevToolsProtocol.EncodedSnapshot, unknown>
|
|
209
|
-
|
|
210
|
-
const decodeSnapshot = Machine.decodeSnapshot as (
|
|
211
|
-
machine: Machine.Machine.Any,
|
|
212
|
-
snapshot: unknown
|
|
213
|
-
) => Effect.Effect<unknown, unknown>
|
|
214
|
-
|
|
215
|
-
const validateSchemaInput = (
|
|
216
|
-
schema: Schema.Top,
|
|
217
|
-
input: unknown
|
|
218
|
-
): Effect.Effect<void, Schema.SchemaError> =>
|
|
219
|
-
schema.makeEffect(input as never, { parseOptions: { errors: "all" } }).pipe(
|
|
220
|
-
Effect.asVoid,
|
|
221
|
-
Effect.mapError((issue) => new Schema.SchemaError(issue))
|
|
222
|
-
)
|
|
223
|
-
|
|
224
|
-
const isJsonSchemaRecord = (value: unknown): value is Record<string, unknown> =>
|
|
225
|
-
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
226
|
-
|
|
227
|
-
const resolveJsonSchemaReference = (
|
|
228
|
-
value: unknown,
|
|
229
|
-
definitions: Readonly<Record<string, unknown>>
|
|
230
|
-
): Record<string, unknown> | undefined => {
|
|
231
|
-
if (!isJsonSchemaRecord(value)) return undefined
|
|
232
|
-
if (typeof value.$ref !== "string" || !value.$ref.startsWith("#/$defs/")) return value
|
|
233
|
-
const target = definitions[decodeURIComponent(value.$ref.slice("#/$defs/".length))]
|
|
234
|
-
return isJsonSchemaRecord(target) ? target : undefined
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const inputEventTags = (schema: Machine.Machine.TaggedSchema): ReadonlySet<string> => {
|
|
238
|
-
const document = Schema.toJsonSchemaDocument(schema)
|
|
239
|
-
const root = resolveJsonSchemaReference(document.schema, document.definitions) ?? document.schema
|
|
240
|
-
const variants = isJsonSchemaRecord(root) && Array.isArray(root.anyOf)
|
|
241
|
-
? root.anyOf
|
|
242
|
-
: isJsonSchemaRecord(root) && Array.isArray(root.oneOf)
|
|
243
|
-
? root.oneOf
|
|
244
|
-
: [root]
|
|
245
|
-
const tags = new Set<string>()
|
|
246
|
-
for (const variant of variants) {
|
|
247
|
-
const resolved = resolveJsonSchemaReference(variant, document.definitions)
|
|
248
|
-
if (resolved === undefined || !isJsonSchemaRecord(resolved.properties)) continue
|
|
249
|
-
const tag = resolveJsonSchemaReference(resolved.properties._tag, document.definitions)
|
|
250
|
-
if (tag === undefined) continue
|
|
251
|
-
if (typeof tag.const === "string" || typeof tag.const === "number") tags.add(String(tag.const))
|
|
252
|
-
if (Array.isArray(tag.enum)) {
|
|
253
|
-
tag.enum.forEach((value) => {
|
|
254
|
-
if (typeof value === "string" || typeof value === "number") tags.add(String(value))
|
|
255
|
-
})
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
return tags
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const inputEventSchemas = Machine.inputEventSchemas as (
|
|
262
|
-
machine: Machine.Machine.Any
|
|
263
|
-
) => ReadonlyArray<Machine.Machine.TaggedSchema>
|
|
264
|
-
|
|
265
|
-
const validateInitialInput = (
|
|
266
|
-
machine: Machine.Machine.Any,
|
|
267
|
-
request: DevToolsProtocol.StartSimulation
|
|
268
|
-
): Effect.Effect<void, Schema.SchemaError | Error> => {
|
|
269
|
-
if (machine.input === undefined) {
|
|
270
|
-
return Object.hasOwn(request, "input")
|
|
271
|
-
? Effect.fail(new Error("This machine does not accept startup input"))
|
|
272
|
-
: Effect.void
|
|
273
|
-
}
|
|
274
|
-
return validateSchemaInput(machine.input, request.input)
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const validateEventInput = (
|
|
278
|
-
machine: Machine.Machine.Any,
|
|
279
|
-
event: unknown
|
|
280
|
-
): Effect.Effect<void, Schema.SchemaError | Error> => {
|
|
281
|
-
if (typeof event !== "object" || event === null || !("_tag" in event)) {
|
|
282
|
-
return Effect.fail(new Error("A public event requires a _tag discriminator"))
|
|
283
|
-
}
|
|
284
|
-
const tag = String(event._tag)
|
|
285
|
-
const schemas = inputEventSchemas(machine)
|
|
286
|
-
const schema = schemas.find((schema) => inputEventTags(schema).has(tag))
|
|
287
|
-
return schema === undefined
|
|
288
|
-
? Effect.fail(new Error(`This machine does not accept the public event ${tag}`))
|
|
289
|
-
: validateSchemaInput(schema, event)
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const configuration = Machine.configuration as (
|
|
293
|
-
machine: Machine.Machine.Any,
|
|
294
|
-
snapshot: unknown
|
|
295
|
-
) => ReadonlyArray<{ readonly path: string }>
|
|
296
|
-
|
|
297
|
-
const enabled = Machine.enabled as (
|
|
298
|
-
machine: Machine.Machine.Any,
|
|
299
|
-
snapshot: unknown
|
|
300
|
-
) => ReadonlyArray<PropertyKey>
|
|
301
|
-
|
|
302
|
-
const simulationEvent = (machine: Machine.Machine.Any, value: Schema.Json): unknown => {
|
|
303
|
-
if (typeof value !== "object" || value === null || Array.isArray(value) || !("_tag" in value)) return value
|
|
304
|
-
const tag = value._tag
|
|
305
|
-
if (typeof tag !== "string" && typeof tag !== "number") return value
|
|
306
|
-
if (!Object.hasOwn(machine.events, tag)) return value
|
|
307
|
-
const constructor = Reflect.get(machine.events, tag)
|
|
308
|
-
if (typeof constructor !== "function") return value
|
|
309
|
-
const { _tag: _, ...payload } = value
|
|
310
|
-
return constructor(payload)
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const simulationSnapshot = (
|
|
314
|
-
machine: Machine.Machine.Any,
|
|
315
|
-
snapshot: unknown
|
|
316
|
-
): DevToolsProtocol.SimulationSnapshot => {
|
|
317
|
-
const publicEvents = new Set(Reflect.ownKeys(machine.events).map(String))
|
|
318
|
-
return {
|
|
319
|
-
activePaths: configuration(machine, snapshot).map((node) => node.path),
|
|
320
|
-
candidateEvents: enabled(machine, snapshot).map(String).filter((event) => publicEvents.has(event))
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
const trigger = (value: Machine.Machine.TransitionTrigger): MachineDocument.Trigger => {
|
|
325
|
-
switch (value.type) {
|
|
326
|
-
case "event":
|
|
327
|
-
return { type: "event", event: String(value.event) }
|
|
328
|
-
case "always":
|
|
329
|
-
return { type: "always" }
|
|
330
|
-
case "done":
|
|
331
|
-
return { type: "done" }
|
|
332
|
-
case "choice":
|
|
333
|
-
return { type: "choice" }
|
|
334
|
-
case "invoke":
|
|
335
|
-
return { type: "invoke", id: value.id, outcome: value.outcome }
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const commandTarget = (target: unknown): string => {
|
|
340
|
-
if (typeof target === "string") return target
|
|
341
|
-
if (typeof target === "object" && target !== null && "id" in target) return String(target.id)
|
|
342
|
-
return String(target)
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const command = (value: Machine.Command): DevToolsProtocol.PlannedCommand =>
|
|
346
|
-
value._tag === "SendTo"
|
|
347
|
-
? { _tag: "SendTo", target: commandTarget(value.target), event: jsonValue(value.event) }
|
|
348
|
-
: { _tag: "Stop", target: commandTarget(value.child) }
|
|
349
|
-
|
|
350
|
-
const microstep = (
|
|
351
|
-
machine: Machine.Machine.Any,
|
|
352
|
-
value: DynamicMicrostep,
|
|
353
|
-
index: number
|
|
354
|
-
): DevToolsProtocol.SimulationMicrostep => ({
|
|
355
|
-
index,
|
|
356
|
-
event: jsonValue(value.event),
|
|
357
|
-
transitions: value.transitions.map((transition) => ({
|
|
358
|
-
source: transition.source,
|
|
359
|
-
trigger: trigger(transition.trigger),
|
|
360
|
-
reenter: transition.reenter,
|
|
361
|
-
branchIndex: transition.branchIndex,
|
|
362
|
-
branchKey: transition.branchKey ?? null,
|
|
363
|
-
target: transition.target ?? null,
|
|
364
|
-
resolvedTarget: transition.resolvedTarget ?? null,
|
|
365
|
-
updates: [...transition.updates]
|
|
366
|
-
})),
|
|
367
|
-
commands: value.commands.map(command),
|
|
368
|
-
raisedEvents: value.raisedEvents.map(jsonValue),
|
|
369
|
-
emittedEvents: value.emittedEvents.map(jsonValue),
|
|
370
|
-
exitPaths: [...value.exitPaths],
|
|
371
|
-
entryPaths: [...value.entryPaths],
|
|
372
|
-
activePaths: configuration(machine, value.next).map((node) => node.path),
|
|
373
|
-
changed: value.changed
|
|
374
|
-
})
|
|
375
|
-
|
|
376
|
-
const simulationDiagnostic = (
|
|
377
|
-
request: DevToolsProtocol.SimulationRequest,
|
|
378
|
-
code: string,
|
|
379
|
-
cause: unknown
|
|
380
|
-
): DevToolsProtocol.SimulationFailed => ({
|
|
381
|
-
_tag: "SimulationFailed",
|
|
382
|
-
protocolVersion: DevToolsProtocol.protocolVersion,
|
|
383
|
-
key: request.key,
|
|
384
|
-
revision: request.revision,
|
|
385
|
-
inputIssues: inputIssuesOf(cause),
|
|
386
|
-
diagnostics: [diagnostic(request.source.file, code, messageOf(cause))]
|
|
387
|
-
})
|
|
388
|
-
|
|
389
|
-
const inputIssuesOf = (cause: unknown): ReadonlyArray<DevToolsProtocol.InputIssue> => {
|
|
390
|
-
const schemaError = Schema.isSchemaError(cause)
|
|
391
|
-
? cause
|
|
392
|
-
: typeof cause === "object" && cause !== null && "cause" in cause && Schema.isSchemaError(cause.cause)
|
|
393
|
-
? cause.cause
|
|
394
|
-
: undefined
|
|
395
|
-
if (schemaError === undefined) return []
|
|
396
|
-
return SchemaIssue.makeFormatterStandardSchemaV1()(schemaError.issue).issues.map((issue) => ({
|
|
397
|
-
path: issue.path?.map((part) => typeof part === "number" ? part : String(part)) ?? [],
|
|
398
|
-
message: issue.message
|
|
399
|
-
}))
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const isProjectFile = (root: string, file: string): boolean => {
|
|
403
|
-
const absoluteRoot = resolve(root)
|
|
404
|
-
const absoluteFile = resolve(absoluteRoot, file)
|
|
405
|
-
const projectPath = relative(absoluteRoot, absoluteFile)
|
|
406
|
-
return projectPath !== "" && !projectPath.startsWith("..") && !isAbsolute(projectPath)
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
const loadSimulationMachine = (
|
|
410
|
-
server: ViteDevServer,
|
|
411
|
-
workerRequest: SimulationWorkerRequest
|
|
412
|
-
): Effect.Effect<Machine.Machine.Any, unknown> => {
|
|
413
|
-
const request = workerRequest.request
|
|
414
|
-
if (!isProjectFile(workerRequest.root, request.source.file)) {
|
|
415
|
-
return Effect.fail(new Error("The requested machine source is outside the project root"))
|
|
416
|
-
}
|
|
417
|
-
if (request.source.exportName === null) {
|
|
418
|
-
return Effect.fail(new Error("The requested machine does not have an exported module binding"))
|
|
419
|
-
}
|
|
420
|
-
return Effect.tryPromise({
|
|
421
|
-
try: () => server.ssrLoadModule(pathToFileURL(resolve(workerRequest.root, request.source.file)).href),
|
|
422
|
-
catch: (cause) => cause
|
|
423
|
-
}).pipe(
|
|
424
|
-
Effect.flatMap((module) => {
|
|
425
|
-
const candidate = module[request.source.exportName!]
|
|
426
|
-
return isMachine(candidate)
|
|
427
|
-
? Effect.succeed(candidate)
|
|
428
|
-
: Effect.fail(new Error(`Export ${request.source.exportName} is not an Effect Machine`))
|
|
429
|
-
})
|
|
430
|
-
)
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
const makeSimulationReady = (
|
|
434
|
-
request: DevToolsProtocol.SimulationRequest,
|
|
435
|
-
machine: Machine.Machine.Any,
|
|
436
|
-
before: unknown,
|
|
437
|
-
after: unknown,
|
|
438
|
-
planResult: DynamicPlan
|
|
439
|
-
): Effect.Effect<DevToolsProtocol.SimulationReady, unknown> =>
|
|
440
|
-
Effect.map(encodeSnapshot(machine, after), (snapshot) => {
|
|
441
|
-
const step = request._tag === "StartSimulation" ? 0 : request.step + 1
|
|
442
|
-
const frame: DevToolsProtocol.SimulationFrame = {
|
|
443
|
-
step,
|
|
444
|
-
trigger: request._tag === "StartSimulation"
|
|
445
|
-
? {
|
|
446
|
-
_tag: "Initial",
|
|
447
|
-
...(Object.hasOwn(request, "input") ? { input: request.input } : {})
|
|
448
|
-
}
|
|
449
|
-
: { _tag: "Event", event: request.event },
|
|
450
|
-
before: simulationSnapshot(machine, before),
|
|
451
|
-
after: simulationSnapshot(machine, after),
|
|
452
|
-
microsteps: planResult.microsteps.map((value, index) => microstep(machine, value, index)),
|
|
453
|
-
commands: planResult.commands.map(command),
|
|
454
|
-
emittedEvents: planResult.emittedEvents.map(jsonValue),
|
|
455
|
-
done: planResult.done,
|
|
456
|
-
...(planResult.done && planResult.output !== undefined ? { output: jsonValue(planResult.output) } : {})
|
|
457
|
-
}
|
|
458
|
-
return {
|
|
459
|
-
_tag: "SimulationReady",
|
|
460
|
-
protocolVersion: DevToolsProtocol.protocolVersion,
|
|
461
|
-
key: request.key,
|
|
462
|
-
revision: request.revision,
|
|
463
|
-
step,
|
|
464
|
-
snapshot,
|
|
465
|
-
current: frame.after,
|
|
466
|
-
frame
|
|
467
|
-
}
|
|
468
|
-
})
|
|
469
|
-
|
|
470
|
-
const simulate = (
|
|
471
|
-
server: ViteDevServer,
|
|
472
|
-
workerRequest: SimulationWorkerRequest
|
|
473
|
-
): Effect.Effect<DevToolsProtocol.SimulationResult> => {
|
|
474
|
-
const request = workerRequest.request
|
|
475
|
-
return loadSimulationMachine(server, workerRequest).pipe(
|
|
476
|
-
Effect.flatMap((machine) => {
|
|
477
|
-
if (request._tag === "StartSimulation") {
|
|
478
|
-
return Effect.flatMap(validateInitialInput(machine, request), () => {
|
|
479
|
-
const planned = Object.hasOwn(request, "input")
|
|
480
|
-
? planInitial(machine, request.input)
|
|
481
|
-
: planInitial(machine)
|
|
482
|
-
return Effect.flatMap(planned, (result) => {
|
|
483
|
-
const before = result.startingState ?? result.state
|
|
484
|
-
const after = result.state
|
|
485
|
-
if (before === undefined || after === undefined) {
|
|
486
|
-
return Effect.fail(new Error("The initial planner did not return a state snapshot"))
|
|
487
|
-
}
|
|
488
|
-
return makeSimulationReady(request, machine, before, after, result)
|
|
489
|
-
})
|
|
490
|
-
})
|
|
491
|
-
}
|
|
492
|
-
return Effect.flatMap(validateEventInput(machine, request.event), () =>
|
|
493
|
-
Effect.flatMap(
|
|
494
|
-
decodeSnapshot(machine, request.snapshot),
|
|
495
|
-
(before) =>
|
|
496
|
-
Effect.flatMap(plan(machine, before, simulationEvent(machine, request.event)), (result) => {
|
|
497
|
-
if (result.next === undefined) return Effect.fail(new Error("The planner did not return a next snapshot"))
|
|
498
|
-
return makeSimulationReady(request, machine, before, result.next, result)
|
|
499
|
-
})
|
|
500
|
-
))
|
|
501
|
-
}),
|
|
502
|
-
Effect.catch((cause) => Effect.succeed(simulationDiagnostic(request, "simulation-planning-failed", cause)))
|
|
503
|
-
)
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
export const run = (server: ViteDevServer): Promise<void> => {
|
|
507
|
-
return Effect.gen(function*() {
|
|
148
|
+
export const run = (server: ViteDevServer): Promise<void> =>
|
|
149
|
+
Effect.gen(function*() {
|
|
508
150
|
const platform = yield* WorkerRunner.WorkerRunnerPlatform
|
|
509
151
|
const runner = yield* platform.start<WorkerResponse, WorkerRequest>()
|
|
510
|
-
yield* runner.run((_portId, request) =>
|
|
511
|
-
const response: Effect.Effect<WorkerResponse> = request._tag === "InspectMachines"
|
|
512
|
-
? handle(server, request)
|
|
513
|
-
: simulate(server, request)
|
|
514
|
-
return Effect.flatMap(response, (value) => runner.send(0, value))
|
|
515
|
-
})
|
|
152
|
+
yield* runner.run((_portId, request) => Effect.flatMap(handle(server, request), (value) => runner.send(0, value)))
|
|
516
153
|
}).pipe(
|
|
517
154
|
Effect.provide(NodeWorkerRunner.layer),
|
|
518
155
|
Effect.runPromise
|
|
519
156
|
)
|
|
520
|
-
}
|
|
@@ -169,7 +169,7 @@ export const make = <M extends Machine.Machine.Any>(
|
|
|
169
169
|
|
|
170
170
|
const initial = Machine.initialDefinition(machine)
|
|
171
171
|
return {
|
|
172
|
-
schemaVersion:
|
|
172
|
+
schemaVersion: 3,
|
|
173
173
|
revision: options.revision ?? 0,
|
|
174
174
|
source: options.source ?? null,
|
|
175
175
|
machineId: machine.id ?? "Machine",
|
|
@@ -190,6 +190,8 @@ export const make = <M extends Machine.Machine.Any>(
|
|
|
190
190
|
parent: node.parent ?? null,
|
|
191
191
|
children: [...childPaths.get(node.path) ?? []],
|
|
192
192
|
initial: node.initial ?? null,
|
|
193
|
+
valueSchema: node.schema === undefined ? null : inputSchema(node.schema),
|
|
194
|
+
outputSchema: node.output === undefined ? null : inputSchema(node.output),
|
|
193
195
|
transitionIds: [...transitionIds.get(node.path) ?? []],
|
|
194
196
|
activityIds: [...activityIds.get(node.path) ?? []]
|
|
195
197
|
})),
|