@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.
Files changed (50) hide show
  1. package/README.md +10 -4
  2. package/dist/DevServer.d.ts +2 -1
  3. package/dist/DevServer.d.ts.map +1 -1
  4. package/dist/DevServer.js.map +1 -1
  5. package/dist/DevToolsProtocol.d.ts +731 -17
  6. package/dist/DevToolsProtocol.d.ts.map +1 -1
  7. package/dist/DevToolsProtocol.js +191 -1
  8. package/dist/DevToolsProtocol.js.map +1 -1
  9. package/dist/MachineDocument.d.ts +78 -2
  10. package/dist/MachineDocument.d.ts.map +1 -1
  11. package/dist/MachineDocument.js +33 -1
  12. package/dist/MachineDocument.js.map +1 -1
  13. package/dist/MachineRegistry.d.ts +36 -6
  14. package/dist/MachineRegistry.d.ts.map +1 -1
  15. package/dist/ProjectInspector.d.ts +2 -0
  16. package/dist/ProjectInspector.d.ts.map +1 -1
  17. package/dist/ProjectInspector.js.map +1 -1
  18. package/dist/client/assets/index-B49Tjawc.js +14 -0
  19. package/dist/client/assets/index-BKH0cOG2.css +1 -0
  20. package/dist/client/index.html +2 -2
  21. package/dist/internal/devServer.d.ts +2 -1
  22. package/dist/internal/devServer.d.ts.map +1 -1
  23. package/dist/internal/devServer.js +68 -7
  24. package/dist/internal/devServer.js.map +1 -1
  25. package/dist/internal/evaluationWorker.d.ts.map +1 -1
  26. package/dist/internal/evaluationWorker.js +279 -4
  27. package/dist/internal/evaluationWorker.js.map +1 -1
  28. package/dist/internal/machineDocument.d.ts.map +1 -1
  29. package/dist/internal/machineDocument.js +63 -2
  30. package/dist/internal/machineDocument.js.map +1 -1
  31. package/dist/internal/projectInspector.d.ts.map +1 -1
  32. package/dist/internal/projectInspector.js +26 -10
  33. package/dist/internal/projectInspector.js.map +1 -1
  34. package/package.json +2 -2
  35. package/src/DevServer.ts +6 -2
  36. package/src/DevToolsProtocol.ts +286 -1
  37. package/src/MachineDocument.ts +54 -1
  38. package/src/ProjectInspector.ts +5 -0
  39. package/src/internal/browser/input-form.ts +669 -0
  40. package/src/internal/browser/planner-example.ts +143 -0
  41. package/src/internal/browser/simulation-client.ts +20 -0
  42. package/src/internal/browser/styles.css +323 -3
  43. package/src/internal/browser/visualizer-app.ts +395 -34
  44. package/src/internal/browser/visualizer.ts +1 -1
  45. package/src/internal/devServer.ts +91 -8
  46. package/src/internal/evaluationWorker.ts +390 -8
  47. package/src/internal/machineDocument.ts +75 -2
  48. package/src/internal/projectInspector.ts +58 -15
  49. package/dist/client/assets/index-BGOhE3Ng.css +0 -1
  50. 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 = 200
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
- registry: MachineRegistry.MachineRegistry["Service"]
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<never, DevServer.DevServerError, MachineRegistry.MachineRegistry> =>
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 type { Machine } from "@typeonce/effect-machine"
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 => cause instanceof Error ? cause.message : String(cause)
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<EvaluationResponse, EvaluationRequest>()
131
- yield* runner.run((_portId, request) =>
132
- Effect.flatMap(handle(server, request), (response) => runner.send(0, response))
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