@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,173 @@
1
+ import type {
2
+ Activity as VisualizationActivity,
3
+ Branch as VisualizationBranch,
4
+ MachineDocument as VisualizationDocument,
5
+ State as VisualizationState,
6
+ Transition as VisualizationTransition
7
+ } from "../../MachineDocument.js"
8
+
9
+ export interface TopologyNode {
10
+ readonly path: string
11
+ readonly label: string
12
+ readonly type: VisualizationState["type"]
13
+ readonly active: boolean
14
+ readonly initial: boolean
15
+ readonly transitionCount: number
16
+ readonly activityCount: number
17
+ readonly children: ReadonlyArray<TopologyNode>
18
+ }
19
+
20
+ export interface StateBreadcrumb {
21
+ readonly path: string
22
+ readonly label: string
23
+ }
24
+
25
+ export interface IncomingTransition {
26
+ readonly transition: VisualizationTransition
27
+ readonly branch: VisualizationBranch
28
+ }
29
+
30
+ export interface StateInspection {
31
+ readonly state: VisualizationState
32
+ readonly label: string
33
+ readonly active: boolean
34
+ readonly initial: boolean
35
+ readonly breadcrumbs: ReadonlyArray<StateBreadcrumb>
36
+ readonly outgoing: ReadonlyArray<VisualizationTransition>
37
+ readonly incoming: ReadonlyArray<IncomingTransition>
38
+ readonly activities: ReadonlyArray<VisualizationActivity>
39
+ }
40
+
41
+ export interface EventInspection {
42
+ readonly event: string
43
+ readonly candidate: boolean
44
+ readonly transitions: ReadonlyArray<VisualizationTransition>
45
+ }
46
+
47
+ export interface VisualizerModel {
48
+ readonly machineId: string
49
+ readonly roots: ReadonlyArray<TopologyNode>
50
+ readonly hasSnapshot: boolean
51
+ readonly activePaths: ReadonlyArray<string>
52
+ readonly candidateEvents: ReadonlyArray<string>
53
+ readonly inspectState: (path: string) => StateInspection | undefined
54
+ readonly inspectEvent: (event: string) => EventInspection
55
+ }
56
+
57
+ export const stateLabel = (state: VisualizationState): string =>
58
+ state.title === null ? state.key : `${state.title} (${state.key})`
59
+
60
+ export const triggerLabel = (transition: VisualizationTransition): string => {
61
+ switch (transition.trigger.type) {
62
+ case "event":
63
+ return transition.trigger.event
64
+ case "always":
65
+ return "Always"
66
+ case "done":
67
+ return "On completion"
68
+ case "choice":
69
+ return "Choice"
70
+ case "invoke":
71
+ return `${transition.trigger.id} · ${transition.trigger.outcome}`
72
+ }
73
+ }
74
+
75
+ const buildInitialPaths = (document: VisualizationDocument): ReadonlySet<string> => {
76
+ const initial = new Set<string>([document.initial.target])
77
+ for (const state of document.states) {
78
+ if (state.initial !== null) initial.add(state.initial)
79
+ }
80
+ return initial
81
+ }
82
+
83
+ export const makeVisualizerModel = (document: VisualizationDocument): VisualizerModel => {
84
+ const states = new Map(document.states.map((state) => [state.path, state]))
85
+ const transitions = new Map(document.transitions.map((transition) => [transition.id, transition]))
86
+ const activities = new Map(document.activities.map((activity) => [activity.id, activity]))
87
+ const active = new Set(document.snapshot?.activePaths ?? [])
88
+ const initial = buildInitialPaths(document)
89
+ const candidateEvents = document.snapshot?.candidateEvents ?? []
90
+
91
+ const outgoing = (state: VisualizationState): ReadonlyArray<VisualizationTransition> =>
92
+ state.transitionIds.flatMap((id): ReadonlyArray<VisualizationTransition> => {
93
+ const transition = transitions.get(id)
94
+ return transition === undefined ? [] : [transition]
95
+ })
96
+
97
+ const ownedActivities = (state: VisualizationState): ReadonlyArray<VisualizationActivity> =>
98
+ state.activityIds.flatMap((id): ReadonlyArray<VisualizationActivity> => {
99
+ const definition = activities.get(id)
100
+ return definition === undefined ? [] : [definition]
101
+ })
102
+
103
+ const incoming = new Map<string, Array<IncomingTransition>>()
104
+ for (const transition of document.transitions) {
105
+ for (const branch of transition.branches) {
106
+ if (branch.target === null) continue
107
+ const registrations = incoming.get(branch.target) ?? []
108
+ registrations.push({ transition, branch })
109
+ incoming.set(branch.target, registrations)
110
+ }
111
+ }
112
+
113
+ const visit = (path: string): TopologyNode | undefined => {
114
+ const state = states.get(path)
115
+ if (state === undefined) return undefined
116
+ return {
117
+ path: state.path,
118
+ label: stateLabel(state),
119
+ type: state.type,
120
+ active: active.has(state.path),
121
+ initial: initial.has(state.path),
122
+ transitionCount: state.transitionIds.length,
123
+ activityCount: state.activityIds.length,
124
+ children: state.children.flatMap((childPath): ReadonlyArray<TopologyNode> => {
125
+ const child = visit(childPath)
126
+ return child === undefined ? [] : [child]
127
+ })
128
+ }
129
+ }
130
+
131
+ const breadcrumbs = (state: VisualizationState): ReadonlyArray<StateBreadcrumb> => {
132
+ const result: Array<StateBreadcrumb> = []
133
+ let current: VisualizationState | undefined = state
134
+ while (current !== undefined) {
135
+ result.unshift({ path: current.path, label: stateLabel(current) })
136
+ current = current.parent === null ? undefined : states.get(current.parent)
137
+ }
138
+ return result
139
+ }
140
+
141
+ return {
142
+ machineId: document.machineId,
143
+ roots: document.roots.flatMap((path): ReadonlyArray<TopologyNode> => {
144
+ const root = visit(path)
145
+ return root === undefined ? [] : [root]
146
+ }),
147
+ hasSnapshot: document.snapshot !== null,
148
+ activePaths: [...active],
149
+ candidateEvents: [...candidateEvents],
150
+ inspectState: (path) => {
151
+ const state = states.get(path)
152
+ return state === undefined
153
+ ? undefined
154
+ : {
155
+ state,
156
+ label: stateLabel(state),
157
+ active: active.has(path),
158
+ initial: initial.has(path),
159
+ breadcrumbs: breadcrumbs(state),
160
+ outgoing: outgoing(state),
161
+ incoming: [...incoming.get(path) ?? []],
162
+ activities: ownedActivities(state)
163
+ }
164
+ },
165
+ inspectEvent: (event) => ({
166
+ event,
167
+ candidate: candidateEvents.includes(event),
168
+ transitions: document.transitions.filter((transition) =>
169
+ transition.trigger.type === "event" && transition.trigger.event === event
170
+ )
171
+ })
172
+ }
173
+ }
@@ -0,0 +1,31 @@
1
+ import type { MachineResult } from "../../DevToolsProtocol.js"
2
+ import { renderVisualizer } from "./visualizer-app.js"
3
+
4
+ const renderError = (root: HTMLElement, source: Extract<MachineResult, { readonly _tag: "Failed" }>): void => {
5
+ const shell = document.createElement("main")
6
+ shell.className = "failure-shell"
7
+ shell.setAttribute("role", "alert")
8
+ const kind = document.createElement("span")
9
+ kind.className = "failure-kind"
10
+ kind.textContent = "Visualizer error"
11
+ const title = document.createElement("h1")
12
+ title.textContent = "Machine could not be inspected"
13
+ const message = document.createElement("pre")
14
+ message.textContent = source.diagnostics.map((diagnostic) => diagnostic.message).join("\n")
15
+ const hint = document.createElement("p")
16
+ hint.textContent = "Fix the machine definition and the page will reload."
17
+ shell.append(kind, title, message, hint)
18
+ root.replaceChildren(shell)
19
+ }
20
+
21
+ export const mountVisualizer = (root: HTMLElement, source: MachineResult): void => {
22
+ switch (source._tag) {
23
+ case "Ready":
24
+ case "Partial":
25
+ renderVisualizer(root, source.document, source.diagnostics)
26
+ break
27
+ case "Failed":
28
+ renderError(root, source)
29
+ break
30
+ }
31
+ }
@@ -0,0 +1,171 @@
1
+ import { type ChokidarOptions, type FSWatcher, watch } from "chokidar"
2
+ import * as Effect from "effect/Effect"
3
+ import * as Stream from "effect/Stream"
4
+ import type { IncomingMessage, ServerResponse } from "node:http"
5
+ import { isAbsolute, relative, resolve } from "node:path"
6
+ import { fileURLToPath } from "node:url"
7
+ import { createServer, type Plugin, type ViteDevServer } from "vite"
8
+ import type * as DevServer from "../DevServer.js"
9
+ import * as MachineRegistry from "../MachineRegistry.js"
10
+
11
+ type DevServerErrorConstructor = typeof DevServer.DevServerError
12
+
13
+ const packageRoot = fileURLToPath(new URL("../..", import.meta.url))
14
+
15
+ const sourceExtension = /\.(?:[cm]?[jt]sx?)$/
16
+
17
+ const isProjectSource = (root: string, file: string): boolean => {
18
+ const projectPath = relative(root, isAbsolute(file) ? file : resolve(root, file))
19
+ return projectPath !== "" &&
20
+ !projectPath.startsWith("..") &&
21
+ sourceExtension.test(projectPath) &&
22
+ !projectPath.split(/[\\/]/).some((part) =>
23
+ part === "node_modules" ||
24
+ part === ".git" ||
25
+ part === "dist" ||
26
+ part === "build" ||
27
+ part === "coverage" ||
28
+ part === ".data" ||
29
+ part === "references"
30
+ )
31
+ }
32
+
33
+ const isIgnored = (file: string): boolean =>
34
+ file.split(/[\\/]/).some((part) =>
35
+ part === "node_modules" ||
36
+ part === ".git" ||
37
+ part === "dist" ||
38
+ part === "build" ||
39
+ part === "coverage" ||
40
+ part === ".data" ||
41
+ part === "references"
42
+ )
43
+
44
+ const writeJson = (response: ServerResponse, value: unknown): void => {
45
+ response.statusCode = 200
46
+ response.setHeader("content-type", "application/json; charset=utf-8")
47
+ response.setHeader("cache-control", "no-store")
48
+ response.end(JSON.stringify(value))
49
+ }
50
+
51
+ const apiPlugin = (
52
+ registry: MachineRegistry.MachineRegistry["Service"]
53
+ ): Plugin => ({
54
+ name: "effect-machine-devtools-api",
55
+ configureServer(server) {
56
+ server.middlewares.use((request: IncomingMessage, response: ServerResponse, next: () => void) => {
57
+ if (request.url === "/api/machines") {
58
+ void Effect.runPromise(registry.get).then(
59
+ (snapshot) => writeJson(response, snapshot),
60
+ (cause) => {
61
+ response.statusCode = 500
62
+ response.end(String(cause))
63
+ }
64
+ )
65
+ return
66
+ }
67
+ if (request.url !== "/api/events") {
68
+ next()
69
+ return
70
+ }
71
+
72
+ response.statusCode = 200
73
+ response.setHeader("content-type", "text/event-stream")
74
+ response.setHeader("cache-control", "no-cache, no-transform")
75
+ response.setHeader("connection", "keep-alive")
76
+ response.flushHeaders()
77
+
78
+ const controller = new AbortController()
79
+ request.on("close", () => controller.abort())
80
+ void Effect.runPromise(
81
+ registry.changes.pipe(
82
+ Stream.runForEach((snapshot) => Effect.sync(() => response.write(`data: ${JSON.stringify(snapshot)}\n\n`)))
83
+ ),
84
+ { signal: controller.signal }
85
+ ).catch(() => undefined)
86
+ })
87
+ }
88
+ })
89
+
90
+ const acquire = (
91
+ ErrorType: DevServerErrorConstructor,
92
+ options: DevServer.Options,
93
+ registry: MachineRegistry.MachineRegistry["Service"]
94
+ ): Effect.Effect<ViteDevServer, DevServer.DevServerError, never> =>
95
+ Effect.tryPromise({
96
+ try: async () => {
97
+ const server = await createServer({
98
+ root: packageRoot,
99
+ appType: "spa",
100
+ logLevel: "error",
101
+ plugins: [apiPlugin(registry)],
102
+ server: {
103
+ host: options.host,
104
+ port: options.port,
105
+ strictPort: true,
106
+ open: options.open ?? false
107
+ }
108
+ })
109
+ await server.listen()
110
+ return server
111
+ },
112
+ catch: (cause) =>
113
+ new ErrorType({
114
+ message: `Could not start the Effect Machine visualizer on ${options.host}:${options.port}`,
115
+ cause
116
+ })
117
+ })
118
+
119
+ const acquireWatcher = (
120
+ options: DevServer.Options,
121
+ registry: MachineRegistry.MachineRegistry["Service"]
122
+ ): Effect.Effect<FSWatcher> =>
123
+ Effect.sync(() => {
124
+ let refreshTimer: ReturnType<typeof setTimeout> | undefined
125
+ const watcher = watch(options.root, watcherOptions(options))
126
+ watcher.on("all", (_event, file) => {
127
+ if (!isProjectSource(options.root, file)) return
128
+ if (refreshTimer !== undefined) clearTimeout(refreshTimer)
129
+ refreshTimer = setTimeout(() => {
130
+ Effect.runFork(
131
+ registry.refresh.pipe(
132
+ Effect.catch((cause) => Effect.logWarning("Machine reload failed", cause))
133
+ )
134
+ )
135
+ }, options.debounce ?? 150)
136
+ })
137
+ watcher.on("close", () => {
138
+ if (refreshTimer !== undefined) clearTimeout(refreshTimer)
139
+ })
140
+ watcher.on("error", (cause) => {
141
+ Effect.runFork(Effect.logError("Machine file watcher failed", cause))
142
+ })
143
+ return watcher
144
+ })
145
+
146
+ export const watcherOptions = (options: DevServer.Options): ChokidarOptions =>
147
+ ({
148
+ ignoreInitial: true,
149
+ interval: 200,
150
+ usePolling: options.watchPolling ?? false,
151
+ ignored: (file, stats) => isIgnored(file) || stats?.isFile() === true && !sourceExtension.test(file)
152
+ }) satisfies ChokidarOptions
153
+
154
+ export const run = (
155
+ ErrorType: DevServerErrorConstructor,
156
+ options: DevServer.Options
157
+ ): Effect.Effect<never, DevServer.DevServerError, MachineRegistry.MachineRegistry> =>
158
+ Effect.gen(function*() {
159
+ const registry = yield* MachineRegistry.MachineRegistry
160
+ const server = yield* Effect.acquireRelease(
161
+ acquire(ErrorType, options, registry),
162
+ (server) => Effect.promise(() => server.close())
163
+ )
164
+ yield* Effect.acquireRelease(
165
+ acquireWatcher(options, registry),
166
+ (watcher) => Effect.promise(() => watcher.close())
167
+ )
168
+ const address = server.resolvedUrls?.local[0] ?? `http://${options.host}:${options.port}/`
169
+ yield* Effect.logInfo(`Effect Machine visualizer: ${address}`)
170
+ return yield* Effect.never
171
+ }).pipe(Effect.scoped)
@@ -0,0 +1,138 @@
1
+ import * as NodeWorkerRunner from "@effect/platform-node/NodeWorkerRunner"
2
+ import type { Machine } from "@typeonce/effect-machine"
3
+ import * as Effect from "effect/Effect"
4
+ import * as WorkerRunner from "effect/unstable/workers/WorkerRunner"
5
+ import { resolve } from "node:path"
6
+ import { pathToFileURL } from "node:url"
7
+ import type { ViteDevServer } from "vite"
8
+ import * as DevToolsProtocol from "../DevToolsProtocol.js"
9
+ import * as MachineDocument from "../MachineDocument.js"
10
+ import type * as ProjectInspector from "../ProjectInspector.js"
11
+
12
+ interface EvaluationRequest {
13
+ readonly root: string
14
+ readonly revision: number
15
+ readonly candidates: ReadonlyArray<ProjectInspector.Candidate>
16
+ }
17
+
18
+ interface EvaluationResponse {
19
+ readonly results: ReadonlyArray<DevToolsProtocol.MachineResult>
20
+ }
21
+
22
+ const isMachine = (value: unknown): value is Machine.Machine.Any =>
23
+ typeof value === "object" &&
24
+ value !== null &&
25
+ "stateNodes" in value &&
26
+ "handlers" in value &&
27
+ "initialDefinition" in value &&
28
+ "initial" in value
29
+
30
+ const sourceOf = (file: string, exportName: string | null): MachineDocument.Source => ({ file, exportName })
31
+
32
+ const diagnostic = (
33
+ file: string,
34
+ code: string,
35
+ message: string
36
+ ): DevToolsProtocol.Diagnostic => ({
37
+ severity: "error",
38
+ code,
39
+ message,
40
+ location: { file, line: null, column: null },
41
+ statePath: null
42
+ })
43
+
44
+ const messageOf = (cause: unknown): string => cause instanceof Error ? cause.message : String(cause)
45
+
46
+ const failed = (
47
+ candidate: ProjectInspector.Candidate,
48
+ code: string,
49
+ message: string,
50
+ exportName: string | null = candidate.exportNames[0] ?? null
51
+ ): DevToolsProtocol.Failed => ({
52
+ _tag: "Failed",
53
+ protocolVersion: DevToolsProtocol.protocolVersion,
54
+ key: exportName === null ? candidate.file : `${candidate.file}#${exportName}`,
55
+ source: sourceOf(candidate.file, exportName),
56
+ machineId: null,
57
+ diagnostics: [diagnostic(candidate.file, code, message)]
58
+ })
59
+
60
+ const evaluateCandidate = (
61
+ server: ViteDevServer,
62
+ request: EvaluationRequest,
63
+ candidate: ProjectInspector.Candidate
64
+ ): Effect.Effect<ReadonlyArray<DevToolsProtocol.MachineResult>> =>
65
+ Effect.tryPromise({
66
+ try: () => server.ssrLoadModule(pathToFileURL(resolve(request.root, candidate.file)).href),
67
+ catch: (cause) => cause
68
+ }).pipe(
69
+ Effect.flatMap((module) =>
70
+ Effect.try(() =>
71
+ Object.entries(module).filter((entry): entry is [string, Machine.Machine.Any] => isMachine(entry[1]))
72
+ )
73
+ ),
74
+ Effect.flatMap((machines) => {
75
+ if (machines.length === 0) {
76
+ return Effect.succeed([failed(
77
+ candidate,
78
+ "machine-export-not-found",
79
+ "The module contains a .handle(...) call but does not export the resulting machine"
80
+ )])
81
+ }
82
+
83
+ const seen = new Set<Machine.Machine.Any>()
84
+ return Effect.forEach(machines, ([exportName, machine]) => {
85
+ if (seen.has(machine)) return Effect.succeed<DevToolsProtocol.MachineResult | undefined>(undefined)
86
+ seen.add(machine)
87
+ const source = sourceOf(candidate.file, exportName)
88
+ return Effect.try({
89
+ try: () =>
90
+ ({
91
+ _tag: "Ready",
92
+ protocolVersion: DevToolsProtocol.protocolVersion,
93
+ key: `${candidate.file}#${exportName}`,
94
+ document: MachineDocument.make(machine, {
95
+ revision: request.revision,
96
+ source
97
+ }),
98
+ diagnostics: []
99
+ }) satisfies DevToolsProtocol.Ready,
100
+ catch: (cause) => cause
101
+ }).pipe(
102
+ Effect.catch((cause) =>
103
+ Effect.succeed(failed(
104
+ candidate,
105
+ "machine-inspection-failed",
106
+ messageOf(cause),
107
+ exportName
108
+ ))
109
+ )
110
+ )
111
+ }).pipe(
112
+ Effect.map((results) =>
113
+ results.filter((result): result is DevToolsProtocol.MachineResult => result !== undefined)
114
+ )
115
+ )
116
+ }),
117
+ Effect.catch((cause) => Effect.succeed([failed(candidate, "module-load-failed", messageOf(cause))]))
118
+ )
119
+
120
+ const handle = (server: ViteDevServer, request: EvaluationRequest): Effect.Effect<EvaluationResponse> =>
121
+ Effect.forEach(request.candidates, (candidate) => evaluateCandidate(server, request, candidate), {
122
+ concurrency: 1
123
+ }).pipe(
124
+ Effect.map((results) => ({ results: results.flat() }))
125
+ )
126
+
127
+ export const run = (server: ViteDevServer): Promise<void> => {
128
+ return Effect.gen(function*() {
129
+ const platform = yield* WorkerRunner.WorkerRunnerPlatform
130
+ const runner = yield* platform.start<EvaluationResponse, EvaluationRequest>()
131
+ yield* runner.run((_portId, request) =>
132
+ Effect.flatMap(handle(server, request), (response) => runner.send(0, response))
133
+ )
134
+ }).pipe(
135
+ Effect.provide(NodeWorkerRunner.layer),
136
+ Effect.runPromise
137
+ )
138
+ }
@@ -0,0 +1,21 @@
1
+ import { workerData } from "node:worker_threads"
2
+ import { createServer } from "vite"
3
+
4
+ const server = await createServer({
5
+ appType: "custom",
6
+ logLevel: "silent",
7
+ server: {
8
+ hmr: false,
9
+ middlewareMode: true
10
+ },
11
+ optimizeDeps: {
12
+ noDiscovery: true
13
+ }
14
+ })
15
+
16
+ try {
17
+ const runtime = await server.ssrLoadModule(workerData.runtimeModuleUrl)
18
+ await runtime.run(server)
19
+ } finally {
20
+ await server.close()
21
+ }
@@ -0,0 +1,138 @@
1
+ import { Machine } from "@typeonce/effect-machine"
2
+ import type * as Public from "../MachineDocument.js"
3
+
4
+ const enabled = Machine.enabled as (
5
+ machine: Machine.Machine.Any,
6
+ snapshot: unknown
7
+ ) => ReadonlyArray<PropertyKey>
8
+
9
+ const selection = (value: Machine.Machine.TransitionTargetSelection): Public.Selection => ({
10
+ path: value.path ?? null,
11
+ kind: value.kind,
12
+ scope: value.scope ?? null
13
+ })
14
+
15
+ const trigger = (value: Machine.Machine.TransitionTrigger): Public.Trigger => {
16
+ switch (value.type) {
17
+ case "event":
18
+ return { type: "event", event: String(value.event) }
19
+ case "always":
20
+ return { type: "always" }
21
+ case "done":
22
+ return { type: "done" }
23
+ case "choice":
24
+ return { type: "choice" }
25
+ case "invoke":
26
+ return { type: "invoke", id: value.id, outcome: value.outcome }
27
+ }
28
+ }
29
+
30
+ const activity = (value: Machine.Machine.ActivityDefinition, id: string): Public.Activity => {
31
+ const common = { id, source: value.source, lifecycleId: value.id }
32
+ switch (value.type) {
33
+ case "process":
34
+ return { ...common, type: "process" }
35
+ case "effect":
36
+ return { ...common, type: "effect", outcomes: { ...value.outcomes } }
37
+ case "timer":
38
+ return { ...common, type: "timer", duration: value.duration }
39
+ case "stream":
40
+ return { ...common, type: "stream" }
41
+ case "machine":
42
+ return { ...common, type: "machine", child: { ...value.child } }
43
+ }
44
+ }
45
+
46
+ const appendReference = (index: Map<string, Array<string>>, owner: string, id: string): void => {
47
+ const references = index.get(owner) ?? []
48
+ references.push(id)
49
+ index.set(owner, references)
50
+ }
51
+
52
+ export const make = <M extends Machine.Machine.Any>(
53
+ machine: M,
54
+ options: Public.MakeOptions<M> = {}
55
+ ): Public.MachineDocument => {
56
+ const nodes = Machine.stateNodes(machine)
57
+ const transitionIds = new Map<string, Array<string>>()
58
+ const activityIds = new Map<string, Array<string>>()
59
+ const transitionOffsets = new Map<string, number>()
60
+ const activityOffsets = new Map<string, number>()
61
+ const childPaths = new Map<string | null, Array<string>>()
62
+
63
+ for (const node of nodes) {
64
+ const parent = node.parent ?? null
65
+ const siblings = childPaths.get(parent) ?? []
66
+ siblings.push(node.path)
67
+ childPaths.set(parent, siblings)
68
+ }
69
+
70
+ const transitions = Machine.transitionDefinitions(machine).map((definition): Public.Transition => {
71
+ const offset = transitionOffsets.get(definition.source) ?? 0
72
+ transitionOffsets.set(definition.source, offset + 1)
73
+ const id = `${definition.source}:transition:${offset}`
74
+ appendReference(transitionIds, definition.source, id)
75
+ return {
76
+ id,
77
+ source: definition.source,
78
+ trigger: trigger(definition.trigger),
79
+ reenter: definition.reenter,
80
+ acceptance: definition.acceptance,
81
+ branches: definition.branches.map((branch, branchIndex): Public.Branch => {
82
+ const common = {
83
+ id: `${id}:branch:${branchIndex}`,
84
+ target: branch.target ?? null,
85
+ selection: selection(branch.selection),
86
+ updates: [...branch.updates]
87
+ }
88
+ return branch.type === "direct"
89
+ ? { ...common, type: "direct" }
90
+ : { ...common, type: "branch", key: branch.key, title: branch.title }
91
+ })
92
+ }
93
+ })
94
+
95
+ const activities = Machine.activityDefinitions(machine).map((definition): Public.Activity => {
96
+ const offset = activityOffsets.get(definition.source) ?? 0
97
+ activityOffsets.set(definition.source, offset + 1)
98
+ const id = `${definition.source}:activity:${offset}`
99
+ appendReference(activityIds, definition.source, id)
100
+ return activity(definition, id)
101
+ })
102
+
103
+ const initial = Machine.initialDefinition(machine)
104
+ return {
105
+ schemaVersion: 1,
106
+ revision: options.revision ?? 0,
107
+ source: options.source ?? null,
108
+ machineId: machine.id ?? "Machine",
109
+ initial: {
110
+ target: initial.target,
111
+ selection: selection(initial.selection)
112
+ },
113
+ roots: [...childPaths.get(null) ?? []],
114
+ states: nodes.map((node): Public.State => ({
115
+ path: node.path,
116
+ key: node.key,
117
+ order: node.order,
118
+ title: node.annotations?.title ?? null,
119
+ description: node.annotations?.description ?? null,
120
+ documentation: node.annotations?.documentation ?? null,
121
+ type: node.type,
122
+ history: node.history ?? null,
123
+ parent: node.parent ?? null,
124
+ children: [...childPaths.get(node.path) ?? []],
125
+ initial: node.initial ?? null,
126
+ transitionIds: [...transitionIds.get(node.path) ?? []],
127
+ activityIds: [...activityIds.get(node.path) ?? []]
128
+ })),
129
+ transitions,
130
+ activities,
131
+ snapshot: options.snapshot === undefined
132
+ ? null
133
+ : {
134
+ activePaths: Machine.configuration(machine, options.snapshot).map((node) => node.path),
135
+ candidateEvents: enabled(machine, options.snapshot).map(String)
136
+ }
137
+ }
138
+ }