@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.
Files changed (82) hide show
  1. package/NOTICE +4 -0
  2. package/README.md +13 -13
  3. package/dist/DevToolsProtocol.d.ts +66 -630
  4. package/dist/DevToolsProtocol.d.ts.map +1 -1
  5. package/dist/DevToolsProtocol.js +0 -190
  6. package/dist/DevToolsProtocol.js.map +1 -1
  7. package/dist/MachineDocument.d.ts +38 -18
  8. package/dist/MachineDocument.d.ts.map +1 -1
  9. package/dist/MachineDocument.js +14 -12
  10. package/dist/MachineDocument.js.map +1 -1
  11. package/dist/MachineRegistry.d.ts +22 -2
  12. package/dist/MachineRegistry.d.ts.map +1 -1
  13. package/dist/MachineWalkthrough.d.ts +171 -0
  14. package/dist/MachineWalkthrough.d.ts.map +1 -0
  15. package/dist/MachineWalkthrough.js +98 -0
  16. package/dist/MachineWalkthrough.js.map +1 -0
  17. package/dist/ProjectInspector.d.ts +0 -2
  18. package/dist/ProjectInspector.d.ts.map +1 -1
  19. package/dist/ProjectInspector.js.map +1 -1
  20. package/dist/client/assets/index-BsorPSDS.css +1 -0
  21. package/dist/client/assets/index-eiptehRd.js +37 -0
  22. package/dist/client/index.html +4 -4
  23. package/dist/index.d.ts +2 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +2 -2
  26. package/dist/index.js.map +1 -1
  27. package/dist/internal/devServer.d.ts.map +1 -1
  28. package/dist/internal/devServer.js +4 -64
  29. package/dist/internal/devServer.js.map +1 -1
  30. package/dist/internal/evaluationWorker.d.ts.map +1 -1
  31. package/dist/internal/evaluationWorker.js +7 -263
  32. package/dist/internal/evaluationWorker.js.map +1 -1
  33. package/dist/internal/machineDocument.d.ts.map +1 -1
  34. package/dist/internal/machineDocument.js +3 -1
  35. package/dist/internal/machineDocument.js.map +1 -1
  36. package/dist/internal/machineWalkthrough.d.ts +26 -0
  37. package/dist/internal/machineWalkthrough.d.ts.map +1 -0
  38. package/dist/internal/machineWalkthrough.js +256 -0
  39. package/dist/internal/machineWalkthrough.js.map +1 -0
  40. package/dist/internal/projectInspector.d.ts.map +1 -1
  41. package/dist/internal/projectInspector.js +1 -11
  42. package/dist/internal/projectInspector.js.map +1 -1
  43. package/index.html +2 -2
  44. package/package.json +8 -7
  45. package/src/DevToolsProtocol.ts +0 -285
  46. package/src/MachineDocument.ts +21 -19
  47. package/src/MachineWalkthrough.ts +199 -0
  48. package/src/ProjectInspector.ts +0 -5
  49. package/src/index.ts +2 -2
  50. package/src/internal/browser/chart-layout-policy.ts +206 -0
  51. package/src/internal/browser/chart-layout.ts +686 -0
  52. package/src/internal/browser/chart-model.ts +244 -0
  53. package/src/internal/browser/chart-renderer.ts +774 -0
  54. package/src/internal/browser/example-machine.ts +15 -6
  55. package/src/internal/browser/input-form.ts +1 -418
  56. package/src/internal/browser/invoke-outcomes-example.ts +231 -0
  57. package/src/internal/browser/parallel-completion-example.ts +199 -0
  58. package/src/internal/browser/planner-example.ts +2 -0
  59. package/src/internal/browser/protocol-events-example.ts +200 -0
  60. package/src/internal/browser/styles.css +974 -342
  61. package/src/internal/browser/transition-semantics-example.ts +245 -0
  62. package/src/internal/browser/visualizer-analysis.ts +52 -0
  63. package/src/internal/browser/visualizer-app.ts +874 -706
  64. package/src/internal/browser/visualizer-model.ts +104 -0
  65. package/src/internal/devServer.ts +4 -82
  66. package/src/internal/evaluationWorker.ts +7 -371
  67. package/src/internal/machineDocument.ts +3 -1
  68. package/src/internal/machineWalkthrough.ts +355 -0
  69. package/src/internal/projectInspector.ts +3 -31
  70. package/dist/MachineSimulator.d.ts +0 -169
  71. package/dist/MachineSimulator.d.ts.map +0 -1
  72. package/dist/MachineSimulator.js +0 -98
  73. package/dist/MachineSimulator.js.map +0 -1
  74. package/dist/client/assets/index-B49Tjawc.js +0 -14
  75. package/dist/client/assets/index-BKH0cOG2.css +0 -1
  76. package/dist/internal/machineSimulator.d.ts +0 -5
  77. package/dist/internal/machineSimulator.d.ts.map +0 -1
  78. package/dist/internal/machineSimulator.js +0 -156
  79. package/dist/internal/machineSimulator.js.map +0 -1
  80. package/src/MachineSimulator.ts +0 -154
  81. package/src/internal/browser/simulation-client.ts +0 -20
  82. package/src/internal/machineSimulator.ts +0 -199
@@ -3,8 +3,14 @@ import { Schema } from "effect"
3
3
 
4
4
  // Shared by the live browser UI and its project-inspection fixture.
5
5
 
6
- class Application extends Schema.TaggedClass<Application>("Application")("Application", {}) {}
7
- class Workflow extends Schema.TaggedClass<Workflow>("Workflow")("Workflow", {}) {}
6
+ class Application extends Schema.TaggedClass<Application>("Application")("Application", {
7
+ workspace: Schema.String,
8
+ revision: Schema.Number
9
+ }) {}
10
+ class Workflow extends Schema.TaggedClass<Workflow>("Workflow")("Workflow", {
11
+ document: Schema.String,
12
+ unsavedChanges: Schema.Number
13
+ }) {}
8
14
  class Idle extends Schema.TaggedClass<Idle>("Idle")("Idle", {}) {}
9
15
  class Running extends Schema.TaggedClass<Running>("Running")("Running", {}) {}
10
16
  class Editing extends Schema.TaggedClass<Editing>("Editing")("Editing", {}) {}
@@ -59,11 +65,11 @@ const States = Machine.states({
59
65
 
60
66
  export const snapshot = {
61
67
  path: "application" as const,
62
- value: new Application({}),
68
+ value: new Application({ workspace: "effect-machine", revision: 7 }),
63
69
  states: {
64
70
  workflow: {
65
71
  path: "application.workflow" as const,
66
- value: new Workflow({}),
72
+ value: new Workflow({ document: "Machine.ts", unsavedChanges: 2 }),
67
73
  state: { path: "application.workflow.idle" as const, value: new Idle({}) }
68
74
  },
69
75
  connection: {
@@ -103,9 +109,12 @@ export const machine = Machine.make({
103
109
  target.decoded(
104
110
  new Running({}),
105
111
  (running) => running.editing.decoded(new Editing({}))
106
- ).update(owner.decoded(new Workflow({})))
112
+ ).update(owner.decoded(new Workflow({ document: "Machine.ts", unsavedChanges: 3 })))
107
113
  ),
108
- Refresh: (to) => to.local.update(({ owner }) => owner.decoded(new Workflow({})))
114
+ Refresh: (to) =>
115
+ to.local.update(({ owner }) =>
116
+ owner.decoded(new Workflow({ document: "Machine.ts", unsavedChanges: 0 }))
117
+ )
109
118
  }
110
119
  },
111
120
  running: {
@@ -1,4 +1,3 @@
1
- import type { InputIssue } from "../../DevToolsProtocol.js"
2
1
  import type { InputSchema } from "../../MachineDocument.js"
3
2
 
4
3
  type JsonPrimitive = string | number | boolean | null
@@ -217,9 +216,7 @@ const project = (
217
216
  case "null":
218
217
  return { _tag: "Literal", ...common, value: null }
219
218
  case "object": {
220
- if (!isRecord(resolved.properties)) {
221
- return { _tag: "Object", ...common, fields: [] }
222
- }
219
+ if (!isRecord(resolved.properties)) return { _tag: "Object", ...common, fields: [] }
223
220
  const required = new Set(
224
221
  Array.isArray(resolved.required)
225
222
  ? resolved.required.filter((item): item is string => typeof item === "string")
@@ -253,417 +250,3 @@ const project = (
253
250
  }
254
251
 
255
252
  export const projectInputSchema = (document: InputSchema): InputField => project(document.schema, document.definitions)
256
-
257
- export interface InputFormResult {
258
- readonly ok: boolean
259
- readonly value?: unknown
260
- }
261
-
262
- export interface InputForm {
263
- readonly element: HTMLFormElement
264
- readonly hasFields: boolean
265
- readonly supported: boolean
266
- readonly read: () => InputFormResult
267
- readonly clearIssues: () => void
268
- readonly setIssues: (issues: ReadonlyArray<InputIssue>) => void
269
- readonly setPending: (pending: boolean) => void
270
- }
271
-
272
- interface Control {
273
- readonly element: HTMLElement
274
- readonly interactive: boolean
275
- readonly supported: boolean
276
- readonly read: () => unknown
277
- }
278
-
279
- interface RenderContext {
280
- readonly issueTargets: Map<string, HTMLElement>
281
- }
282
-
283
- let nextControlId = 0
284
-
285
- const element = <Tag extends keyof HTMLElementTagNameMap>(
286
- tag: Tag,
287
- className?: string,
288
- text?: string
289
- ): HTMLElementTagNameMap[Tag] => {
290
- const node = document.createElement(tag)
291
- if (className !== undefined) node.className = className
292
- if (text !== undefined) node.textContent = text
293
- return node
294
- }
295
-
296
- const valueKey = (value: JsonPrimitive): string => JSON.stringify(value)
297
-
298
- const labelText = (field: InputField, fallback: string): string => field.title ?? fallback
299
-
300
- const description = (field: InputField): HTMLElement | undefined =>
301
- field.description === undefined ? undefined : element("p", "input-description", field.description)
302
-
303
- const fieldType = (field: InputField): string => {
304
- switch (field._tag) {
305
- case "String":
306
- return field.format ?? "string"
307
- case "Number":
308
- return field.integer ? "integer" : "number"
309
- case "Boolean":
310
- return "boolean"
311
- case "Enum":
312
- return "enum"
313
- case "Literal":
314
- return "literal"
315
- case "Object":
316
- return "object"
317
- case "Array":
318
- return `${fieldType(field.item)}[]`
319
- case "Union":
320
- return "union"
321
- case "Unsupported":
322
- return "unsupported"
323
- }
324
- }
325
-
326
- const fieldConstraints = (field: InputField): ReadonlyArray<string> => {
327
- switch (field._tag) {
328
- case "String":
329
- return [
330
- field.minLength === undefined ? undefined : `min ${field.minLength} characters`,
331
- field.maxLength === undefined ? undefined : `max ${field.maxLength} characters`,
332
- field.pattern === undefined ? undefined : `pattern ${field.pattern}`,
333
- field.defaultValue === undefined ? undefined : `default ${field.defaultValue}`
334
- ].filter((value): value is string => value !== undefined)
335
- case "Number":
336
- return [
337
- field.minimum === undefined ? undefined : `min ${field.minimum}`,
338
- field.maximum === undefined ? undefined : `max ${field.maximum}`,
339
- field.defaultValue === undefined ? undefined : `default ${field.defaultValue}`
340
- ].filter((value): value is string => value !== undefined)
341
- case "Enum":
342
- return [
343
- `${field.values.length} options`,
344
- field.defaultValue === undefined ? undefined : `default ${String(field.defaultValue)}`
345
- ].filter((value): value is string => value !== undefined)
346
- case "Literal":
347
- return [`fixed ${String(field.value)}`]
348
- case "Object":
349
- return [`${field.fields.length} fields`]
350
- case "Array":
351
- return [
352
- field.minItems === 0 ? undefined : `min ${field.minItems} items`,
353
- field.maxItems === undefined ? undefined : `max ${field.maxItems} items`
354
- ].filter((value): value is string => value !== undefined)
355
- case "Union":
356
- return [`${field.alternatives.length} variants`]
357
- case "Boolean":
358
- case "Unsupported":
359
- return []
360
- }
361
- }
362
-
363
- const renderControl = (
364
- field: InputField,
365
- name: string,
366
- context: RenderContext,
367
- path: ReadonlyArray<string | number>
368
- ): Control => {
369
- switch (field._tag) {
370
- case "String": {
371
- const input = element("input", "input-control")
372
- input.type = field.format === "date-time"
373
- ? "datetime-local"
374
- : field.format === "date"
375
- ? "date"
376
- : field.format === "email"
377
- ? "email"
378
- : field.format === "uri"
379
- ? "url"
380
- : "text"
381
- input.name = name
382
- input.value = field.defaultValue ?? ""
383
- if (field.minLength !== undefined) input.minLength = field.minLength
384
- if (field.maxLength !== undefined) input.maxLength = field.maxLength
385
- if (field.pattern !== undefined) input.pattern = field.pattern
386
- return { element: input, interactive: true, supported: true, read: () => input.value }
387
- }
388
- case "Number": {
389
- const input = element("input", "input-control")
390
- input.type = "number"
391
- input.name = name
392
- input.step = field.integer ? "1" : "any"
393
- if (field.defaultValue !== undefined) input.value = String(field.defaultValue)
394
- if (field.minimum !== undefined) input.min = String(field.minimum)
395
- if (field.maximum !== undefined) input.max = String(field.maximum)
396
- return {
397
- element: input,
398
- interactive: true,
399
- supported: true,
400
- read: () => input.value === "" ? undefined : Number(input.value)
401
- }
402
- }
403
- case "Boolean": {
404
- const wrapper = element("label", "boolean-control")
405
- const input = element("input")
406
- input.type = "checkbox"
407
- input.name = name
408
- input.checked = field.defaultValue ?? false
409
- wrapper.append(input, element("span", undefined, "Enabled"))
410
- return { element: wrapper, interactive: true, supported: true, read: () => input.checked }
411
- }
412
- case "Enum": {
413
- const select = element("select", "input-control")
414
- select.name = name
415
- field.values.forEach((value) => {
416
- const option = element("option", undefined, String(value))
417
- option.value = valueKey(value)
418
- option.selected = Object.is(value, field.defaultValue)
419
- select.append(option)
420
- })
421
- return {
422
- element: select,
423
- interactive: true,
424
- supported: true,
425
- read: () => JSON.parse(select.value) as JsonPrimitive
426
- }
427
- }
428
- case "Literal": {
429
- const output = element("output", "literal-control", String(field.value))
430
- return { element: output, interactive: false, supported: true, read: () => field.value }
431
- }
432
- case "Object": {
433
- const group = element("fieldset", "input-object")
434
- const legend = element("legend", undefined, labelText(field, name))
435
- group.append(legend)
436
- const controls: Array<{
437
- readonly key: string
438
- readonly included: HTMLInputElement | undefined
439
- readonly control: Control
440
- }> = []
441
- for (const property of field.fields) {
442
- const row = element("div", "input-field")
443
- const heading = element("div", "input-field-heading")
444
- const label = element("label", "input-label", labelText(property.field, property.key))
445
- const identity = element("div", "input-field-identity")
446
- identity.append(label, element("span", "input-field-type", fieldType(property.field)))
447
- const required = property.required ? element("span", "input-required", "required") : undefined
448
- let included: HTMLInputElement | undefined
449
- const propertyPath = [...path, property.key]
450
- const control = renderControl(property.field, `${name}.${property.key}`, context, propertyPath)
451
- const labelled = control.element instanceof HTMLInputElement || control.element instanceof HTMLSelectElement
452
- ? control.element
453
- : control.element.querySelector<HTMLInputElement | HTMLSelectElement>(":scope > input, :scope > select")
454
- if (labelled !== null) {
455
- const id = `machine-input-${nextControlId++}`
456
- labelled.id = id
457
- label.htmlFor = id
458
- }
459
- if (property.required) {
460
- if (control.element instanceof HTMLSelectElement) {
461
- control.element.required = true
462
- } else if (control.element instanceof HTMLInputElement) {
463
- control.element.required = property.field._tag === "Number" ||
464
- (property.field._tag === "String" &&
465
- (property.field.format !== undefined || (property.field.minLength ?? 0) > 0))
466
- }
467
- heading.append(identity, required!)
468
- } else {
469
- const optional = element("label", "input-optional")
470
- included = element("input")
471
- included.type = "checkbox"
472
- optional.append(included, element("span", undefined, "include"))
473
- heading.append(identity, optional)
474
- control.element.toggleAttribute("inert", true)
475
- control.element.classList.add("is-disabled")
476
- control.element.querySelectorAll<HTMLInputElement | HTMLSelectElement | HTMLButtonElement>(
477
- "input, select, button"
478
- ).forEach((item) => item.disabled = true)
479
- if (
480
- control.element instanceof HTMLInputElement ||
481
- control.element instanceof HTMLSelectElement ||
482
- control.element instanceof HTMLButtonElement
483
- ) control.element.disabled = true
484
- included.addEventListener("change", () => {
485
- control.element.toggleAttribute("inert", !included!.checked)
486
- control.element.classList.toggle("is-disabled", !included!.checked)
487
- control.element.querySelectorAll<HTMLInputElement | HTMLSelectElement | HTMLButtonElement>(
488
- "input, select, button"
489
- ).forEach((item) => item.disabled = !included!.checked)
490
- if (
491
- control.element instanceof HTMLInputElement ||
492
- control.element instanceof HTMLSelectElement ||
493
- control.element instanceof HTMLButtonElement
494
- ) control.element.disabled = !included!.checked
495
- })
496
- }
497
- const issues = element("div", "input-errors")
498
- issues.hidden = true
499
- context.issueTargets.set(JSON.stringify(propertyPath), issues)
500
- row.append(heading, control.element)
501
- const constraints = fieldConstraints(property.field)
502
- if (constraints.length > 0) {
503
- const metadata = element("div", "input-constraints")
504
- constraints.forEach((constraint) => metadata.append(element("span", undefined, constraint)))
505
- row.append(metadata)
506
- }
507
- const details = description(property.field)
508
- if (details !== undefined) row.append(details)
509
- row.append(issues)
510
- group.append(row)
511
- controls.push({ key: property.key, included, control })
512
- }
513
- return {
514
- element: group,
515
- interactive: controls.some(({ control }) => control.interactive),
516
- supported: controls.every(({ control }) => control.supported),
517
- read: () =>
518
- Object.fromEntries(
519
- controls
520
- .filter(({ included }) => included === undefined || included.checked)
521
- .map(({ key, control }) => [key, control.read()])
522
- .filter((entry) => entry[1] !== undefined)
523
- )
524
- }
525
- }
526
- case "Array": {
527
- const group = element("fieldset", "input-array")
528
- group.append(element("legend", undefined, labelText(field, name)))
529
- const items = element("div", "input-array-items")
530
- const controls: Array<{
531
- readonly row: HTMLElement
532
- readonly control: Control
533
- readonly remove: HTMLButtonElement
534
- }> = []
535
- const add = element("button", "input-array-add", "Add item")
536
- add.type = "button"
537
- const refreshActions = (): void => {
538
- add.disabled = field.maxItems !== undefined && controls.length >= field.maxItems
539
- controls.forEach(({ remove }) => remove.disabled = controls.length <= field.minItems)
540
- }
541
- const addItem = (): void => {
542
- if (field.maxItems !== undefined && controls.length >= field.maxItems) return
543
- const row = element("div", "input-array-item")
544
- const control = renderControl(field.item, `${name}.${controls.length}`, context, [...path, controls.length])
545
- const remove = element("button", "input-array-remove", "Remove")
546
- remove.type = "button"
547
- remove.addEventListener("click", () => {
548
- row.remove()
549
- const index = controls.findIndex((item) => item.row === row)
550
- if (index >= 0) controls.splice(index, 1)
551
- refreshActions()
552
- })
553
- row.append(control.element, remove)
554
- items.append(row)
555
- controls.push({ row, control, remove })
556
- refreshActions()
557
- }
558
- for (let index = 0; index < field.minItems; index++) addItem()
559
- add.addEventListener("click", addItem)
560
- group.append(items, add)
561
- return {
562
- element: group,
563
- interactive: true,
564
- supported: controls.every(({ control }) => control.supported) && field.item._tag !== "Unsupported",
565
- read: () => controls.map(({ control }) => control.read())
566
- }
567
- }
568
- case "Union": {
569
- const group = element("fieldset", "input-union")
570
- group.append(element("legend", undefined, labelText(field, name)))
571
- const select = element("select", "input-control")
572
- const body = element("div", "input-union-body")
573
- let selected = 0
574
- const controls = field.alternatives.map((alternative, index) => {
575
- const control = renderControl(alternative, `${name}.${index}`, context, path)
576
- const option = element("option", undefined, labelText(alternative, `Option ${index + 1}`))
577
- option.value = String(index)
578
- select.append(option)
579
- return control
580
- })
581
- const update = (): void => {
582
- selected = Number(select.value)
583
- body.replaceChildren(controls[selected]?.element ?? element("div"))
584
- }
585
- select.addEventListener("change", update)
586
- group.append(select, body)
587
- update()
588
- return {
589
- element: group,
590
- interactive: true,
591
- supported: controls.every(({ supported }) => supported),
592
- read: () => controls[selected]?.read()
593
- }
594
- }
595
- case "Unsupported": {
596
- const message = element("p", "input-unsupported", field.reason)
597
- return { element: message, interactive: false, supported: false, read: () => undefined }
598
- }
599
- }
600
- }
601
-
602
- export const renderInputForm = (
603
- schema: InputSchema,
604
- options: {
605
- readonly name: string
606
- readonly fixed?: Readonly<Record<string, JsonPrimitive>>
607
- readonly omit?: ReadonlyArray<string>
608
- }
609
- ): InputForm => {
610
- const form = element("form", "schema-form")
611
- const formIssues = element("div", "input-errors input-errors-form")
612
- formIssues.hidden = true
613
- const context: RenderContext = { issueTargets: new Map() }
614
- const projected = projectInputSchema(schema)
615
- const visible = projected._tag === "Object" && options.omit !== undefined
616
- ? { ...projected, fields: projected.fields.filter(({ key }) => !options.omit!.includes(key)) }
617
- : projected
618
- const control = renderControl(visible, options.name, context, [])
619
- form.append(formIssues, control.element)
620
- const clearIssues = (): void => {
621
- formIssues.replaceChildren()
622
- formIssues.hidden = true
623
- context.issueTargets.forEach((target) => {
624
- target.replaceChildren()
625
- target.hidden = true
626
- })
627
- }
628
- const setIssues = (issues: ReadonlyArray<InputIssue>): void => {
629
- clearIssues()
630
- const grouped = new Map<HTMLElement, Array<string>>()
631
- for (const issue of issues) {
632
- let target: HTMLElement = formIssues
633
- for (let length = issue.path.length; length > 0; length--) {
634
- const candidate = context.issueTargets.get(JSON.stringify(issue.path.slice(0, length)))
635
- if (candidate !== undefined) {
636
- target = candidate
637
- break
638
- }
639
- }
640
- const messages = grouped.get(target) ?? []
641
- if (!messages.includes(issue.message)) messages.push(issue.message)
642
- grouped.set(target, messages)
643
- }
644
- grouped.forEach((messages, target) => {
645
- messages.forEach((message) => target.append(element("div", undefined, message)))
646
- target.hidden = false
647
- })
648
- }
649
- return {
650
- element: form,
651
- hasFields: control.interactive,
652
- supported: control.supported,
653
- read: () => {
654
- clearIssues()
655
- if (!form.reportValidity() || !control.supported) return { ok: false }
656
- const value = control.read()
657
- return {
658
- ok: true,
659
- value: isRecord(value) && options.fixed !== undefined ? { ...value, ...options.fixed } : value
660
- }
661
- },
662
- clearIssues,
663
- setIssues,
664
- setPending: (pending) => {
665
- form.toggleAttribute("inert", pending)
666
- form.setAttribute("aria-busy", String(pending))
667
- }
668
- }
669
- }