@typeonce/effect-machine-devtools 0.24.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +4 -0
- package/README.md +11 -11
- package/dist/DevToolsProtocol.d.ts +66 -630
- package/dist/DevToolsProtocol.d.ts.map +1 -1
- package/dist/DevToolsProtocol.js +0 -190
- package/dist/DevToolsProtocol.js.map +1 -1
- package/dist/MachineDocument.d.ts +38 -18
- package/dist/MachineDocument.d.ts.map +1 -1
- package/dist/MachineDocument.js +14 -12
- package/dist/MachineDocument.js.map +1 -1
- package/dist/MachineRegistry.d.ts +22 -2
- package/dist/MachineRegistry.d.ts.map +1 -1
- package/dist/MachineWalkthrough.d.ts +171 -0
- package/dist/MachineWalkthrough.d.ts.map +1 -0
- package/dist/MachineWalkthrough.js +98 -0
- package/dist/MachineWalkthrough.js.map +1 -0
- package/dist/ProjectInspector.d.ts +0 -2
- package/dist/ProjectInspector.d.ts.map +1 -1
- package/dist/ProjectInspector.js.map +1 -1
- package/dist/client/assets/index-BTOAhezX.js +37 -0
- package/dist/client/assets/index-Cg7xzSkz.css +1 -0
- package/dist/client/index.html +4 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/devServer.d.ts.map +1 -1
- package/dist/internal/devServer.js +4 -64
- package/dist/internal/devServer.js.map +1 -1
- package/dist/internal/evaluationWorker.d.ts.map +1 -1
- package/dist/internal/evaluationWorker.js +7 -263
- package/dist/internal/evaluationWorker.js.map +1 -1
- package/dist/internal/machineDocument.d.ts.map +1 -1
- package/dist/internal/machineDocument.js +3 -1
- package/dist/internal/machineDocument.js.map +1 -1
- package/dist/internal/machineWalkthrough.d.ts +26 -0
- package/dist/internal/machineWalkthrough.d.ts.map +1 -0
- package/dist/internal/machineWalkthrough.js +256 -0
- package/dist/internal/machineWalkthrough.js.map +1 -0
- package/dist/internal/projectInspector.d.ts.map +1 -1
- package/dist/internal/projectInspector.js +1 -11
- package/dist/internal/projectInspector.js.map +1 -1
- package/index.html +2 -2
- package/package.json +4 -3
- package/src/DevToolsProtocol.ts +0 -285
- package/src/MachineDocument.ts +21 -19
- package/src/MachineWalkthrough.ts +199 -0
- package/src/ProjectInspector.ts +0 -5
- package/src/index.ts +2 -2
- package/src/internal/browser/chart-layout.ts +433 -0
- package/src/internal/browser/chart-model.ts +234 -0
- package/src/internal/browser/chart-renderer.ts +661 -0
- package/src/internal/browser/example-machine.ts +15 -6
- package/src/internal/browser/input-form.ts +1 -418
- package/src/internal/browser/invoke-outcomes-example.ts +231 -0
- package/src/internal/browser/parallel-completion-example.ts +199 -0
- package/src/internal/browser/planner-example.ts +2 -0
- package/src/internal/browser/protocol-events-example.ts +200 -0
- package/src/internal/browser/styles.css +795 -341
- package/src/internal/browser/transition-semantics-example.ts +236 -0
- package/src/internal/browser/visualizer-app.ts +781 -713
- package/src/internal/browser/visualizer-model.ts +104 -0
- package/src/internal/devServer.ts +4 -82
- package/src/internal/evaluationWorker.ts +7 -371
- package/src/internal/machineDocument.ts +3 -1
- package/src/internal/machineWalkthrough.ts +355 -0
- package/src/internal/projectInspector.ts +3 -31
- package/dist/MachineSimulator.d.ts +0 -169
- package/dist/MachineSimulator.d.ts.map +0 -1
- package/dist/MachineSimulator.js +0 -98
- package/dist/MachineSimulator.js.map +0 -1
- package/dist/client/assets/index-B49Tjawc.js +0 -14
- package/dist/client/assets/index-BKH0cOG2.css +0 -1
- package/dist/internal/machineSimulator.d.ts +0 -5
- package/dist/internal/machineSimulator.d.ts.map +0 -1
- package/dist/internal/machineSimulator.js +0 -156
- package/dist/internal/machineSimulator.js.map +0 -1
- package/src/MachineSimulator.ts +0 -154
- package/src/internal/browser/simulation-client.ts +0 -20
- package/src/internal/machineSimulator.ts +0 -199
|
@@ -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
|
-
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { Machine } from "@typeonce/effect-machine"
|
|
2
|
+
import { Effect, Schema, Stream } from "effect"
|
|
3
|
+
|
|
4
|
+
class ChildWorking extends Schema.TaggedClass<ChildWorking>("InvokeGalleryChildWorking")("Working", {
|
|
5
|
+
task: Schema.String
|
|
6
|
+
}) {}
|
|
7
|
+
class ChildDone extends Schema.TaggedClass<ChildDone>("InvokeGalleryChildDone")("Done", {
|
|
8
|
+
result: Schema.String
|
|
9
|
+
}) {}
|
|
10
|
+
class FinishChild extends Schema.TaggedClass<FinishChild>("InvokeGalleryFinishChild")("FinishChild", {}) {}
|
|
11
|
+
|
|
12
|
+
const ChildStates = Machine.states({
|
|
13
|
+
Working: ChildWorking,
|
|
14
|
+
Done: { schema: ChildDone, type: "final", output: Schema.String }
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
export const invokeGalleryChildMachine = Machine.make({
|
|
18
|
+
id: "invoke-gallery-child",
|
|
19
|
+
states: ChildStates.states,
|
|
20
|
+
events: Machine.events(FinishChild),
|
|
21
|
+
initial: (to) => to.Working().resolve(({ target }) => target.decoded(new ChildWorking({ task: "render-preview" })))
|
|
22
|
+
}).handle({
|
|
23
|
+
Working: {
|
|
24
|
+
on: {
|
|
25
|
+
FinishChild: (to) =>
|
|
26
|
+
to.full.Done().resolve(({ state, target }) =>
|
|
27
|
+
target.decoded(new ChildDone({ result: `${state.task}:complete` }))
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
Done: {
|
|
32
|
+
output: ({ state }) => state.result
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const InvokedChild = Machine.child("preview-worker", invokeGalleryChildMachine)
|
|
37
|
+
|
|
38
|
+
class Gallery extends Schema.TaggedClass<Gallery>("InvokeGallery")("Gallery", {
|
|
39
|
+
selectedDemo: Schema.NullOr(Schema.String)
|
|
40
|
+
}) {}
|
|
41
|
+
class Choose extends Schema.TaggedClass<Choose>("InvokeGalleryChoose")("Choose", {}) {}
|
|
42
|
+
class LoadingDocument extends Schema.TaggedClass<LoadingDocument>("InvokeGalleryLoadingDocument")(
|
|
43
|
+
"LoadingDocument",
|
|
44
|
+
{ request: Schema.String }
|
|
45
|
+
) {}
|
|
46
|
+
class StreamingUpdates extends Schema.TaggedClass<StreamingUpdates>("InvokeGalleryStreamingUpdates")(
|
|
47
|
+
"StreamingUpdates",
|
|
48
|
+
{ values: Schema.Array(Schema.Number) }
|
|
49
|
+
) {}
|
|
50
|
+
class WaitingForTimeout extends Schema.TaggedClass<WaitingForTimeout>("InvokeGalleryWaitingForTimeout")(
|
|
51
|
+
"WaitingForTimeout",
|
|
52
|
+
{ delay: Schema.Literal("2 seconds") }
|
|
53
|
+
) {}
|
|
54
|
+
class WatchingProcess extends Schema.TaggedClass<WatchingProcess>("InvokeGalleryWatchingProcess")(
|
|
55
|
+
"WatchingProcess",
|
|
56
|
+
{ revision: Schema.Number }
|
|
57
|
+
) {}
|
|
58
|
+
class RunningChild extends Schema.TaggedClass<RunningChild>("InvokeGalleryRunningChild")("RunningChild", {}) {}
|
|
59
|
+
class Completed extends Schema.TaggedClass<Completed>("InvokeGalleryCompleted")("Completed", {
|
|
60
|
+
source: Schema.String,
|
|
61
|
+
result: Schema.String
|
|
62
|
+
}) {}
|
|
63
|
+
class Failed extends Schema.TaggedClass<Failed>("InvokeGalleryFailed")("Failed", {
|
|
64
|
+
source: Schema.String,
|
|
65
|
+
message: Schema.String
|
|
66
|
+
}) {}
|
|
67
|
+
|
|
68
|
+
class RunEffect extends Schema.TaggedClass<RunEffect>("InvokeGalleryRunEffect")("RunEffect", {
|
|
69
|
+
request: Schema.String
|
|
70
|
+
}) {}
|
|
71
|
+
class RunStream extends Schema.TaggedClass<RunStream>("InvokeGalleryRunStream")("RunStream", {}) {}
|
|
72
|
+
class RunTimer extends Schema.TaggedClass<RunTimer>("InvokeGalleryRunTimer")("RunTimer", {}) {}
|
|
73
|
+
class RunProcess extends Schema.TaggedClass<RunProcess>("InvokeGalleryRunProcess")("RunProcess", {}) {}
|
|
74
|
+
class RunChild extends Schema.TaggedClass<RunChild>("InvokeGalleryRunChild")("RunChild", {}) {}
|
|
75
|
+
class Reset extends Schema.TaggedClass<Reset>("InvokeGalleryReset")("Reset", {}) {}
|
|
76
|
+
class StreamValue extends Schema.TaggedClass<StreamValue>("InvokeGalleryStreamValue")("StreamValue", {
|
|
77
|
+
value: Schema.Number
|
|
78
|
+
}) {}
|
|
79
|
+
|
|
80
|
+
const GalleryEvents = Machine.events(RunEffect, RunStream, RunTimer, RunProcess, RunChild, Reset)
|
|
81
|
+
const GalleryInternalEvents = Machine.internalEvents(StreamValue)
|
|
82
|
+
const processLogic = Machine.logic({
|
|
83
|
+
initial: "starting" as "starting" | "ready",
|
|
84
|
+
run: () => Effect.fail("process stopped")
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const GalleryStates = Machine.states({
|
|
88
|
+
Gallery: {
|
|
89
|
+
schema: Gallery,
|
|
90
|
+
initial: "Choose",
|
|
91
|
+
states: {
|
|
92
|
+
Choose,
|
|
93
|
+
LoadingDocument,
|
|
94
|
+
StreamingUpdates,
|
|
95
|
+
WaitingForTimeout,
|
|
96
|
+
WatchingProcess,
|
|
97
|
+
RunningChild
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
Completed,
|
|
101
|
+
Failed
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
export const invokeOutcomesMachine = Machine.make({
|
|
105
|
+
id: "invoke-outcomes",
|
|
106
|
+
states: GalleryStates.states,
|
|
107
|
+
events: GalleryEvents,
|
|
108
|
+
internalEvents: GalleryInternalEvents,
|
|
109
|
+
initial: (to) =>
|
|
110
|
+
to.Gallery.initial.resolve(({ target }) =>
|
|
111
|
+
target.decoded(
|
|
112
|
+
new Gallery({ selectedDemo: null }),
|
|
113
|
+
(gallery) => gallery.Choose.decoded(new Choose({}))
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
}).handle({
|
|
117
|
+
Gallery: {
|
|
118
|
+
initialize: ({ builder }) => builder.decoded(new Choose({})),
|
|
119
|
+
on: {
|
|
120
|
+
Reset: (to) => to.local.Choose().resolve(({ target }) => target.decoded(new Choose({})))
|
|
121
|
+
},
|
|
122
|
+
states: {
|
|
123
|
+
Choose: {
|
|
124
|
+
on: {
|
|
125
|
+
RunEffect: (to) =>
|
|
126
|
+
to.local.LoadingDocument().resolve(({ event, target }) =>
|
|
127
|
+
target.decoded(new LoadingDocument({ request: event.request }))
|
|
128
|
+
),
|
|
129
|
+
RunStream: (to) =>
|
|
130
|
+
to.local.StreamingUpdates().resolve(({ target }) => target.decoded(new StreamingUpdates({ values: [] }))),
|
|
131
|
+
RunTimer: (to) =>
|
|
132
|
+
to.local.WaitingForTimeout().resolve(({ target }) =>
|
|
133
|
+
target.decoded(new WaitingForTimeout({ delay: "2 seconds" }))
|
|
134
|
+
),
|
|
135
|
+
RunProcess: (to) =>
|
|
136
|
+
to.local.WatchingProcess().resolve(({ target }) => target.decoded(new WatchingProcess({ revision: 1 }))),
|
|
137
|
+
RunChild: (to) => to.local.RunningChild().resolve(({ target }) => target.decoded(new RunningChild({})))
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
LoadingDocument: {
|
|
141
|
+
invoke: (from) =>
|
|
142
|
+
from.effect(
|
|
143
|
+
"load-document",
|
|
144
|
+
({ state }) =>
|
|
145
|
+
state.request === "fail" ? Effect.fail("document unavailable") : Effect.succeed("document loaded")
|
|
146
|
+
).onDone((to) =>
|
|
147
|
+
to.full.Completed().resolve(({ output, target }) =>
|
|
148
|
+
target.decoded(new Completed({ source: "effect", result: output }))
|
|
149
|
+
)
|
|
150
|
+
).onFailure((to) =>
|
|
151
|
+
to.full.Failed().resolve(({ error, target }) =>
|
|
152
|
+
target.decoded(new Failed({ source: "effect", message: error }))
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
},
|
|
156
|
+
StreamingUpdates: {
|
|
157
|
+
invoke: (from) =>
|
|
158
|
+
from.stream(
|
|
159
|
+
"document-updates",
|
|
160
|
+
() => Stream.fromIterable([1, 2, 3]).pipe(Stream.concat(Stream.fail("stream disconnected")))
|
|
161
|
+
).onElement((to) =>
|
|
162
|
+
to.none.resolve(({ element }, enqueue) => {
|
|
163
|
+
enqueue.raise(GalleryInternalEvents.StreamValue({ value: element }))
|
|
164
|
+
})
|
|
165
|
+
).onDone((to) =>
|
|
166
|
+
to.full.Completed().resolve(({ state, target }) =>
|
|
167
|
+
target.decoded(new Completed({ source: "stream", result: state.values.join(", ") }))
|
|
168
|
+
)
|
|
169
|
+
).onFailure((to) =>
|
|
170
|
+
to.full.Failed().resolve(({ error, target }) =>
|
|
171
|
+
target.decoded(new Failed({ source: "stream", message: error }))
|
|
172
|
+
)
|
|
173
|
+
),
|
|
174
|
+
on: {
|
|
175
|
+
StreamValue: (to) =>
|
|
176
|
+
to.local.StreamingUpdates().resolve(({ event, state, target }) =>
|
|
177
|
+
target.decoded(new StreamingUpdates({ values: [...state.values, event.value] }))
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
WaitingForTimeout: {
|
|
182
|
+
invoke: (from) =>
|
|
183
|
+
from.timer("request-timeout", "2 seconds").onDone((to) =>
|
|
184
|
+
to.full.Completed().resolve(({ target }) =>
|
|
185
|
+
target.decoded(new Completed({ source: "timer", result: "timeout elapsed" }))
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
},
|
|
189
|
+
WatchingProcess: {
|
|
190
|
+
invoke: (from) =>
|
|
191
|
+
from.logic("status-worker", {
|
|
192
|
+
address: Machine.childAddress("status-worker"),
|
|
193
|
+
logic: processLogic
|
|
194
|
+
}).onFailure((to) =>
|
|
195
|
+
to.full.Failed().resolve(({ error, target }) =>
|
|
196
|
+
target.decoded(new Failed({ source: "process", message: String(error) }))
|
|
197
|
+
)
|
|
198
|
+
).onSnapshot((to) =>
|
|
199
|
+
to.branches({
|
|
200
|
+
ready: { title: "Worker reports ready", target: to.full.Completed() },
|
|
201
|
+
waiting: { title: "Worker is still starting", target: to.none }
|
|
202
|
+
}).resolve(({ select, snapshot }) =>
|
|
203
|
+
snapshot.state === "ready"
|
|
204
|
+
? select.ready.decoded(new Completed({ source: "process", result: "ready" }))
|
|
205
|
+
: select.waiting()
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
},
|
|
209
|
+
RunningChild: {
|
|
210
|
+
invoke: (from) =>
|
|
211
|
+
from.child(InvokedChild).onDone((to) =>
|
|
212
|
+
to.full.Completed().resolve(({ output, target }) =>
|
|
213
|
+
target.decoded(new Completed({ source: "child machine", result: output }))
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
Completed: {
|
|
220
|
+
on: {
|
|
221
|
+
Reset: (to) =>
|
|
222
|
+
to.full.Gallery.initial.resolve(({ target }) => target.decoded(new Gallery({ selectedDemo: null })))
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
Failed: {
|
|
226
|
+
on: {
|
|
227
|
+
Reset: (to) =>
|
|
228
|
+
to.full.Gallery.initial.resolve(({ target }) => target.decoded(new Gallery({ selectedDemo: null })))
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
})
|