@typeonce/effect-machine-devtools 0.23.0 → 0.24.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 +10 -4
- package/dist/DevServer.d.ts +2 -1
- package/dist/DevServer.d.ts.map +1 -1
- package/dist/DevServer.js.map +1 -1
- package/dist/DevToolsProtocol.d.ts +731 -17
- package/dist/DevToolsProtocol.d.ts.map +1 -1
- package/dist/DevToolsProtocol.js +191 -1
- package/dist/DevToolsProtocol.js.map +1 -1
- package/dist/MachineDocument.d.ts +78 -2
- package/dist/MachineDocument.d.ts.map +1 -1
- package/dist/MachineDocument.js +33 -1
- package/dist/MachineDocument.js.map +1 -1
- package/dist/MachineRegistry.d.ts +36 -6
- package/dist/MachineRegistry.d.ts.map +1 -1
- package/dist/ProjectInspector.d.ts +2 -0
- package/dist/ProjectInspector.d.ts.map +1 -1
- package/dist/ProjectInspector.js.map +1 -1
- package/dist/client/assets/index-B49Tjawc.js +14 -0
- package/dist/client/assets/index-BKH0cOG2.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/internal/devServer.d.ts +2 -1
- package/dist/internal/devServer.d.ts.map +1 -1
- package/dist/internal/devServer.js +68 -7
- package/dist/internal/devServer.js.map +1 -1
- package/dist/internal/evaluationWorker.d.ts.map +1 -1
- package/dist/internal/evaluationWorker.js +279 -4
- package/dist/internal/evaluationWorker.js.map +1 -1
- package/dist/internal/machineDocument.d.ts.map +1 -1
- package/dist/internal/machineDocument.js +63 -2
- package/dist/internal/machineDocument.js.map +1 -1
- package/dist/internal/projectInspector.d.ts.map +1 -1
- package/dist/internal/projectInspector.js +26 -10
- package/dist/internal/projectInspector.js.map +1 -1
- package/package.json +2 -2
- package/src/DevServer.ts +6 -2
- package/src/DevToolsProtocol.ts +286 -1
- package/src/MachineDocument.ts +54 -1
- package/src/ProjectInspector.ts +5 -0
- package/src/internal/browser/input-form.ts +669 -0
- package/src/internal/browser/planner-example.ts +143 -0
- package/src/internal/browser/simulation-client.ts +20 -0
- package/src/internal/browser/styles.css +323 -3
- package/src/internal/browser/visualizer-app.ts +395 -34
- package/src/internal/browser/visualizer.ts +1 -1
- package/src/internal/devServer.ts +91 -8
- package/src/internal/evaluationWorker.ts +390 -8
- package/src/internal/machineDocument.ts +75 -2
- package/src/internal/projectInspector.ts +58 -15
- package/dist/client/assets/index-BGOhE3Ng.css +0 -1
- package/dist/client/assets/index-DiqGhsGR.js +0 -14
|
@@ -1,12 +1,15 @@
|
|
|
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"
|
|
3
4
|
import * as Stream from "effect/Stream"
|
|
4
5
|
import type { IncomingMessage, ServerResponse } from "node:http"
|
|
5
6
|
import { isAbsolute, relative, resolve } from "node:path"
|
|
6
7
|
import { fileURLToPath } from "node:url"
|
|
7
8
|
import { createServer, type Plugin, type ViteDevServer } from "vite"
|
|
8
9
|
import type * as DevServer from "../DevServer.js"
|
|
10
|
+
import * as DevToolsProtocol from "../DevToolsProtocol.js"
|
|
9
11
|
import * as MachineRegistry from "../MachineRegistry.js"
|
|
12
|
+
import * as ProjectInspector from "../ProjectInspector.js"
|
|
10
13
|
|
|
11
14
|
type DevServerErrorConstructor = typeof DevServer.DevServerError
|
|
12
15
|
|
|
@@ -41,20 +44,63 @@ const isIgnored = (file: string): boolean =>
|
|
|
41
44
|
part === "references"
|
|
42
45
|
)
|
|
43
46
|
|
|
44
|
-
const writeJson = (response: ServerResponse, value: unknown): void => {
|
|
45
|
-
response.statusCode =
|
|
47
|
+
const writeJson = (response: ServerResponse, value: unknown, status = 200): void => {
|
|
48
|
+
response.statusCode = status
|
|
46
49
|
response.setHeader("content-type", "application/json; charset=utf-8")
|
|
47
50
|
response.setHeader("cache-control", "no-store")
|
|
48
51
|
response.end(JSON.stringify(value))
|
|
49
52
|
}
|
|
50
53
|
|
|
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
|
+
|
|
51
95
|
const apiPlugin = (
|
|
52
|
-
|
|
96
|
+
root: string,
|
|
97
|
+
registry: MachineRegistry.MachineRegistry["Service"],
|
|
98
|
+
inspector: ProjectInspector.ProjectInspector["Service"]
|
|
53
99
|
): Plugin => ({
|
|
54
100
|
name: "effect-machine-devtools-api",
|
|
55
101
|
configureServer(server) {
|
|
56
102
|
server.middlewares.use((request: IncomingMessage, response: ServerResponse, next: () => void) => {
|
|
57
|
-
if (request.url === "/api/machines") {
|
|
103
|
+
if (request.method === "GET" && request.url === "/api/machines") {
|
|
58
104
|
void Effect.runPromise(registry.get).then(
|
|
59
105
|
(snapshot) => writeJson(response, snapshot),
|
|
60
106
|
(cause) => {
|
|
@@ -64,6 +110,37 @@ const apiPlugin = (
|
|
|
64
110
|
)
|
|
65
111
|
return
|
|
66
112
|
}
|
|
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
|
+
}
|
|
67
144
|
if (request.url !== "/api/events") {
|
|
68
145
|
next()
|
|
69
146
|
return
|
|
@@ -90,7 +167,8 @@ const apiPlugin = (
|
|
|
90
167
|
const acquire = (
|
|
91
168
|
ErrorType: DevServerErrorConstructor,
|
|
92
169
|
options: DevServer.Options,
|
|
93
|
-
registry: MachineRegistry.MachineRegistry["Service"]
|
|
170
|
+
registry: MachineRegistry.MachineRegistry["Service"],
|
|
171
|
+
inspector: ProjectInspector.ProjectInspector["Service"]
|
|
94
172
|
): Effect.Effect<ViteDevServer, DevServer.DevServerError, never> =>
|
|
95
173
|
Effect.tryPromise({
|
|
96
174
|
try: async () => {
|
|
@@ -98,7 +176,7 @@ const acquire = (
|
|
|
98
176
|
root: packageRoot,
|
|
99
177
|
appType: "spa",
|
|
100
178
|
logLevel: "error",
|
|
101
|
-
plugins: [apiPlugin(registry)],
|
|
179
|
+
plugins: [apiPlugin(options.root, registry, inspector)],
|
|
102
180
|
server: {
|
|
103
181
|
host: options.host,
|
|
104
182
|
port: options.port,
|
|
@@ -154,11 +232,16 @@ export const watcherOptions = (options: DevServer.Options): ChokidarOptions =>
|
|
|
154
232
|
export const run = (
|
|
155
233
|
ErrorType: DevServerErrorConstructor,
|
|
156
234
|
options: DevServer.Options
|
|
157
|
-
): Effect.Effect<
|
|
235
|
+
): Effect.Effect<
|
|
236
|
+
never,
|
|
237
|
+
DevServer.DevServerError,
|
|
238
|
+
MachineRegistry.MachineRegistry | ProjectInspector.ProjectInspector
|
|
239
|
+
> =>
|
|
158
240
|
Effect.gen(function*() {
|
|
159
241
|
const registry = yield* MachineRegistry.MachineRegistry
|
|
242
|
+
const inspector = yield* ProjectInspector.ProjectInspector
|
|
160
243
|
const server = yield* Effect.acquireRelease(
|
|
161
|
-
acquire(ErrorType, options, registry),
|
|
244
|
+
acquire(ErrorType, options, registry, inspector),
|
|
162
245
|
(server) => Effect.promise(() => server.close())
|
|
163
246
|
)
|
|
164
247
|
yield* Effect.acquireRelease(
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import * as NodeWorkerRunner from "@effect/platform-node/NodeWorkerRunner"
|
|
2
|
-
import
|
|
2
|
+
import { Machine } from "@typeonce/effect-machine"
|
|
3
3
|
import * as Effect from "effect/Effect"
|
|
4
|
+
import * as Schema from "effect/Schema"
|
|
5
|
+
import * as SchemaIssue from "effect/SchemaIssue"
|
|
4
6
|
import * as WorkerRunner from "effect/unstable/workers/WorkerRunner"
|
|
5
|
-
import { resolve } from "node:path"
|
|
7
|
+
import { isAbsolute, relative, resolve } from "node:path"
|
|
6
8
|
import { pathToFileURL } from "node:url"
|
|
7
9
|
import type { ViteDevServer } from "vite"
|
|
8
10
|
import * as DevToolsProtocol from "../DevToolsProtocol.js"
|
|
@@ -10,15 +12,26 @@ import * as MachineDocument from "../MachineDocument.js"
|
|
|
10
12
|
import type * as ProjectInspector from "../ProjectInspector.js"
|
|
11
13
|
|
|
12
14
|
interface EvaluationRequest {
|
|
15
|
+
readonly _tag: "InspectMachines"
|
|
13
16
|
readonly root: string
|
|
14
17
|
readonly revision: number
|
|
15
18
|
readonly candidates: ReadonlyArray<ProjectInspector.Candidate>
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
interface EvaluationResponse {
|
|
22
|
+
readonly _tag: "InspectedMachines"
|
|
19
23
|
readonly results: ReadonlyArray<DevToolsProtocol.MachineResult>
|
|
20
24
|
}
|
|
21
25
|
|
|
26
|
+
interface SimulationWorkerRequest {
|
|
27
|
+
readonly _tag: "Simulate"
|
|
28
|
+
readonly root: string
|
|
29
|
+
readonly request: DevToolsProtocol.SimulationRequest
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type WorkerRequest = EvaluationRequest | SimulationWorkerRequest
|
|
33
|
+
type WorkerResponse = EvaluationResponse | DevToolsProtocol.SimulationResult
|
|
34
|
+
|
|
22
35
|
const isMachine = (value: unknown): value is Machine.Machine.Any =>
|
|
23
36
|
typeof value === "object" &&
|
|
24
37
|
value !== null &&
|
|
@@ -41,7 +54,38 @@ const diagnostic = (
|
|
|
41
54
|
statePath: null
|
|
42
55
|
})
|
|
43
56
|
|
|
44
|
-
const messageOf = (cause: unknown): string =>
|
|
57
|
+
const messageOf = (cause: unknown): string => {
|
|
58
|
+
if (Schema.isSchemaError(cause)) return `Invalid machine value: ${cause.message}`
|
|
59
|
+
if (cause instanceof Error && cause.message.length > 0) return cause.message
|
|
60
|
+
if (typeof cause === "object" && cause !== null && "cause" in cause && Schema.isSchemaError(cause.cause)) {
|
|
61
|
+
const boundary = "boundary" in cause ? String(cause.boundary) : "value"
|
|
62
|
+
return `Invalid machine ${boundary}: ${cause.cause.message}`
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const encoded = JSON.stringify(cause, null, 2)
|
|
66
|
+
if (encoded !== undefined && encoded !== "{}") return encoded
|
|
67
|
+
} catch {
|
|
68
|
+
// Fall back to the runtime string representation below.
|
|
69
|
+
}
|
|
70
|
+
const rendered = String(cause)
|
|
71
|
+
return rendered.length > 0 ? rendered : "The planner failed without a diagnostic message"
|
|
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
|
|
88
|
+
}
|
|
45
89
|
|
|
46
90
|
const failed = (
|
|
47
91
|
candidate: ProjectInspector.Candidate,
|
|
@@ -121,16 +165,354 @@ const handle = (server: ViteDevServer, request: EvaluationRequest): Effect.Effec
|
|
|
121
165
|
Effect.forEach(request.candidates, (candidate) => evaluateCandidate(server, request, candidate), {
|
|
122
166
|
concurrency: 1
|
|
123
167
|
}).pipe(
|
|
124
|
-
Effect.map((results) => ({ results: results.flat() }))
|
|
168
|
+
Effect.map((results) => ({ _tag: "InspectedMachines" as const, results: results.flat() }))
|
|
125
169
|
)
|
|
126
170
|
|
|
171
|
+
interface DynamicMicrostep {
|
|
172
|
+
readonly next: unknown
|
|
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
|
+
|
|
127
506
|
export const run = (server: ViteDevServer): Promise<void> => {
|
|
128
507
|
return Effect.gen(function*() {
|
|
129
508
|
const platform = yield* WorkerRunner.WorkerRunnerPlatform
|
|
130
|
-
const runner = yield* platform.start<
|
|
131
|
-
yield* runner.run((_portId, request) =>
|
|
132
|
-
Effect.
|
|
133
|
-
|
|
509
|
+
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
|
+
})
|
|
134
516
|
}).pipe(
|
|
135
517
|
Effect.provide(NodeWorkerRunner.layer),
|
|
136
518
|
Effect.runPromise
|