@typeonce/effect-machine-devtools 0.23.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/NOTICE +4 -0
  2. package/README.md +14 -8
  3. package/dist/DevServer.d.ts +2 -1
  4. package/dist/DevServer.d.ts.map +1 -1
  5. package/dist/DevServer.js.map +1 -1
  6. package/dist/DevToolsProtocol.d.ts +167 -17
  7. package/dist/DevToolsProtocol.d.ts.map +1 -1
  8. package/dist/DevToolsProtocol.js +1 -1
  9. package/dist/MachineDocument.d.ts +98 -2
  10. package/dist/MachineDocument.d.ts.map +1 -1
  11. package/dist/MachineDocument.js +35 -1
  12. package/dist/MachineDocument.js.map +1 -1
  13. package/dist/MachineRegistry.d.ts +56 -6
  14. package/dist/MachineRegistry.d.ts.map +1 -1
  15. package/dist/MachineWalkthrough.d.ts +171 -0
  16. package/dist/MachineWalkthrough.d.ts.map +1 -0
  17. package/dist/MachineWalkthrough.js +98 -0
  18. package/dist/MachineWalkthrough.js.map +1 -0
  19. package/dist/client/assets/index-BTOAhezX.js +37 -0
  20. package/dist/client/assets/index-Cg7xzSkz.css +1 -0
  21. package/dist/client/index.html +4 -4
  22. package/dist/index.d.ts +2 -2
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +2 -2
  25. package/dist/index.js.map +1 -1
  26. package/dist/internal/devServer.d.ts +2 -1
  27. package/dist/internal/devServer.d.ts.map +1 -1
  28. package/dist/internal/devServer.js +4 -3
  29. package/dist/internal/devServer.js.map +1 -1
  30. package/dist/internal/evaluationWorker.d.ts.map +1 -1
  31. package/dist/internal/evaluationWorker.js +28 -9
  32. package/dist/internal/evaluationWorker.js.map +1 -1
  33. package/dist/internal/machineDocument.d.ts.map +1 -1
  34. package/dist/internal/machineDocument.js +65 -2
  35. package/dist/internal/machineDocument.js.map +1 -1
  36. package/dist/internal/machineWalkthrough.d.ts +26 -0
  37. package/dist/internal/machineWalkthrough.d.ts.map +1 -0
  38. package/dist/internal/machineWalkthrough.js +256 -0
  39. package/dist/internal/machineWalkthrough.js.map +1 -0
  40. package/dist/internal/projectInspector.d.ts.map +1 -1
  41. package/dist/internal/projectInspector.js +15 -9
  42. package/dist/internal/projectInspector.js.map +1 -1
  43. package/index.html +2 -2
  44. package/package.json +4 -3
  45. package/src/DevServer.ts +6 -2
  46. package/src/DevToolsProtocol.ts +1 -1
  47. package/src/MachineDocument.ts +56 -1
  48. package/src/MachineWalkthrough.ts +199 -0
  49. package/src/index.ts +2 -2
  50. package/src/internal/browser/chart-layout.ts +433 -0
  51. package/src/internal/browser/chart-model.ts +234 -0
  52. package/src/internal/browser/chart-renderer.ts +661 -0
  53. package/src/internal/browser/example-machine.ts +15 -6
  54. package/src/internal/browser/input-form.ts +252 -0
  55. package/src/internal/browser/invoke-outcomes-example.ts +231 -0
  56. package/src/internal/browser/parallel-completion-example.ts +199 -0
  57. package/src/internal/browser/planner-example.ts +145 -0
  58. package/src/internal/browser/protocol-events-example.ts +200 -0
  59. package/src/internal/browser/styles.css +959 -185
  60. package/src/internal/browser/transition-semantics-example.ts +236 -0
  61. package/src/internal/browser/visualizer-app.ts +818 -389
  62. package/src/internal/browser/visualizer-model.ts +104 -0
  63. package/src/internal/browser/visualizer.ts +1 -1
  64. package/src/internal/devServer.ts +9 -4
  65. package/src/internal/evaluationWorker.ts +28 -10
  66. package/src/internal/machineDocument.ts +77 -2
  67. package/src/internal/machineWalkthrough.ts +355 -0
  68. package/src/internal/projectInspector.ts +29 -14
  69. package/dist/MachineSimulator.d.ts +0 -169
  70. package/dist/MachineSimulator.d.ts.map +0 -1
  71. package/dist/MachineSimulator.js +0 -98
  72. package/dist/MachineSimulator.js.map +0 -1
  73. package/dist/client/assets/index-BGOhE3Ng.css +0 -1
  74. package/dist/client/assets/index-DiqGhsGR.js +0 -14
  75. package/dist/internal/machineSimulator.d.ts +0 -5
  76. package/dist/internal/machineSimulator.d.ts.map +0 -1
  77. package/dist/internal/machineSimulator.js +0 -156
  78. package/dist/internal/machineSimulator.js.map +0 -1
  79. package/src/MachineSimulator.ts +0 -154
  80. package/src/internal/machineSimulator.ts +0 -199
@@ -72,6 +72,110 @@ export const triggerLabel = (transition: VisualizationTransition): string => {
72
72
  }
73
73
  }
74
74
 
75
+ const propertyAccess = (key: string): string => /^[$A-Z_a-z][$\w]*$/.test(key) ? `.${key}` : `[${JSON.stringify(key)}]`
76
+
77
+ const pathAccess = (path: string): string => path.split(".").map(propertyAccess).join("")
78
+
79
+ const nearestCompoundScope = (
80
+ document: VisualizationDocument,
81
+ source: string
82
+ ): string | undefined => {
83
+ const states = new Map(document.states.map((state) => [state.path, state]))
84
+ let current = states.get(source)
85
+ while (current !== undefined) {
86
+ if (current.type === "compound") return current.path
87
+ current = current.parent === null ? undefined : states.get(current.parent)
88
+ }
89
+ return undefined
90
+ }
91
+
92
+ const localPathAccess = (
93
+ document: VisualizationDocument,
94
+ source: string,
95
+ path: string
96
+ ): string | undefined => {
97
+ const scope = nearestCompoundScope(document, source)
98
+ if (scope === undefined) return undefined
99
+ if (path === scope) return ""
100
+ const prefix = `${scope}.`
101
+ return path.startsWith(prefix) ? pathAccess(path.slice(prefix.length)) : undefined
102
+ }
103
+
104
+ /** Canonical Effect Machine selector represented by one retained transition branch. */
105
+ export const branchTargetApi = (
106
+ document: VisualizationDocument,
107
+ source: string,
108
+ branch: VisualizationBranch
109
+ ): string | undefined => {
110
+ const { selection } = branch
111
+ if (selection.kind === "none") return "to.none"
112
+ const path = selection.path
113
+ if (path === null || selection.scope === null) return undefined
114
+
115
+ let api: string | undefined
116
+ if (selection.kind === "history" && selection.scope === "full") {
117
+ api = `to.history${pathAccess(path)}`
118
+ } else if (selection.scope === "local") {
119
+ const local = localPathAccess(document, source, path)
120
+ if (local === undefined) return undefined
121
+ switch (selection.kind) {
122
+ case "state":
123
+ api = local === "" ? "to.local.with" : `to.local${local}()`
124
+ break
125
+ case "choice":
126
+ api = local === "" ? undefined : `to.local${local}()`
127
+ break
128
+ case "initial":
129
+ api = local === "" ? undefined : `to.local${local}.initial`
130
+ break
131
+ case "update":
132
+ api = local === "" ? "to.local.update" : undefined
133
+ break
134
+ case "history":
135
+ break
136
+ }
137
+ } else if (selection.scope === "branch") {
138
+ switch (selection.kind) {
139
+ case "state":
140
+ case "choice":
141
+ api = `to.branch${pathAccess(path)}()`
142
+ break
143
+ case "initial":
144
+ api = `to.branch${pathAccess(path)}.initial`
145
+ break
146
+ case "update":
147
+ api = `to.branch${pathAccess(path)}.update`
148
+ break
149
+ case "history":
150
+ break
151
+ }
152
+ } else if (selection.scope === "full") {
153
+ switch (selection.kind) {
154
+ case "state":
155
+ case "choice":
156
+ api = `to.full${pathAccess(path)}()`
157
+ break
158
+ case "initial":
159
+ api = `to.full${pathAccess(path)}.initial`
160
+ break
161
+ case "history":
162
+ api = `to.history${pathAccess(path)}`
163
+ break
164
+ case "update":
165
+ break
166
+ }
167
+ } else if (selection.scope === "initial") {
168
+ if (selection.kind === "state") api = `to${pathAccess(path)}()`
169
+ if (selection.kind === "initial") api = `to${pathAccess(path)}.initial`
170
+ }
171
+
172
+ if (api === undefined || selection.kind === "update") return api
173
+ return branch.updates.reduce(
174
+ (expression, owner) => `${expression}.updating(to.branch${pathAccess(owner)})`,
175
+ api
176
+ )
177
+ }
178
+
75
179
  const buildInitialPaths = (document: VisualizationDocument): ReadonlySet<string> => {
76
180
  const initial = new Set<string>([document.initial.target])
77
181
  for (const state of document.states) {
@@ -22,7 +22,7 @@ export const mountVisualizer = (root: HTMLElement, source: MachineResult): void
22
22
  switch (source._tag) {
23
23
  case "Ready":
24
24
  case "Partial":
25
- renderVisualizer(root, source.document, source.diagnostics)
25
+ renderVisualizer(root, source.key, source.document, source.diagnostics)
26
26
  break
27
27
  case "Failed":
28
28
  renderError(root, source)
@@ -7,6 +7,7 @@ import { fileURLToPath } from "node:url"
7
7
  import { createServer, type Plugin, type ViteDevServer } from "vite"
8
8
  import type * as DevServer from "../DevServer.js"
9
9
  import * as MachineRegistry from "../MachineRegistry.js"
10
+ import * as ProjectInspector from "../ProjectInspector.js"
10
11
 
11
12
  type DevServerErrorConstructor = typeof DevServer.DevServerError
12
13
 
@@ -41,8 +42,8 @@ const isIgnored = (file: string): boolean =>
41
42
  part === "references"
42
43
  )
43
44
 
44
- const writeJson = (response: ServerResponse, value: unknown): void => {
45
- response.statusCode = 200
45
+ const writeJson = (response: ServerResponse, value: unknown, status = 200): void => {
46
+ response.statusCode = status
46
47
  response.setHeader("content-type", "application/json; charset=utf-8")
47
48
  response.setHeader("cache-control", "no-store")
48
49
  response.end(JSON.stringify(value))
@@ -54,7 +55,7 @@ const apiPlugin = (
54
55
  name: "effect-machine-devtools-api",
55
56
  configureServer(server) {
56
57
  server.middlewares.use((request: IncomingMessage, response: ServerResponse, next: () => void) => {
57
- if (request.url === "/api/machines") {
58
+ if (request.method === "GET" && request.url === "/api/machines") {
58
59
  void Effect.runPromise(registry.get).then(
59
60
  (snapshot) => writeJson(response, snapshot),
60
61
  (cause) => {
@@ -154,7 +155,11 @@ export const watcherOptions = (options: DevServer.Options): ChokidarOptions =>
154
155
  export const run = (
155
156
  ErrorType: DevServerErrorConstructor,
156
157
  options: DevServer.Options
157
- ): Effect.Effect<never, DevServer.DevServerError, MachineRegistry.MachineRegistry> =>
158
+ ): Effect.Effect<
159
+ never,
160
+ DevServer.DevServerError,
161
+ MachineRegistry.MachineRegistry | ProjectInspector.ProjectInspector
162
+ > =>
158
163
  Effect.gen(function*() {
159
164
  const registry = yield* MachineRegistry.MachineRegistry
160
165
  const server = yield* Effect.acquireRelease(
@@ -1,6 +1,7 @@
1
1
  import * as NodeWorkerRunner from "@effect/platform-node/NodeWorkerRunner"
2
- import type { Machine } from "@typeonce/effect-machine"
2
+ import { Machine } from "@typeonce/effect-machine"
3
3
  import * as Effect from "effect/Effect"
4
+ import * as Schema from "effect/Schema"
4
5
  import * as WorkerRunner from "effect/unstable/workers/WorkerRunner"
5
6
  import { resolve } from "node:path"
6
7
  import { pathToFileURL } from "node:url"
@@ -10,15 +11,20 @@ import * as MachineDocument from "../MachineDocument.js"
10
11
  import type * as ProjectInspector from "../ProjectInspector.js"
11
12
 
12
13
  interface EvaluationRequest {
14
+ readonly _tag: "InspectMachines"
13
15
  readonly root: string
14
16
  readonly revision: number
15
17
  readonly candidates: ReadonlyArray<ProjectInspector.Candidate>
16
18
  }
17
19
 
18
20
  interface EvaluationResponse {
21
+ readonly _tag: "InspectedMachines"
19
22
  readonly results: ReadonlyArray<DevToolsProtocol.MachineResult>
20
23
  }
21
24
 
25
+ type WorkerRequest = EvaluationRequest
26
+ type WorkerResponse = EvaluationResponse
27
+
22
28
  const isMachine = (value: unknown): value is Machine.Machine.Any =>
23
29
  typeof value === "object" &&
24
30
  value !== null &&
@@ -41,7 +47,22 @@ const diagnostic = (
41
47
  statePath: null
42
48
  })
43
49
 
44
- const messageOf = (cause: unknown): string => cause instanceof Error ? cause.message : String(cause)
50
+ const messageOf = (cause: unknown): string => {
51
+ if (Schema.isSchemaError(cause)) return `Invalid machine value: ${cause.message}`
52
+ if (cause instanceof Error && cause.message.length > 0) return cause.message
53
+ if (typeof cause === "object" && cause !== null && "cause" in cause && Schema.isSchemaError(cause.cause)) {
54
+ const boundary = "boundary" in cause ? String(cause.boundary) : "value"
55
+ return `Invalid machine ${boundary}: ${cause.cause.message}`
56
+ }
57
+ try {
58
+ const encoded = JSON.stringify(cause, null, 2)
59
+ if (encoded !== undefined && encoded !== "{}") return encoded
60
+ } catch {
61
+ // Fall back to the runtime string representation below.
62
+ }
63
+ const rendered = String(cause)
64
+ return rendered.length > 0 ? rendered : "The evaluator failed without a diagnostic message"
65
+ }
45
66
 
46
67
  const failed = (
47
68
  candidate: ProjectInspector.Candidate,
@@ -121,18 +142,15 @@ const handle = (server: ViteDevServer, request: EvaluationRequest): Effect.Effec
121
142
  Effect.forEach(request.candidates, (candidate) => evaluateCandidate(server, request, candidate), {
122
143
  concurrency: 1
123
144
  }).pipe(
124
- Effect.map((results) => ({ results: results.flat() }))
145
+ Effect.map((results) => ({ _tag: "InspectedMachines" as const, results: results.flat() }))
125
146
  )
126
147
 
127
- export const run = (server: ViteDevServer): Promise<void> => {
128
- return Effect.gen(function*() {
148
+ export const run = (server: ViteDevServer): Promise<void> =>
149
+ Effect.gen(function*() {
129
150
  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
- )
151
+ const runner = yield* platform.start<WorkerResponse, WorkerRequest>()
152
+ yield* runner.run((_portId, request) => Effect.flatMap(handle(server, request), (value) => runner.send(0, value)))
134
153
  }).pipe(
135
154
  Effect.provide(NodeWorkerRunner.layer),
136
155
  Effect.runPromise
137
156
  )
138
- }
@@ -1,4 +1,5 @@
1
1
  import { Machine } from "@typeonce/effect-machine"
2
+ import * as Schema from "effect/Schema"
2
3
  import type * as Public from "../MachineDocument.js"
3
4
 
4
5
  const enabled = Machine.enabled as (
@@ -6,6 +7,72 @@ const enabled = Machine.enabled as (
6
7
  snapshot: unknown
7
8
  ) => ReadonlyArray<PropertyKey>
8
9
 
10
+ const inputEventSchemas = Machine.inputEventSchemas as (
11
+ machine: Machine.Machine.Any
12
+ ) => ReadonlyArray<Machine.Machine.TaggedSchema>
13
+
14
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
15
+ typeof value === "object" && value !== null && !Array.isArray(value)
16
+
17
+ const resolveReference = (
18
+ value: unknown,
19
+ definitions: Readonly<Record<string, unknown>>
20
+ ): Record<string, unknown> | undefined => {
21
+ if (!isRecord(value)) return undefined
22
+ if (typeof value.$ref !== "string" || !value.$ref.startsWith("#/$defs/")) return value
23
+ const name = decodeURIComponent(value.$ref.slice("#/$defs/".length))
24
+ const target = definitions[name]
25
+ return isRecord(target) ? target : undefined
26
+ }
27
+
28
+ const tagsOf = (
29
+ value: unknown,
30
+ definitions: Readonly<Record<string, unknown>>
31
+ ): ReadonlyArray<string> => {
32
+ const schema = resolveReference(value, definitions)
33
+ if (schema === undefined || !isRecord(schema.properties)) return []
34
+ const tag = resolveReference(schema.properties._tag, definitions)
35
+ if (tag === undefined) return []
36
+ if (typeof tag.const === "string" || typeof tag.const === "number") return [String(tag.const)]
37
+ return Array.isArray(tag.enum)
38
+ ? tag.enum.filter((item): item is string | number => typeof item === "string" || typeof item === "number").map(
39
+ String
40
+ )
41
+ : []
42
+ }
43
+
44
+ const inputSchema = (schema: Schema.Top): Public.InputSchema => {
45
+ const document = Schema.toJsonSchemaDocument(schema)
46
+ return {
47
+ dialect: document.dialect,
48
+ schema: document.schema as Schema.Json,
49
+ definitions: document.definitions as Record<string, Schema.Json>
50
+ }
51
+ }
52
+
53
+ const eventInputs = (machine: Machine.Machine.Any): ReadonlyArray<Public.EventInput> => {
54
+ const inputs: Array<Public.EventInput> = []
55
+ for (const eventSchema of inputEventSchemas(machine)) {
56
+ const document = inputSchema(eventSchema)
57
+ const root = document.schema
58
+ const record = isRecord(root) ? root : undefined
59
+ const variants = record !== undefined && Array.isArray(record.anyOf)
60
+ ? record.anyOf
61
+ : record !== undefined && Array.isArray(record.oneOf)
62
+ ? record.oneOf
63
+ : [root]
64
+ for (const variant of variants) {
65
+ for (const event of tagsOf(variant, document.definitions)) {
66
+ inputs.push({
67
+ event,
68
+ schema: { ...document, schema: variant as Schema.Json }
69
+ })
70
+ }
71
+ }
72
+ }
73
+ return inputs
74
+ }
75
+
9
76
  const selection = (value: Machine.Machine.TransitionTargetSelection): Public.Selection => ({
10
77
  path: value.path ?? null,
11
78
  kind: value.kind,
@@ -102,7 +169,7 @@ export const make = <M extends Machine.Machine.Any>(
102
169
 
103
170
  const initial = Machine.initialDefinition(machine)
104
171
  return {
105
- schemaVersion: 1,
172
+ schemaVersion: 3,
106
173
  revision: options.revision ?? 0,
107
174
  source: options.source ?? null,
108
175
  machineId: machine.id ?? "Machine",
@@ -123,16 +190,24 @@ export const make = <M extends Machine.Machine.Any>(
123
190
  parent: node.parent ?? null,
124
191
  children: [...childPaths.get(node.path) ?? []],
125
192
  initial: node.initial ?? null,
193
+ valueSchema: node.schema === undefined ? null : inputSchema(node.schema),
194
+ outputSchema: node.output === undefined ? null : inputSchema(node.output),
126
195
  transitionIds: [...transitionIds.get(node.path) ?? []],
127
196
  activityIds: [...activityIds.get(node.path) ?? []]
128
197
  })),
129
198
  transitions,
130
199
  activities,
200
+ inputs: {
201
+ machine: machine.input === undefined ? null : inputSchema(machine.input),
202
+ events: eventInputs(machine)
203
+ },
131
204
  snapshot: options.snapshot === undefined
132
205
  ? null
133
206
  : {
134
207
  activePaths: Machine.configuration(machine, options.snapshot).map((node) => node.path),
135
- candidateEvents: enabled(machine, options.snapshot).map(String)
208
+ candidateEvents: enabled(machine, options.snapshot)
209
+ .map(String)
210
+ .filter((event) => Object.hasOwn(machine.events, event))
136
211
  }
137
212
  }
138
213
  }