@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,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)
|