@typeonce/effect-machine-devtools 0.23.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 (91) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +3 -0
  3. package/README.md +81 -0
  4. package/dist/DevServer.d.ts +42 -0
  5. package/dist/DevServer.d.ts.map +1 -0
  6. package/dist/DevServer.js +28 -0
  7. package/dist/DevServer.js.map +1 -0
  8. package/dist/DevToolsProtocol.d.ts +954 -0
  9. package/dist/DevToolsProtocol.d.ts.map +1 -0
  10. package/dist/DevToolsProtocol.js +96 -0
  11. package/dist/DevToolsProtocol.js.map +1 -0
  12. package/dist/MachineDocument.d.ts +396 -0
  13. package/dist/MachineDocument.d.ts.map +1 -0
  14. package/dist/MachineDocument.js +172 -0
  15. package/dist/MachineDocument.js.map +1 -0
  16. package/dist/MachineRegistry.d.ts +349 -0
  17. package/dist/MachineRegistry.d.ts.map +1 -0
  18. package/dist/MachineRegistry.js +46 -0
  19. package/dist/MachineRegistry.js.map +1 -0
  20. package/dist/MachineSimulator.d.ts +169 -0
  21. package/dist/MachineSimulator.d.ts.map +1 -0
  22. package/dist/MachineSimulator.js +98 -0
  23. package/dist/MachineSimulator.js.map +1 -0
  24. package/dist/ProjectInspector.d.ts +116 -0
  25. package/dist/ProjectInspector.d.ts.map +1 -0
  26. package/dist/ProjectInspector.js +82 -0
  27. package/dist/ProjectInspector.js.map +1 -0
  28. package/dist/bin.d.ts +3 -0
  29. package/dist/bin.d.ts.map +1 -0
  30. package/dist/bin.js +23 -0
  31. package/dist/bin.js.map +1 -0
  32. package/dist/client/assets/index-BGOhE3Ng.css +1 -0
  33. package/dist/client/assets/index-DiqGhsGR.js +14 -0
  34. package/dist/client/index.html +14 -0
  35. package/dist/index.d.ts +18 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +18 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/internal/devServer.d.ts +9 -0
  40. package/dist/internal/devServer.d.ts.map +1 -0
  41. package/dist/internal/devServer.js +119 -0
  42. package/dist/internal/devServer.js.map +1 -0
  43. package/dist/internal/evaluationWorker.d.ts +3 -0
  44. package/dist/internal/evaluationWorker.d.ts.map +1 -0
  45. package/dist/internal/evaluationWorker.js +69 -0
  46. package/dist/internal/evaluationWorker.js.map +1 -0
  47. package/dist/internal/evaluationWorkerBootstrap.d.ts +2 -0
  48. package/dist/internal/evaluationWorkerBootstrap.d.ts.map +1 -0
  49. package/dist/internal/evaluationWorkerBootstrap.js +21 -0
  50. package/dist/internal/evaluationWorkerBootstrap.js.map +1 -0
  51. package/dist/internal/machineDocument.d.ts +4 -0
  52. package/dist/internal/machineDocument.d.ts.map +1 -0
  53. package/dist/internal/machineDocument.js +122 -0
  54. package/dist/internal/machineDocument.js.map +1 -0
  55. package/dist/internal/machineRegistry.d.ts +12 -0
  56. package/dist/internal/machineRegistry.d.ts.map +1 -0
  57. package/dist/internal/machineRegistry.js +97 -0
  58. package/dist/internal/machineRegistry.js.map +1 -0
  59. package/dist/internal/machineSimulator.d.ts +5 -0
  60. package/dist/internal/machineSimulator.d.ts.map +1 -0
  61. package/dist/internal/machineSimulator.js +156 -0
  62. package/dist/internal/machineSimulator.js.map +1 -0
  63. package/dist/internal/projectInspector.d.ts +11 -0
  64. package/dist/internal/projectInspector.d.ts.map +1 -0
  65. package/dist/internal/projectInspector.js +150 -0
  66. package/dist/internal/projectInspector.js.map +1 -0
  67. package/index.html +13 -0
  68. package/package.json +69 -0
  69. package/src/DevServer.ts +45 -0
  70. package/src/DevToolsProtocol.ts +146 -0
  71. package/src/MachineDocument.ts +264 -0
  72. package/src/MachineRegistry.ts +69 -0
  73. package/src/MachineSimulator.ts +154 -0
  74. package/src/ProjectInspector.ts +140 -0
  75. package/src/bin.ts +56 -0
  76. package/src/index.ts +20 -0
  77. package/src/internal/browser/example-machine.ts +134 -0
  78. package/src/internal/browser/machine-index.ts +81 -0
  79. package/src/internal/browser/main.ts +42 -0
  80. package/src/internal/browser/styles.css +817 -0
  81. package/src/internal/browser/text-tree.ts +214 -0
  82. package/src/internal/browser/visualizer-app.ts +659 -0
  83. package/src/internal/browser/visualizer-model.ts +173 -0
  84. package/src/internal/browser/visualizer.ts +31 -0
  85. package/src/internal/devServer.ts +171 -0
  86. package/src/internal/evaluationWorker.ts +138 -0
  87. package/src/internal/evaluationWorkerBootstrap.js +21 -0
  88. package/src/internal/machineDocument.ts +138 -0
  89. package/src/internal/machineRegistry.ts +128 -0
  90. package/src/internal/machineSimulator.ts +199 -0
  91. package/src/internal/projectInspector.ts +226 -0
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Side-effect-free, best-effort simulation over a machine document.
3
+ *
4
+ * @since 0.23.0
5
+ */
6
+ import * as Schema from "effect/Schema"
7
+ import * as internal from "./internal/machineSimulator.js"
8
+ import type * as MachineDocument from "./MachineDocument.js"
9
+
10
+ /**
11
+ * Serializable state of a simulation session.
12
+ *
13
+ * @category schemas
14
+ * @since 0.23.0
15
+ */
16
+ export const Snapshot = Schema.Struct({
17
+ step: Schema.Natural,
18
+ activePaths: Schema.Array(Schema.String),
19
+ candidateEvents: Schema.Array(Schema.String)
20
+ })
21
+
22
+ /**
23
+ * @category models
24
+ * @since 0.23.0
25
+ */
26
+ export type Snapshot = Schema.Schema.Type<typeof Snapshot>
27
+
28
+ /**
29
+ * Opaque simulation state paired with its source document.
30
+ *
31
+ * @category models
32
+ * @since 0.23.0
33
+ */
34
+ export interface Session {
35
+ readonly document: MachineDocument.MachineDocument
36
+ readonly snapshot: Snapshot
37
+ }
38
+
39
+ /**
40
+ * Runtime behavior deliberately skipped by a best-effort step.
41
+ *
42
+ * @category schemas
43
+ * @since 0.23.0
44
+ */
45
+ export const Note = Schema.Literals([
46
+ "runtime-effects-skipped",
47
+ "state-updates-skipped",
48
+ "reentry-lifecycles-skipped",
49
+ "automatic-transitions-skipped"
50
+ ])
51
+
52
+ /**
53
+ * @category models
54
+ * @since 0.23.0
55
+ */
56
+ export type Note = Schema.Schema.Type<typeof Note>
57
+
58
+ const ResultFields = {
59
+ event: Schema.String,
60
+ transitionIds: Schema.Array(Schema.String),
61
+ session: Snapshot
62
+ }
63
+
64
+ /**
65
+ * A topologically deterministic step.
66
+ *
67
+ * @category schemas
68
+ * @since 0.23.0
69
+ */
70
+ export const Applied = Schema.Struct({
71
+ ...ResultFields,
72
+ _tag: Schema.tag("Applied"),
73
+ notes: Schema.Array(Note)
74
+ })
75
+
76
+ /**
77
+ * @category models
78
+ * @since 0.23.0
79
+ */
80
+ export type Applied = Schema.Schema.Type<typeof Applied>
81
+
82
+ /**
83
+ * An event with no registration in the current active configuration.
84
+ *
85
+ * @category schemas
86
+ * @since 0.23.0
87
+ */
88
+ export const Blocked = Schema.Struct({
89
+ ...ResultFields,
90
+ _tag: Schema.tag("Blocked"),
91
+ reason: Schema.Literal("event-not-enabled")
92
+ })
93
+
94
+ /**
95
+ * @category models
96
+ * @since 0.23.0
97
+ */
98
+ export type Blocked = Schema.Schema.Type<typeof Blocked>
99
+
100
+ /**
101
+ * A step whose topology depends on runtime behavior the document cannot safely
102
+ * evaluate.
103
+ *
104
+ * @category schemas
105
+ * @since 0.23.0
106
+ */
107
+ export const Indeterminate = Schema.Struct({
108
+ ...ResultFields,
109
+ _tag: Schema.tag("Indeterminate"),
110
+ reason: Schema.Literals([
111
+ "multiple-transitions",
112
+ "declinable-transition",
113
+ "conditional-branches",
114
+ "history-target",
115
+ "choice-target",
116
+ "missing-target"
117
+ ])
118
+ })
119
+
120
+ /**
121
+ * @category models
122
+ * @since 0.23.0
123
+ */
124
+ export type Indeterminate = Schema.Schema.Type<typeof Indeterminate>
125
+
126
+ /**
127
+ * @category schemas
128
+ * @since 0.23.0
129
+ */
130
+ export const StepResult = Schema.Union([Applied, Blocked, Indeterminate])
131
+
132
+ /**
133
+ * @category models
134
+ * @since 0.23.0
135
+ */
136
+ export type StepResult = Schema.Schema.Type<typeof StepResult>
137
+
138
+ /**
139
+ * Starts from the captured snapshot when present, otherwise from the static
140
+ * initial topology.
141
+ *
142
+ * @category constructors
143
+ * @since 0.23.0
144
+ */
145
+ export const start: (document: MachineDocument.MachineDocument) => Session = internal.start
146
+
147
+ /**
148
+ * Sends an event without running user code. `Applied` means the active topology
149
+ * is statically known; its notes list runtime behavior that was skipped.
150
+ *
151
+ * @category combinators
152
+ * @since 0.23.0
153
+ */
154
+ export const send: (session: Session, event: string) => StepResult = internal.send
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Discovery and isolated evaluation of Effect Machine definitions in a project.
3
+ *
4
+ * @since 0.23.0
5
+ */
6
+ import * as Context from "effect/Context"
7
+ import * as Effect from "effect/Effect"
8
+ import * as Schema from "effect/Schema"
9
+ import type * as DevToolsProtocol from "./DevToolsProtocol.js"
10
+ import * as internal from "./internal/projectInspector.js"
11
+
12
+ /**
13
+ * A source module containing at least one `.handle(...)` machine definition.
14
+ *
15
+ * @category schemas
16
+ * @since 0.23.0
17
+ */
18
+ export const Candidate = Schema.Struct({
19
+ file: Schema.String,
20
+ exportNames: Schema.Array(Schema.String)
21
+ })
22
+
23
+ /**
24
+ * @category models
25
+ * @since 0.23.0
26
+ */
27
+ export type Candidate = Schema.Schema.Type<typeof Candidate>
28
+
29
+ /**
30
+ * Options shared by discovery and inspection.
31
+ *
32
+ * Include and exclude entries are glob patterns relative to `root`.
33
+ *
34
+ * @category models
35
+ * @since 0.23.0
36
+ */
37
+ export interface InspectOptions {
38
+ readonly root: string
39
+ readonly include?: string | undefined
40
+ readonly exclude?: ReadonlyArray<string> | undefined
41
+ readonly revision?: number | undefined
42
+ /**
43
+ * Last-known machine candidates. A syntactically incomplete source file is
44
+ * evaluated with this identity so the registry can retain its document.
45
+ *
46
+ * @internal
47
+ */
48
+ readonly retainedCandidates?: ReadonlyArray<Candidate> | undefined
49
+ }
50
+
51
+ /**
52
+ * A typed failure while reading or parsing the project source tree.
53
+ *
54
+ * @category errors
55
+ * @since 0.23.0
56
+ */
57
+ export class DiscoveryError extends Schema.Error<DiscoveryError>(
58
+ "@typeonce/effect-machine-devtools/ProjectInspector/DiscoveryError"
59
+ )({
60
+ _tag: Schema.tag("DiscoveryError"),
61
+ message: Schema.String,
62
+ cause: Schema.optional(Schema.Defect())
63
+ }) {}
64
+
65
+ /**
66
+ * A typed failure in the isolated evaluator itself. Failures in individual
67
+ * project modules are returned as `DevToolsProtocol.Failed` values instead.
68
+ *
69
+ * @category errors
70
+ * @since 0.23.0
71
+ */
72
+ export class EvaluationError extends Schema.Error<EvaluationError>(
73
+ "@typeonce/effect-machine-devtools/ProjectInspector/EvaluationError"
74
+ )({
75
+ _tag: Schema.tag("EvaluationError"),
76
+ message: Schema.String,
77
+ cause: Schema.optional(Schema.Defect())
78
+ }) {}
79
+
80
+ /**
81
+ * Service for locating and evaluating machine modules.
82
+ *
83
+ * @category services
84
+ * @since 0.23.0
85
+ */
86
+ export class ProjectInspector extends Context.Service<ProjectInspector, {
87
+ readonly discover: (options: InspectOptions) => Effect.Effect<ReadonlyArray<Candidate>, DiscoveryError>
88
+ readonly evaluate: (
89
+ candidates: ReadonlyArray<Candidate>,
90
+ options: InspectOptions
91
+ ) => Effect.Effect<ReadonlyArray<DevToolsProtocol.MachineResult>, EvaluationError>
92
+ readonly inspect: (
93
+ options: InspectOptions
94
+ ) => Effect.Effect<ReadonlyArray<DevToolsProtocol.MachineResult>, DiscoveryError | EvaluationError>
95
+ }>()("@typeonce/effect-machine-devtools/ProjectInspector") {}
96
+
97
+ /**
98
+ * Discovers machine candidates with the configured inspector.
99
+ *
100
+ * @category combinators
101
+ * @since 0.23.0
102
+ */
103
+ export const discover = (options: InspectOptions): Effect.Effect<
104
+ ReadonlyArray<Candidate>,
105
+ DiscoveryError,
106
+ ProjectInspector
107
+ > => Effect.flatMap(ProjectInspector, (inspector) => inspector.discover(options))
108
+
109
+ /**
110
+ * Evaluates machine candidates with the configured inspector.
111
+ *
112
+ * @category combinators
113
+ * @since 0.23.0
114
+ */
115
+ export const evaluate = (
116
+ candidates: ReadonlyArray<Candidate>,
117
+ options: InspectOptions
118
+ ): Effect.Effect<ReadonlyArray<DevToolsProtocol.MachineResult>, EvaluationError, ProjectInspector> =>
119
+ Effect.flatMap(ProjectInspector, (inspector) => inspector.evaluate(candidates, options))
120
+
121
+ /**
122
+ * Discovers and evaluates every machine candidate in a project.
123
+ *
124
+ * @category combinators
125
+ * @since 0.23.0
126
+ */
127
+ export const inspect = (options: InspectOptions): Effect.Effect<
128
+ ReadonlyArray<DevToolsProtocol.MachineResult>,
129
+ DiscoveryError | EvaluationError,
130
+ ProjectInspector
131
+ > => Effect.flatMap(ProjectInspector, (inspector) => inspector.inspect(options))
132
+
133
+ /**
134
+ * Node-backed inspector layer. Each inspection evaluates candidates inside a
135
+ * fresh worker so a broken project module cannot corrupt the long-lived host.
136
+ *
137
+ * @category layers
138
+ * @since 0.23.0
139
+ */
140
+ export const layer = internal.layer({ ProjectInspector, DiscoveryError, EvaluationError })
package/src/bin.ts ADDED
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ import * as NodeRuntime from "@effect/platform-node/NodeRuntime"
3
+ import * as NodeServices from "@effect/platform-node/NodeServices"
4
+ import * as Effect from "effect/Effect"
5
+ import * as Layer from "effect/Layer"
6
+ import * as Command from "effect/unstable/cli/Command"
7
+ import * as Flag from "effect/unstable/cli/Flag"
8
+ import PackageJson from "../package.json" with { type: "json" }
9
+ import * as DevServer from "./DevServer.js"
10
+ import * as MachineRegistry from "./MachineRegistry.js"
11
+ import * as ProjectInspector from "./ProjectInspector.js"
12
+
13
+ const root = Flag.directory("root", { mustExist: true }).pipe(
14
+ Flag.withDescription("Project root to inspect"),
15
+ Flag.withDefault(process.cwd())
16
+ )
17
+
18
+ const include = Flag.string("include").pipe(
19
+ Flag.withDescription("Machine source glob relative to the project root"),
20
+ Flag.withDefault("**/src/**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}")
21
+ )
22
+
23
+ const host = Flag.string("host").pipe(
24
+ Flag.withDescription("Host for the local visualizer"),
25
+ Flag.withDefault("127.0.0.1")
26
+ )
27
+
28
+ const port = Flag.integer("port").pipe(
29
+ Flag.withDescription("Port for the local visualizer"),
30
+ Flag.withDefault(5173)
31
+ )
32
+
33
+ const open = Flag.boolean("open").pipe(
34
+ Flag.withDescription("Open the visualizer in the default browser"),
35
+ Flag.withDefault(false)
36
+ )
37
+
38
+ const watchPolling = Flag.boolean("watch-polling").pipe(
39
+ Flag.withDescription("Use polling instead of native file-system events"),
40
+ Flag.withDefault(false)
41
+ )
42
+
43
+ const cli = Command.make("effect-machine", { root, include, host, port, open, watchPolling }).pipe(
44
+ Command.withDescription("Inspect Effect Machine definitions in a live local visualizer"),
45
+ Command.withHandler(({ host, include, open, port, root, watchPolling }) => {
46
+ const RegistryLayer = MachineRegistry.layer({ root, include }).pipe(
47
+ Layer.provideMerge(ProjectInspector.layer)
48
+ )
49
+ return DevServer.run({ root, host, port, open, watchPolling }).pipe(Effect.provide(RegistryLayer))
50
+ })
51
+ )
52
+
53
+ Command.run(cli, { version: PackageJson.version }).pipe(
54
+ Effect.provide(NodeServices.layer),
55
+ NodeRuntime.runMain
56
+ )
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Local development tools for Effect Machine.
3
+ *
4
+ * @since 0.23.0
5
+ */
6
+
7
+ /**
8
+ * @since 0.23.0
9
+ */
10
+ export * as DevToolsProtocol from "./DevToolsProtocol.js"
11
+
12
+ /**
13
+ * @since 0.23.0
14
+ */
15
+ export * as MachineDocument from "./MachineDocument.js"
16
+
17
+ /**
18
+ * @since 0.23.0
19
+ */
20
+ export * as MachineSimulator from "./MachineSimulator.js"
@@ -0,0 +1,134 @@
1
+ import { Machine } from "@typeonce/effect-machine"
2
+ import { Schema } from "effect"
3
+
4
+ // Shared by the live browser UI and its project-inspection fixture.
5
+
6
+ class Application extends Schema.TaggedClass<Application>("Application")("Application", {}) {}
7
+ class Workflow extends Schema.TaggedClass<Workflow>("Workflow")("Workflow", {}) {}
8
+ class Idle extends Schema.TaggedClass<Idle>("Idle")("Idle", {}) {}
9
+ class Running extends Schema.TaggedClass<Running>("Running")("Running", {}) {}
10
+ class Editing extends Schema.TaggedClass<Editing>("Editing")("Editing", {}) {}
11
+ class Complete extends Schema.TaggedClass<Complete>("Complete")("Complete", {}) {}
12
+ class Connection extends Schema.TaggedClass<Connection>("Connection")("Connection", {}) {}
13
+ class Online extends Schema.TaggedClass<Online>("Online")("Online", {}) {}
14
+ class Offline extends Schema.TaggedClass<Offline>("Offline")("Offline", {}) {}
15
+ class Disabled extends Schema.TaggedClass<Disabled>("Disabled")("Disabled", {}) {}
16
+ class Start extends Schema.TaggedClass<Start>("Start")("Start", {}) {}
17
+ class Finish extends Schema.TaggedClass<Finish>("Finish")("Finish", {}) {}
18
+ class Disconnect extends Schema.TaggedClass<Disconnect>("Disconnect")("Disconnect", {}) {}
19
+ class Refresh extends Schema.TaggedClass<Refresh>("Refresh")("Refresh", {}) {}
20
+
21
+ const States = Machine.states({
22
+ application: {
23
+ schema: Application,
24
+ type: "parallel",
25
+ states: {
26
+ workflow: {
27
+ schema: Workflow,
28
+ initial: "idle",
29
+ states: {
30
+ idle: Idle,
31
+ running: {
32
+ schema: Running,
33
+ initial: "editing",
34
+ states: {
35
+ editing: Editing,
36
+ complete: {
37
+ schema: Complete,
38
+ type: "final"
39
+ }
40
+ }
41
+ },
42
+ recent: {
43
+ type: "history"
44
+ }
45
+ }
46
+ },
47
+ connection: {
48
+ schema: Connection,
49
+ initial: "online",
50
+ states: {
51
+ online: Online,
52
+ offline: Offline
53
+ }
54
+ }
55
+ }
56
+ },
57
+ disabled: Disabled
58
+ })
59
+
60
+ export const snapshot = {
61
+ path: "application" as const,
62
+ value: new Application({}),
63
+ states: {
64
+ workflow: {
65
+ path: "application.workflow" as const,
66
+ value: new Workflow({}),
67
+ state: { path: "application.workflow.idle" as const, value: new Idle({}) }
68
+ },
69
+ connection: {
70
+ path: "application.connection" as const,
71
+ value: new Connection({}),
72
+ state: { path: "application.connection.online" as const, value: new Online({}) }
73
+ }
74
+ }
75
+ }
76
+
77
+ const initialWorkflow = (): Machine.Machine.CompleteSnapshotContaining<
78
+ typeof States.states,
79
+ "application.workflow"
80
+ > => snapshot
81
+
82
+ export const machine = Machine.make({
83
+ id: "inspection-example",
84
+ states: States.states,
85
+ events: Machine.events(Start, Finish, Disconnect, Refresh),
86
+ initial: (to) => to.application.initial.resolve(() => snapshot)
87
+ }).handle({
88
+ application: {
89
+ states: {
90
+ workflow: {
91
+ history: {
92
+ recent: {
93
+ default: initialWorkflow
94
+ }
95
+ },
96
+ states: {
97
+ idle: {
98
+ on: {
99
+ Start: (to) =>
100
+ to.local.running()
101
+ .updating(to.branch.application.workflow)
102
+ .resolve(({ owner, target }) =>
103
+ target.decoded(
104
+ new Running({}),
105
+ (running) => running.editing.decoded(new Editing({}))
106
+ ).update(owner.decoded(new Workflow({})))
107
+ ),
108
+ Refresh: (to) => to.local.update(({ owner }) => owner.decoded(new Workflow({})))
109
+ }
110
+ },
111
+ running: {
112
+ initialize: ({ builder }) => builder.decoded(new Editing({})),
113
+ states: {
114
+ editing: {
115
+ on: {
116
+ Finish: (to) => to.local.complete().resolve(({ target }) => target.decoded(new Complete({})))
117
+ }
118
+ }
119
+ }
120
+ }
121
+ }
122
+ },
123
+ connection: {
124
+ states: {
125
+ online: {
126
+ on: {
127
+ Disconnect: (to) => to.local.offline().resolve(({ target }) => target.decoded(new Offline({})))
128
+ }
129
+ }
130
+ }
131
+ }
132
+ }
133
+ }
134
+ })
@@ -0,0 +1,81 @@
1
+ import type { MachineResult, RegistrySnapshot } from "../../DevToolsProtocol.js"
2
+ import { mountVisualizer } from "./visualizer.js"
3
+
4
+ let selectedKey: string | undefined
5
+
6
+ const createElement = <Tag extends keyof HTMLElementTagNameMap>(
7
+ tag: Tag,
8
+ className?: string,
9
+ text?: string
10
+ ): HTMLElementTagNameMap[Tag] => {
11
+ const element = document.createElement(tag)
12
+ if (className !== undefined) element.className = className
13
+ if (text !== undefined) element.textContent = text
14
+ return element
15
+ }
16
+
17
+ const resultLabel = (result: MachineResult): string => {
18
+ if (result._tag === "Ready" || result._tag === "Partial") return result.document.machineId
19
+ return result.machineId ?? result.source.exportName ?? result.source.file.split(/[\\/]/).at(-1) ?? result.key
20
+ }
21
+
22
+ const resultFile = (result: MachineResult): string =>
23
+ result._tag === "Ready" || result._tag === "Partial"
24
+ ? result.document.source?.file ?? "unknown source"
25
+ : result.source.file
26
+
27
+ const statusLabel = (result: MachineResult): string => {
28
+ switch (result._tag) {
29
+ case "Ready":
30
+ return "ready"
31
+ case "Partial":
32
+ return "partial"
33
+ case "Failed":
34
+ return "error"
35
+ }
36
+ }
37
+
38
+ export const mountMachineIndex = (root: HTMLElement, snapshot: RegistrySnapshot): void => {
39
+ const results = [...snapshot.results].sort((left, right) => resultLabel(left).localeCompare(resultLabel(right)))
40
+ if (selectedKey === undefined || !results.some((result) => result.key === selectedKey)) {
41
+ selectedKey = results[0]?.key
42
+ }
43
+
44
+ const shell = createElement("main", "devtools-shell")
45
+ const index = createElement("nav", "machine-index")
46
+ index.setAttribute("aria-label", "Machines")
47
+ const view = createElement("div", "machine-view")
48
+
49
+ if (results.length === 0) {
50
+ index.append(createElement("div", "machine-index-empty", "No machines"))
51
+ const empty = createElement("div", "registry-empty")
52
+ empty.append(
53
+ createElement("strong", undefined, "No .handle machines found"),
54
+ createElement("span", undefined, "The list updates when a matching source file changes.")
55
+ )
56
+ view.append(empty)
57
+ } else {
58
+ for (const result of results) {
59
+ const button = createElement("button", `machine-row${result.key === selectedKey ? " is-selected" : ""}`)
60
+ button.type = "button"
61
+ button.dataset.machineKey = result.key
62
+ button.setAttribute("aria-current", result.key === selectedKey ? "true" : "false")
63
+ const label = createElement("span", "machine-row-label", resultLabel(result))
64
+ const file = createElement("span", "machine-row-file", resultFile(result))
65
+ const status = createElement("span", `machine-row-status status-${statusLabel(result)}`)
66
+ status.setAttribute("aria-label", statusLabel(result))
67
+ button.append(status, label, file)
68
+ button.addEventListener("click", () => {
69
+ selectedKey = result.key
70
+ mountMachineIndex(root, snapshot)
71
+ })
72
+ index.append(button)
73
+ }
74
+
75
+ const selected = results.find((result) => result.key === selectedKey) ?? results[0]
76
+ if (selected !== undefined) mountVisualizer(view, selected)
77
+ }
78
+
79
+ shell.append(index, view)
80
+ root.replaceChildren(shell)
81
+ }
@@ -0,0 +1,42 @@
1
+ import * as BrowserRuntime from "@effect/platform-browser/BrowserRuntime"
2
+ import * as Effect from "effect/Effect"
3
+ import * as Schema from "effect/Schema"
4
+ import "./styles.css"
5
+ import * as DevToolsProtocol from "../../DevToolsProtocol.js"
6
+ import { mountMachineIndex } from "./machine-index.js"
7
+
8
+ const root = document.querySelector<HTMLDivElement>("#app")
9
+ if (root === null) throw new Error("Visualizer root element was not found")
10
+
11
+ const showConnectionFailure = (message: string): void => {
12
+ const failure = document.createElement("div")
13
+ failure.className = "connection-failure"
14
+ failure.textContent = message
15
+ root.replaceChildren(failure)
16
+ }
17
+
18
+ const connect = Effect.acquireRelease(
19
+ Effect.sync(() => new EventSource("/api/events")),
20
+ (events) => Effect.sync(() => events.close())
21
+ ).pipe(
22
+ Effect.flatMap((events) =>
23
+ Effect.callback<void>((_resume, signal) => {
24
+ events.onmessage = (event) => {
25
+ try {
26
+ const snapshot = Schema.decodeUnknownSync(DevToolsProtocol.RegistrySnapshot)(JSON.parse(event.data))
27
+ mountMachineIndex(root, snapshot)
28
+ } catch (cause) {
29
+ showConnectionFailure(cause instanceof Error ? cause.message : String(cause))
30
+ }
31
+ }
32
+ events.onerror = () => {
33
+ if (events.readyState === EventSource.CLOSED) {
34
+ showConnectionFailure("The visualizer server disconnected. Restart the command to reconnect.")
35
+ }
36
+ }
37
+ signal.addEventListener("abort", () => events.close(), { once: true })
38
+ })
39
+ )
40
+ )
41
+
42
+ Effect.scoped(connect).pipe(BrowserRuntime.runMain)