@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,11 +1,18 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
type Diagnostic,
|
|
3
|
+
protocolVersion,
|
|
4
|
+
type SimulationFrame,
|
|
5
|
+
type SimulationReady,
|
|
6
|
+
type SimulationRequest
|
|
7
|
+
} from "../../DevToolsProtocol.js"
|
|
2
8
|
import type {
|
|
3
9
|
Activity as VisualizationActivity,
|
|
4
10
|
Branch as VisualizationBranch,
|
|
5
11
|
MachineDocument as VisualizationDocument,
|
|
6
12
|
Transition as VisualizationTransition
|
|
7
13
|
} from "../../MachineDocument.js"
|
|
8
|
-
import
|
|
14
|
+
import { type InputForm, renderInputForm } from "./input-form.js"
|
|
15
|
+
import { requestSimulation } from "./simulation-client.js"
|
|
9
16
|
import {
|
|
10
17
|
type EventInspection,
|
|
11
18
|
type IncomingTransition,
|
|
@@ -159,22 +166,27 @@ const inspectionSection = (title: string, count: number): HTMLElement => {
|
|
|
159
166
|
return header
|
|
160
167
|
}
|
|
161
168
|
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
169
|
+
const formSection = (title: string): HTMLElement => {
|
|
170
|
+
const header = createElement("div", "section-heading")
|
|
171
|
+
header.append(createElement("h3", undefined, title))
|
|
172
|
+
return header
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const prettyJson = (value: unknown): string => JSON.stringify(value, null, 2)
|
|
176
|
+
|
|
177
|
+
const jsonBlock = (value: unknown): HTMLElement => createElement("pre", "json-value", prettyJson(value))
|
|
178
|
+
|
|
179
|
+
const eventName = (value: unknown): string =>
|
|
180
|
+
typeof value === "object" && value !== null && "_tag" in value ? String(value._tag) : "event"
|
|
181
|
+
|
|
182
|
+
const activeTopology = (paths: ReadonlyArray<string>): string => {
|
|
183
|
+
const leaves = paths.filter((path) => !paths.some((candidate) => candidate.startsWith(`${path}.`)))
|
|
184
|
+
return leaves.map((path) => path.split(".").at(-1) ?? path).join(" + ") || "none"
|
|
174
185
|
}
|
|
175
186
|
|
|
176
187
|
export const renderVisualizer = (
|
|
177
188
|
root: HTMLElement,
|
|
189
|
+
machineKey: string,
|
|
178
190
|
visualization: VisualizationDocument,
|
|
179
191
|
diagnostics: ReadonlyArray<Diagnostic> = []
|
|
180
192
|
): void => {
|
|
@@ -183,13 +195,17 @@ export const renderVisualizer = (
|
|
|
183
195
|
const nodes = new Map<string, HTMLElement>()
|
|
184
196
|
const statuses = new Map<string, HTMLElement>()
|
|
185
197
|
const eventButtons = new Map<string, HTMLElement>()
|
|
198
|
+
const eventSchemas = new Map(visualization.inputs.events.map(({ event, schema }) => [event, schema]))
|
|
186
199
|
const relatedPaths = new Set<string>()
|
|
187
200
|
let selectedPath: string | undefined
|
|
188
201
|
let selectedEvent: string | undefined
|
|
189
|
-
let
|
|
202
|
+
let selectedFrame: SimulationFrame | undefined
|
|
203
|
+
let simulation: SimulationReady | undefined
|
|
204
|
+
let simulationPending = false
|
|
205
|
+
let activeInputForm: InputForm | undefined
|
|
190
206
|
|
|
191
|
-
const activePaths = (): ReadonlyArray<string> => simulation?.
|
|
192
|
-
const candidateEvents = (): ReadonlyArray<string> => simulation?.
|
|
207
|
+
const activePaths = (): ReadonlyArray<string> => simulation?.current.activePaths ?? model.activePaths
|
|
208
|
+
const candidateEvents = (): ReadonlyArray<string> => simulation?.current.candidateEvents ?? model.candidateEvents
|
|
193
209
|
|
|
194
210
|
const shell = createElement("main", "app-shell")
|
|
195
211
|
const workspace = createElement("section", "workspace")
|
|
@@ -213,6 +229,7 @@ export const renderVisualizer = (
|
|
|
213
229
|
simulationButton.disabled = model.roots.length === 0
|
|
214
230
|
|
|
215
231
|
const renderEmptyInspector = (): void => {
|
|
232
|
+
activeInputForm = undefined
|
|
216
233
|
inspector.replaceChildren()
|
|
217
234
|
const summary = createElement("div", "inspector-empty")
|
|
218
235
|
summary.append(createElement("span", "inspector-empty-kind", "Machine"))
|
|
@@ -229,6 +246,7 @@ export const renderVisualizer = (
|
|
|
229
246
|
}
|
|
230
247
|
|
|
231
248
|
const renderInspection = (inspection: StateInspection): void => {
|
|
249
|
+
activeInputForm = undefined
|
|
232
250
|
inspector.replaceChildren()
|
|
233
251
|
const header = createElement("header", "inspector-header")
|
|
234
252
|
const breadcrumbs = createElement("nav", "breadcrumbs")
|
|
@@ -290,6 +308,7 @@ export const renderVisualizer = (
|
|
|
290
308
|
}
|
|
291
309
|
|
|
292
310
|
const renderEventInspection = (inspection: EventInspection): void => {
|
|
311
|
+
activeInputForm = undefined
|
|
293
312
|
inspector.replaceChildren()
|
|
294
313
|
const header = createElement("header", "inspector-header")
|
|
295
314
|
const eyebrow = createElement("div", "inspector-eyebrow")
|
|
@@ -303,6 +322,55 @@ export const renderVisualizer = (
|
|
|
303
322
|
]))
|
|
304
323
|
inspector.append(header)
|
|
305
324
|
|
|
325
|
+
if (simulation !== undefined && candidate) {
|
|
326
|
+
const schema = eventSchemas.get(inspection.event)
|
|
327
|
+
const composer = createElement("section", "inspector-section simulation-composer")
|
|
328
|
+
composer.append(formSection("Event input"))
|
|
329
|
+
if (schema === undefined) {
|
|
330
|
+
composer.append(createElement(
|
|
331
|
+
"p",
|
|
332
|
+
"input-unsupported",
|
|
333
|
+
"No public input schema is available for this event."
|
|
334
|
+
))
|
|
335
|
+
} else {
|
|
336
|
+
const input = renderInputForm(schema, {
|
|
337
|
+
name: inspection.event,
|
|
338
|
+
fixed: { _tag: inspection.event },
|
|
339
|
+
omit: ["_tag"]
|
|
340
|
+
})
|
|
341
|
+
activeInputForm = input
|
|
342
|
+
const send = createElement("button", "simulation-action", `Send ${inspection.event}`)
|
|
343
|
+
send.type = "submit"
|
|
344
|
+
send.disabled = simulationPending || !input.supported
|
|
345
|
+
input.element.addEventListener("submit", (event) => {
|
|
346
|
+
event.preventDefault()
|
|
347
|
+
const result = input.read()
|
|
348
|
+
const source = visualization.source
|
|
349
|
+
if (!result.ok || source === null || simulation === undefined) return
|
|
350
|
+
void runSimulation({
|
|
351
|
+
_tag: "SendSimulationEvent",
|
|
352
|
+
protocolVersion,
|
|
353
|
+
key: machineKey,
|
|
354
|
+
revision: visualization.revision,
|
|
355
|
+
source,
|
|
356
|
+
step: simulation.step,
|
|
357
|
+
snapshot: simulation.snapshot,
|
|
358
|
+
event: result.value as never
|
|
359
|
+
})
|
|
360
|
+
})
|
|
361
|
+
input.element.append(send)
|
|
362
|
+
composer.append(
|
|
363
|
+
input.element,
|
|
364
|
+
createElement(
|
|
365
|
+
"p",
|
|
366
|
+
"simulation-note",
|
|
367
|
+
"The real planner validates this event and shows every synchronous transition it selects."
|
|
368
|
+
)
|
|
369
|
+
)
|
|
370
|
+
}
|
|
371
|
+
inspector.append(composer)
|
|
372
|
+
}
|
|
373
|
+
|
|
306
374
|
const transitions = createElement("section", "inspector-section")
|
|
307
375
|
transitions.append(inspectionSection("Transitions", inspection.transitions.length))
|
|
308
376
|
inspection.transitions.forEach((transition) =>
|
|
@@ -311,6 +379,265 @@ export const renderVisualizer = (
|
|
|
311
379
|
inspector.append(transitions)
|
|
312
380
|
}
|
|
313
381
|
|
|
382
|
+
const renderSimulationFailure = (failureDiagnostics: ReadonlyArray<Diagnostic>): void => {
|
|
383
|
+
activeInputForm = undefined
|
|
384
|
+
inspector.replaceChildren()
|
|
385
|
+
const header = createElement("header", "inspector-header")
|
|
386
|
+
const eyebrow = createElement("div", "inspector-eyebrow")
|
|
387
|
+
eyebrow.append(badge("planner error", "error"))
|
|
388
|
+
header.append(eyebrow, createElement("h2", undefined, "Simulation could not continue"))
|
|
389
|
+
inspector.append(header)
|
|
390
|
+
const list = createElement("section", "inspector-section trace-list")
|
|
391
|
+
failureDiagnostics.forEach((item) => {
|
|
392
|
+
const card = createElement("article", "inspection-card trace-card")
|
|
393
|
+
card.append(createElement("strong", undefined, item.code), createElement("pre", "trace-error", item.message))
|
|
394
|
+
list.append(card)
|
|
395
|
+
})
|
|
396
|
+
inspector.append(list)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const renderPathGroup = (label: string, paths: ReadonlyArray<string>): HTMLElement => {
|
|
400
|
+
const group = createElement("div", "trace-path-group")
|
|
401
|
+
group.append(createElement("span", "trace-label", label))
|
|
402
|
+
const values = createElement("div", "trace-paths")
|
|
403
|
+
if (paths.length === 0) values.append(createElement("span", "section-empty", "none"))
|
|
404
|
+
paths.forEach((path) => values.append(stateLink(path, path, navigateToState)))
|
|
405
|
+
group.append(values)
|
|
406
|
+
return group
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const renderTraceValues = (label: string, values: ReadonlyArray<unknown>): HTMLElement => {
|
|
410
|
+
const section = createElement("div", "trace-values")
|
|
411
|
+
section.append(createElement("span", "trace-label", label))
|
|
412
|
+
if (values.length === 0) {
|
|
413
|
+
section.append(createElement("span", "section-empty", "none"))
|
|
414
|
+
} else {
|
|
415
|
+
values.forEach((value) => section.append(jsonBlock(value)))
|
|
416
|
+
}
|
|
417
|
+
return section
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const renderSimulationTrace = (frame: SimulationFrame): void => {
|
|
421
|
+
activeInputForm = undefined
|
|
422
|
+
inspector.replaceChildren()
|
|
423
|
+
const header = createElement("header", "inspector-header trace-header")
|
|
424
|
+
const eyebrow = createElement("div", "inspector-eyebrow")
|
|
425
|
+
eyebrow.append(badge("planned", "active"))
|
|
426
|
+
if (frame.done) eyebrow.append(badge("done", "state"))
|
|
427
|
+
const title = frame.trigger._tag === "Initial"
|
|
428
|
+
? "Initial plan"
|
|
429
|
+
: `Event · ${eventName(frame.trigger.event)}`
|
|
430
|
+
header.append(eyebrow, createElement("h2", undefined, title))
|
|
431
|
+
header.append(metadata([
|
|
432
|
+
["Step", String(frame.step)],
|
|
433
|
+
["Microsteps", String(frame.microsteps.length)],
|
|
434
|
+
["Active before", String(frame.before.activePaths.length)],
|
|
435
|
+
["Active after", String(frame.after.activePaths.length)]
|
|
436
|
+
]))
|
|
437
|
+
const received = frame.trigger._tag === "Initial" ? frame.trigger.input : frame.trigger.event
|
|
438
|
+
if (received !== undefined) header.append(jsonBlock(received))
|
|
439
|
+
inspector.append(header)
|
|
440
|
+
|
|
441
|
+
const topology = createElement("section", "inspector-section trace-topology")
|
|
442
|
+
topology.append(inspectionSection("Topology change", frame.microsteps.length))
|
|
443
|
+
topology.append(
|
|
444
|
+
renderPathGroup("Before", frame.before.activePaths),
|
|
445
|
+
renderPathGroup("After", frame.after.activePaths)
|
|
446
|
+
)
|
|
447
|
+
inspector.append(topology)
|
|
448
|
+
|
|
449
|
+
const steps = createElement("section", "inspector-section trace-list")
|
|
450
|
+
steps.append(inspectionSection("Microsteps", frame.microsteps.length))
|
|
451
|
+
if (frame.microsteps.length === 0) {
|
|
452
|
+
steps.append(createElement(
|
|
453
|
+
"p",
|
|
454
|
+
"section-empty",
|
|
455
|
+
frame.trigger._tag === "Initial"
|
|
456
|
+
? "No automatic microsteps were needed."
|
|
457
|
+
: "No transition accepted this event."
|
|
458
|
+
))
|
|
459
|
+
}
|
|
460
|
+
frame.microsteps.forEach((step) => {
|
|
461
|
+
const card = createElement("article", "inspection-card trace-card")
|
|
462
|
+
const cardHeader = createElement("div", "card-header")
|
|
463
|
+
const cardTitle = createElement("div", "card-title")
|
|
464
|
+
cardTitle.append(
|
|
465
|
+
badge(`#${step.index + 1}`, "count"),
|
|
466
|
+
createElement("strong", undefined, eventName(step.event))
|
|
467
|
+
)
|
|
468
|
+
cardHeader.append(cardTitle, badge(step.changed ? "changed" : "unchanged", step.changed ? "active" : "neutral"))
|
|
469
|
+
card.append(cardHeader)
|
|
470
|
+
if (step.transitions.length > 0) {
|
|
471
|
+
const selected = createElement("div", "trace-transitions")
|
|
472
|
+
selected.append(createElement("span", "trace-label", "Selected transitions"))
|
|
473
|
+
step.transitions.forEach((transition) => {
|
|
474
|
+
const row = createElement("div", "trace-transition")
|
|
475
|
+
row.append(stateLink(transition.source, transition.source, navigateToState))
|
|
476
|
+
row.append(createElement("span", "branch-arrow", "→"))
|
|
477
|
+
const target = transition.resolvedTarget ?? transition.target
|
|
478
|
+
if (target === null) row.append(createElement("span", "branch-target", "No target"))
|
|
479
|
+
else row.append(stateLink(target, target, navigateToState))
|
|
480
|
+
if (transition.branchKey !== null) row.append(badge(transition.branchKey, "condition"))
|
|
481
|
+
if (transition.reenter) row.append(badge("reenter"))
|
|
482
|
+
selected.append(row)
|
|
483
|
+
if (transition.updates.length > 0) selected.append(renderPathGroup("Updates", transition.updates))
|
|
484
|
+
})
|
|
485
|
+
card.append(selected)
|
|
486
|
+
}
|
|
487
|
+
card.append(renderPathGroup("Exit", step.exitPaths), renderPathGroup("Entry", step.entryPaths))
|
|
488
|
+
if (step.raisedEvents.length > 0) card.append(renderTraceValues("Raised", step.raisedEvents))
|
|
489
|
+
if (step.emittedEvents.length > 0) card.append(renderTraceValues("Emitted", step.emittedEvents))
|
|
490
|
+
if (step.commands.length > 0) card.append(renderTraceValues("Commands", step.commands))
|
|
491
|
+
card.append(renderPathGroup("Active after", step.activePaths))
|
|
492
|
+
steps.append(card)
|
|
493
|
+
})
|
|
494
|
+
inspector.append(steps)
|
|
495
|
+
|
|
496
|
+
if (frame.commands.length > 0 || frame.emittedEvents.length > 0 || frame.output !== undefined) {
|
|
497
|
+
const results = createElement("section", "inspector-section trace-results")
|
|
498
|
+
results.append(inspectionSection("Plan result", frame.commands.length + frame.emittedEvents.length))
|
|
499
|
+
if (frame.commands.length > 0) results.append(renderTraceValues("Planned commands", frame.commands))
|
|
500
|
+
if (frame.emittedEvents.length > 0) results.append(renderTraceValues("Emitted events", frame.emittedEvents))
|
|
501
|
+
if (frame.output !== undefined) results.append(renderTraceValues("Output", [frame.output]))
|
|
502
|
+
inspector.append(results)
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const renderStartSimulation = (): void => {
|
|
507
|
+
activeInputForm = undefined
|
|
508
|
+
inspector.replaceChildren()
|
|
509
|
+
const header = createElement("header", "inspector-header")
|
|
510
|
+
const eyebrow = createElement("div", "inspector-eyebrow")
|
|
511
|
+
eyebrow.append(badge("planner", "active"))
|
|
512
|
+
header.append(eyebrow, createElement("h2", undefined, "Start simulation"))
|
|
513
|
+
inspector.append(header)
|
|
514
|
+
const composer = createElement("section", "inspector-section simulation-composer")
|
|
515
|
+
composer.append(formSection("Machine input"))
|
|
516
|
+
const input = visualization.inputs.machine
|
|
517
|
+
if (input === null) {
|
|
518
|
+
composer.append(createElement("p", "section-empty", "This machine does not declare startup input."))
|
|
519
|
+
inspector.append(composer)
|
|
520
|
+
return
|
|
521
|
+
}
|
|
522
|
+
const form = renderInputForm(input, { name: "Machine input" })
|
|
523
|
+
activeInputForm = form
|
|
524
|
+
const start = createElement("button", "simulation-action", "Start simulation")
|
|
525
|
+
start.type = "submit"
|
|
526
|
+
start.disabled = simulationPending || visualization.source === null || !form.supported
|
|
527
|
+
form.element.addEventListener("submit", (event) => {
|
|
528
|
+
event.preventDefault()
|
|
529
|
+
const source = visualization.source
|
|
530
|
+
if (source === null) return
|
|
531
|
+
const result = form.read()
|
|
532
|
+
if (!result.ok) return
|
|
533
|
+
const request: SimulationRequest = {
|
|
534
|
+
_tag: "StartSimulation",
|
|
535
|
+
protocolVersion,
|
|
536
|
+
key: machineKey,
|
|
537
|
+
revision: visualization.revision,
|
|
538
|
+
source,
|
|
539
|
+
input: result.value as never
|
|
540
|
+
}
|
|
541
|
+
void runSimulation(request)
|
|
542
|
+
})
|
|
543
|
+
form.element.append(start)
|
|
544
|
+
composer.append(
|
|
545
|
+
form.element,
|
|
546
|
+
createElement(
|
|
547
|
+
"p",
|
|
548
|
+
"simulation-note",
|
|
549
|
+
"Initialization and synchronous callbacks run in isolation. Runtime activities and planned commands are not started."
|
|
550
|
+
)
|
|
551
|
+
)
|
|
552
|
+
inspector.append(composer)
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async function runSimulation(request: SimulationRequest): Promise<void> {
|
|
556
|
+
if (simulationPending) return
|
|
557
|
+
simulationPending = true
|
|
558
|
+
activeInputForm?.clearIssues()
|
|
559
|
+
activeInputForm?.setPending(true)
|
|
560
|
+
simulationFeedback.textContent = "Planning in an isolated worker…"
|
|
561
|
+
simulationFeedback.dataset.status = "pending"
|
|
562
|
+
updateSimulationUi()
|
|
563
|
+
try {
|
|
564
|
+
const result = await requestSimulation(request)
|
|
565
|
+
if (result._tag === "SimulationFailed") {
|
|
566
|
+
if (result.inputIssues.length > 0 && activeInputForm !== undefined) {
|
|
567
|
+
simulationFeedback.textContent = "Some input fields are invalid"
|
|
568
|
+
simulationFeedback.dataset.status = "error"
|
|
569
|
+
activeInputForm.setIssues(result.inputIssues)
|
|
570
|
+
return
|
|
571
|
+
}
|
|
572
|
+
selectedFrame = undefined
|
|
573
|
+
if (selectedEvent !== undefined) eventButtons.get(selectedEvent)?.classList.remove("is-selected")
|
|
574
|
+
selectedEvent = undefined
|
|
575
|
+
simulationFeedback.textContent = result.diagnostics[0]?.message ?? "Simulation failed"
|
|
576
|
+
simulationFeedback.dataset.status = "error"
|
|
577
|
+
renderSimulationFailure(result.diagnostics)
|
|
578
|
+
return
|
|
579
|
+
}
|
|
580
|
+
simulation = result
|
|
581
|
+
activeInputForm = undefined
|
|
582
|
+
if (selectedPath !== undefined) {
|
|
583
|
+
nodes.get(selectedPath)?.classList.remove("is-selected")
|
|
584
|
+
rows.get(selectedPath)?.setAttribute("aria-selected", "false")
|
|
585
|
+
}
|
|
586
|
+
if (selectedEvent !== undefined) eventButtons.get(selectedEvent)?.classList.remove("is-selected")
|
|
587
|
+
selectedPath = undefined
|
|
588
|
+
selectedEvent = undefined
|
|
589
|
+
selectedFrame = result.frame
|
|
590
|
+
clearRelations()
|
|
591
|
+
result.frame.microsteps.forEach((step) => {
|
|
592
|
+
step.transitions.forEach((transition) => {
|
|
593
|
+
relatedPaths.add(transition.source)
|
|
594
|
+
nodes.get(transition.source)?.classList.add("is-related-source")
|
|
595
|
+
})
|
|
596
|
+
step.entryPaths.forEach((path) => {
|
|
597
|
+
relatedPaths.add(path)
|
|
598
|
+
nodes.get(path)?.classList.add("is-related-target")
|
|
599
|
+
})
|
|
600
|
+
step.transitions.flatMap((transition) => transition.updates).forEach((path) => {
|
|
601
|
+
relatedPaths.add(path)
|
|
602
|
+
nodes.get(path)?.classList.add("is-related-update")
|
|
603
|
+
})
|
|
604
|
+
})
|
|
605
|
+
clearButton.disabled = false
|
|
606
|
+
const before = activeTopology(result.frame.before.activePaths)
|
|
607
|
+
const after = activeTopology(result.frame.after.activePaths)
|
|
608
|
+
simulationFeedback.textContent = result.frame.trigger._tag === "Initial"
|
|
609
|
+
? `Started in ${after}`
|
|
610
|
+
: result.frame.microsteps.length === 0
|
|
611
|
+
? `${eventName(result.frame.trigger.event)} was not accepted · remained in ${after}`
|
|
612
|
+
: before === after
|
|
613
|
+
? `${eventName(result.frame.trigger.event)} handled · remained in ${after}`
|
|
614
|
+
: `${before} → ${after} · ${result.frame.microsteps.length} microstep${
|
|
615
|
+
result.frame.microsteps.length === 1 ? "" : "s"
|
|
616
|
+
}`
|
|
617
|
+
simulationFeedback.dataset.status = "applied"
|
|
618
|
+
} catch (cause) {
|
|
619
|
+
selectedFrame = undefined
|
|
620
|
+
if (selectedEvent !== undefined) eventButtons.get(selectedEvent)?.classList.remove("is-selected")
|
|
621
|
+
selectedEvent = undefined
|
|
622
|
+
const failure: Diagnostic = {
|
|
623
|
+
severity: "error",
|
|
624
|
+
code: "simulation-request-failed",
|
|
625
|
+
message: cause instanceof Error ? cause.message : String(cause),
|
|
626
|
+
location: visualization.source === null
|
|
627
|
+
? null
|
|
628
|
+
: { file: visualization.source.file, line: null, column: null },
|
|
629
|
+
statePath: null
|
|
630
|
+
}
|
|
631
|
+
simulationFeedback.textContent = failure.message
|
|
632
|
+
simulationFeedback.dataset.status = "error"
|
|
633
|
+
renderSimulationFailure([failure])
|
|
634
|
+
} finally {
|
|
635
|
+
simulationPending = false
|
|
636
|
+
activeInputForm?.setPending(false)
|
|
637
|
+
updateSimulationUi()
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
314
641
|
const clearRelations = (): void => {
|
|
315
642
|
for (const path of relatedPaths) {
|
|
316
643
|
nodes.get(path)?.classList.remove("is-related-source", "is-related-target", "is-related-update")
|
|
@@ -327,6 +654,7 @@ export const renderVisualizer = (
|
|
|
327
654
|
clearRelations()
|
|
328
655
|
selectedPath = undefined
|
|
329
656
|
selectedEvent = undefined
|
|
657
|
+
selectedFrame = undefined
|
|
330
658
|
clearButton.disabled = true
|
|
331
659
|
renderEmptyInspector()
|
|
332
660
|
}
|
|
@@ -380,6 +708,7 @@ export const renderVisualizer = (
|
|
|
380
708
|
if (selectedEvent !== undefined) eventButtons.get(selectedEvent)?.classList.remove("is-selected")
|
|
381
709
|
selectedPath = path
|
|
382
710
|
selectedEvent = undefined
|
|
711
|
+
selectedFrame = undefined
|
|
383
712
|
nodes.get(path)?.classList.add("is-selected")
|
|
384
713
|
rows.get(path)?.setAttribute("aria-selected", "true")
|
|
385
714
|
markRelatedStates(inspection)
|
|
@@ -404,10 +733,32 @@ export const renderVisualizer = (
|
|
|
404
733
|
if (selectedEvent !== undefined) eventButtons.get(selectedEvent)?.classList.remove("is-selected")
|
|
405
734
|
selectedPath = undefined
|
|
406
735
|
selectedEvent = event
|
|
736
|
+
selectedFrame = undefined
|
|
407
737
|
eventButtons.get(event)?.classList.add("is-selected")
|
|
408
738
|
clearRelations()
|
|
409
739
|
markTransitions(inspection.transitions)
|
|
410
740
|
clearButton.disabled = false
|
|
741
|
+
const schema = eventSchemas.get(event)
|
|
742
|
+
if (simulation !== undefined && candidateEvents().includes(event) && schema !== undefined) {
|
|
743
|
+
const input = renderInputForm(schema, { name: event, fixed: { _tag: event }, omit: ["_tag"] })
|
|
744
|
+
if (!input.hasFields && input.supported) {
|
|
745
|
+
const result = input.read()
|
|
746
|
+
const source = visualization.source
|
|
747
|
+
if (result.ok && source !== null) {
|
|
748
|
+
void runSimulation({
|
|
749
|
+
_tag: "SendSimulationEvent",
|
|
750
|
+
protocolVersion,
|
|
751
|
+
key: machineKey,
|
|
752
|
+
revision: visualization.revision,
|
|
753
|
+
source,
|
|
754
|
+
step: simulation.step,
|
|
755
|
+
snapshot: simulation.snapshot,
|
|
756
|
+
event: result.value as never
|
|
757
|
+
})
|
|
758
|
+
return
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
411
762
|
renderEventInspection(inspection)
|
|
412
763
|
}
|
|
413
764
|
|
|
@@ -511,16 +862,8 @@ export const renderVisualizer = (
|
|
|
511
862
|
candidates.forEach((event) => {
|
|
512
863
|
const button = createElement("button", `event-button${event === selectedEvent ? " is-selected" : ""}`, event)
|
|
513
864
|
button.type = "button"
|
|
514
|
-
button.
|
|
515
|
-
|
|
516
|
-
const result = MachineSimulator.send(simulation, event)
|
|
517
|
-
if (result._tag === "Applied") simulation = { document: visualization, snapshot: result.session }
|
|
518
|
-
simulationFeedback.textContent = simulationResultMessage(result)
|
|
519
|
-
simulationFeedback.dataset.status = result._tag.toLowerCase()
|
|
520
|
-
updateSimulationUi()
|
|
521
|
-
}
|
|
522
|
-
selectEvent(event)
|
|
523
|
-
})
|
|
865
|
+
button.disabled = simulationPending
|
|
866
|
+
button.addEventListener("click", () => selectEvent(event))
|
|
524
867
|
eventButtons.set(event, button)
|
|
525
868
|
events.append(button)
|
|
526
869
|
})
|
|
@@ -536,34 +879,52 @@ export const renderVisualizer = (
|
|
|
536
879
|
const hasRuntimeState = simulation !== undefined || model.hasSnapshot
|
|
537
880
|
runtimeDot.classList.toggle("has-snapshot", hasRuntimeState)
|
|
538
881
|
runtimeText.textContent = simulation !== undefined
|
|
539
|
-
? `${active.size} active · step ${simulation.
|
|
882
|
+
? `${active.size} active · step ${simulation.step}`
|
|
540
883
|
: diagnostics.length > 0
|
|
541
884
|
? "Partial"
|
|
542
885
|
: model.hasSnapshot
|
|
543
886
|
? `${active.size} active`
|
|
544
887
|
: "Structure only"
|
|
545
888
|
simulationButton.textContent = simulation === undefined ? "Start simulation" : "Reset simulation"
|
|
889
|
+
simulationButton.disabled = simulationPending || model.roots.length === 0 || visualization.source === null
|
|
546
890
|
revealActiveButton.disabled = active.size === 0
|
|
547
891
|
events.hidden = !hasRuntimeState
|
|
548
|
-
simulationFeedback.hidden = simulation === undefined
|
|
892
|
+
simulationFeedback.hidden = simulation === undefined && !simulationPending && simulationFeedback.textContent === ""
|
|
549
893
|
renderEventButtons()
|
|
550
|
-
if (
|
|
894
|
+
if (selectedFrame !== undefined) {
|
|
895
|
+
renderSimulationTrace(selectedFrame)
|
|
896
|
+
} else if (selectedPath !== undefined) {
|
|
551
897
|
const inspection = model.inspectState(selectedPath)
|
|
552
898
|
if (inspection !== undefined) renderInspection(inspection)
|
|
553
|
-
} else if (selectedEvent !== undefined) {
|
|
899
|
+
} else if (selectedEvent !== undefined && activeInputForm === undefined) {
|
|
554
900
|
renderEventInspection(model.inspectEvent(selectedEvent))
|
|
555
901
|
}
|
|
556
902
|
}
|
|
557
903
|
|
|
558
904
|
simulationButton.addEventListener("click", () => {
|
|
559
905
|
if (simulation === undefined) {
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
906
|
+
selectedPath = undefined
|
|
907
|
+
selectedEvent = undefined
|
|
908
|
+
selectedFrame = undefined
|
|
909
|
+
clearRelations()
|
|
910
|
+
clearButton.disabled = false
|
|
911
|
+
if (visualization.inputs.machine === null && visualization.source !== null) {
|
|
912
|
+
void runSimulation({
|
|
913
|
+
_tag: "StartSimulation",
|
|
914
|
+
protocolVersion,
|
|
915
|
+
key: machineKey,
|
|
916
|
+
revision: visualization.revision,
|
|
917
|
+
source: visualization.source
|
|
918
|
+
})
|
|
919
|
+
} else {
|
|
920
|
+
renderStartSimulation()
|
|
921
|
+
}
|
|
563
922
|
} else {
|
|
564
923
|
simulation = undefined
|
|
924
|
+
selectedFrame = undefined
|
|
565
925
|
simulationFeedback.textContent = ""
|
|
566
926
|
delete simulationFeedback.dataset.status
|
|
927
|
+
clearSelection()
|
|
567
928
|
}
|
|
568
929
|
updateSimulationUi()
|
|
569
930
|
})
|
|
@@ -22,7 +22,7 @@ export const mountVisualizer = (root: HTMLElement, source: MachineResult): void
|
|
|
22
22
|
switch (source._tag) {
|
|
23
23
|
case "Ready":
|
|
24
24
|
case "Partial":
|
|
25
|
-
renderVisualizer(root, source.document, source.diagnostics)
|
|
25
|
+
renderVisualizer(root, source.key, source.document, source.diagnostics)
|
|
26
26
|
break
|
|
27
27
|
case "Failed":
|
|
28
28
|
renderError(root, source)
|