@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.
- package/LICENSE +21 -0
- package/NOTICE +3 -0
- package/README.md +81 -0
- package/dist/DevServer.d.ts +42 -0
- package/dist/DevServer.d.ts.map +1 -0
- package/dist/DevServer.js +28 -0
- package/dist/DevServer.js.map +1 -0
- package/dist/DevToolsProtocol.d.ts +954 -0
- package/dist/DevToolsProtocol.d.ts.map +1 -0
- package/dist/DevToolsProtocol.js +96 -0
- package/dist/DevToolsProtocol.js.map +1 -0
- package/dist/MachineDocument.d.ts +396 -0
- package/dist/MachineDocument.d.ts.map +1 -0
- package/dist/MachineDocument.js +172 -0
- package/dist/MachineDocument.js.map +1 -0
- package/dist/MachineRegistry.d.ts +349 -0
- package/dist/MachineRegistry.d.ts.map +1 -0
- package/dist/MachineRegistry.js +46 -0
- package/dist/MachineRegistry.js.map +1 -0
- package/dist/MachineSimulator.d.ts +169 -0
- package/dist/MachineSimulator.d.ts.map +1 -0
- package/dist/MachineSimulator.js +98 -0
- package/dist/MachineSimulator.js.map +1 -0
- package/dist/ProjectInspector.d.ts +116 -0
- package/dist/ProjectInspector.d.ts.map +1 -0
- package/dist/ProjectInspector.js +82 -0
- package/dist/ProjectInspector.js.map +1 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +23 -0
- package/dist/bin.js.map +1 -0
- package/dist/client/assets/index-BGOhE3Ng.css +1 -0
- package/dist/client/assets/index-DiqGhsGR.js +14 -0
- package/dist/client/index.html +14 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/devServer.d.ts +9 -0
- package/dist/internal/devServer.d.ts.map +1 -0
- package/dist/internal/devServer.js +119 -0
- package/dist/internal/devServer.js.map +1 -0
- package/dist/internal/evaluationWorker.d.ts +3 -0
- package/dist/internal/evaluationWorker.d.ts.map +1 -0
- package/dist/internal/evaluationWorker.js +69 -0
- package/dist/internal/evaluationWorker.js.map +1 -0
- package/dist/internal/evaluationWorkerBootstrap.d.ts +2 -0
- package/dist/internal/evaluationWorkerBootstrap.d.ts.map +1 -0
- package/dist/internal/evaluationWorkerBootstrap.js +21 -0
- package/dist/internal/evaluationWorkerBootstrap.js.map +1 -0
- package/dist/internal/machineDocument.d.ts +4 -0
- package/dist/internal/machineDocument.d.ts.map +1 -0
- package/dist/internal/machineDocument.js +122 -0
- package/dist/internal/machineDocument.js.map +1 -0
- package/dist/internal/machineRegistry.d.ts +12 -0
- package/dist/internal/machineRegistry.d.ts.map +1 -0
- package/dist/internal/machineRegistry.js +97 -0
- package/dist/internal/machineRegistry.js.map +1 -0
- package/dist/internal/machineSimulator.d.ts +5 -0
- package/dist/internal/machineSimulator.d.ts.map +1 -0
- package/dist/internal/machineSimulator.js +156 -0
- package/dist/internal/machineSimulator.js.map +1 -0
- package/dist/internal/projectInspector.d.ts +11 -0
- package/dist/internal/projectInspector.d.ts.map +1 -0
- package/dist/internal/projectInspector.js +150 -0
- package/dist/internal/projectInspector.js.map +1 -0
- package/index.html +13 -0
- package/package.json +69 -0
- package/src/DevServer.ts +45 -0
- package/src/DevToolsProtocol.ts +146 -0
- package/src/MachineDocument.ts +264 -0
- package/src/MachineRegistry.ts +69 -0
- package/src/MachineSimulator.ts +154 -0
- package/src/ProjectInspector.ts +140 -0
- package/src/bin.ts +56 -0
- package/src/index.ts +20 -0
- package/src/internal/browser/example-machine.ts +134 -0
- package/src/internal/browser/machine-index.ts +81 -0
- package/src/internal/browser/main.ts +42 -0
- package/src/internal/browser/styles.css +817 -0
- package/src/internal/browser/text-tree.ts +214 -0
- package/src/internal/browser/visualizer-app.ts +659 -0
- package/src/internal/browser/visualizer-model.ts +173 -0
- package/src/internal/browser/visualizer.ts +31 -0
- package/src/internal/devServer.ts +171 -0
- package/src/internal/evaluationWorker.ts +138 -0
- package/src/internal/evaluationWorkerBootstrap.js +21 -0
- package/src/internal/machineDocument.ts +138 -0
- package/src/internal/machineRegistry.ts +128 -0
- package/src/internal/machineSimulator.ts +199 -0
- 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
|
+
}
|