@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.
Files changed (80) hide show
  1. package/NOTICE +4 -0
  2. package/README.md +11 -11
  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-BTOAhezX.js +37 -0
  21. package/dist/client/assets/index-Cg7xzSkz.css +1 -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 +4 -3
  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.ts +433 -0
  51. package/src/internal/browser/chart-model.ts +234 -0
  52. package/src/internal/browser/chart-renderer.ts +661 -0
  53. package/src/internal/browser/example-machine.ts +15 -6
  54. package/src/internal/browser/input-form.ts +1 -418
  55. package/src/internal/browser/invoke-outcomes-example.ts +231 -0
  56. package/src/internal/browser/parallel-completion-example.ts +199 -0
  57. package/src/internal/browser/planner-example.ts +2 -0
  58. package/src/internal/browser/protocol-events-example.ts +200 -0
  59. package/src/internal/browser/styles.css +795 -341
  60. package/src/internal/browser/transition-semantics-example.ts +236 -0
  61. package/src/internal/browser/visualizer-app.ts +781 -713
  62. package/src/internal/browser/visualizer-model.ts +104 -0
  63. package/src/internal/devServer.ts +4 -82
  64. package/src/internal/evaluationWorker.ts +7 -371
  65. package/src/internal/machineDocument.ts +3 -1
  66. package/src/internal/machineWalkthrough.ts +355 -0
  67. package/src/internal/projectInspector.ts +3 -31
  68. package/dist/MachineSimulator.d.ts +0 -169
  69. package/dist/MachineSimulator.d.ts.map +0 -1
  70. package/dist/MachineSimulator.js +0 -98
  71. package/dist/MachineSimulator.js.map +0 -1
  72. package/dist/client/assets/index-B49Tjawc.js +0 -14
  73. package/dist/client/assets/index-BKH0cOG2.css +0 -1
  74. package/dist/internal/machineSimulator.d.ts +0 -5
  75. package/dist/internal/machineSimulator.d.ts.map +0 -1
  76. package/dist/internal/machineSimulator.js +0 -156
  77. package/dist/internal/machineSimulator.js.map +0 -1
  78. package/src/MachineSimulator.ts +0 -154
  79. package/src/internal/browser/simulation-client.ts +0 -20
  80. 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
+ })