@typeonce/effect-machine-devtools 0.31.2 → 0.32.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 (33) hide show
  1. package/dist/MachineRegistry.d.ts +4 -3
  2. package/dist/MachineRegistry.d.ts.map +1 -1
  3. package/dist/MachineRegistry.js +4 -3
  4. package/dist/MachineRegistry.js.map +1 -1
  5. package/dist/client/assets/index-2uvShdlX.js +37 -0
  6. package/dist/client/index.html +1 -1
  7. package/dist/internal/devServer.d.ts +3 -1
  8. package/dist/internal/devServer.d.ts.map +1 -1
  9. package/dist/internal/devServer.js +8 -6
  10. package/dist/internal/devServer.js.map +1 -1
  11. package/dist/internal/machineRegistry.js +9 -6
  12. package/dist/internal/machineRegistry.js.map +1 -1
  13. package/package.json +2 -2
  14. package/src/MachineRegistry.ts +4 -3
  15. package/src/internal/browser/chart-layout-policy.ts +2 -2
  16. package/src/internal/browser/chart-layout.ts +236 -51
  17. package/src/internal/browser/chart-model.ts +3 -0
  18. package/src/internal/browser/chart-renderer.ts +5 -4
  19. package/src/internal/browser/example-machine.ts +94 -85
  20. package/src/internal/browser/hierarchy-routing-example.ts +67 -58
  21. package/src/internal/browser/invoke-outcomes-example.ts +153 -143
  22. package/src/internal/browser/layout-resilience-example.ts +115 -104
  23. package/src/internal/browser/parallel-completion-example.ts +117 -107
  24. package/src/internal/browser/parallel-owner-routing-example.ts +104 -0
  25. package/src/internal/browser/planner-example.ts +44 -38
  26. package/src/internal/browser/protocol-events-example.ts +123 -102
  27. package/src/internal/browser/shared-terminal-routing-example.ts +125 -120
  28. package/src/internal/browser/text-tree.ts +2 -2
  29. package/src/internal/browser/transition-semantics-example.ts +143 -131
  30. package/src/internal/browser/visualizer-model.ts +7 -7
  31. package/src/internal/devServer.ts +15 -12
  32. package/src/internal/machineRegistry.ts +17 -17
  33. package/dist/client/assets/index-CSgOYHLh.js +0 -37
@@ -74,46 +74,53 @@ class Archive extends Schema.TaggedClass<Archive>("TransitionArchive")("Archive"
74
74
  reason: Schema.String
75
75
  }) {}
76
76
 
77
- const TransitionStates = Machine.states({
78
- Workspace: {
79
- schema: Workspace,
80
- initial: "Routing",
81
- states: {
82
- Routing: { type: "choice" },
83
- Draft,
84
- AutoSaving,
85
- Review: {
86
- schema: Review,
87
- initial: "Checking",
88
- states: {
89
- Checking,
90
- ChangesRequested,
91
- Approved: { schema: Approved, type: "final" }
92
- }
93
- },
94
- Finished: { schema: WorkspaceFinished, type: "final" },
95
- recent: { type: "history" },
96
- exact: { type: "history", history: "deep" }
97
- }
98
- },
99
- Paused,
100
- Disabled,
101
- Published: { schema: Published, type: "final", output: Schema.String }
77
+ const TransitionStates = Machine.state({
78
+ initial: "Paused",
79
+ states: {
80
+ Workspace: {
81
+ schema: Workspace,
82
+ initial: "Routing",
83
+ states: {
84
+ Routing: { type: "choice" },
85
+ Draft,
86
+ AutoSaving,
87
+ Review: {
88
+ schema: Review,
89
+ initial: "Checking",
90
+ states: {
91
+ Checking,
92
+ ChangesRequested,
93
+ Approved: { schema: Approved, type: "final" }
94
+ }
95
+ },
96
+ Finished: { schema: WorkspaceFinished, type: "final" },
97
+ recent: { type: "history" },
98
+ exact: { type: "history", history: "deep" }
99
+ }
100
+ },
101
+ Paused,
102
+ Disabled,
103
+ Published: { schema: Published, type: "final", output: Schema.String }
104
+ }
102
105
  })
103
106
 
104
107
  const defaultWorkspaceSnapshot = () => ({
105
- path: "Workspace" as const,
106
- value: new Workspace({ revision: 0, preferredRoute: "draft" as const }),
108
+ path: "" as const,
109
+ value: undefined,
107
110
  state: {
108
- path: "Workspace.Draft" as const,
109
- value: new Draft({ text: "Recovered draft", autosaves: 0 })
111
+ path: "Workspace" as const,
112
+ value: new Workspace({ revision: 0, preferredRoute: "draft" as const }),
113
+ state: {
114
+ path: "Workspace.Draft" as const,
115
+ value: new Draft({ text: "Recovered draft", autosaves: 0 })
116
+ }
110
117
  }
111
118
  })
112
119
 
113
120
  export const transitionSemanticsMachine = Machine.make({
114
121
  id: "transition-semantics",
115
- states: TransitionStates.states,
116
- events: Machine.events(
122
+ root: TransitionStates,
123
+ events: Machine.eventsFromSchemas(
117
124
  Create,
118
125
  Edit,
119
126
  Save,
@@ -131,115 +138,120 @@ export const transitionSemanticsMachine = Machine.make({
131
138
  BumpWorkspace,
132
139
  Archive
133
140
  ),
134
- initial: (to) => to.Paused().resolve(({ target }) => target.decoded(new Paused({ reason: "not started" })))
141
+ initialConfiguration: (root) =>
142
+ root.resolve(({ target }) => target.from((to) => to.Paused.decoded(new Paused({ reason: "not started" }))))
135
143
  }).handle({
136
- Workspace: {
137
- history: {
138
- recent: { default: defaultWorkspaceSnapshot },
139
- exact: { default: defaultWorkspaceSnapshot }
140
- },
141
- on: {
142
- Pause: (to) =>
143
- to.full.Paused().resolve(({ event, target }) => target.decoded(new Paused({ reason: event.reason }))),
144
- BumpWorkspace: (to) =>
145
- to.branch.Workspace.update(({ current, owner }) =>
146
- owner.decoded(
147
- new Workspace({
148
- revision: current.revision + 1,
149
- preferredRoute: current.preferredRoute
150
- })
151
- )
152
- )
153
- },
154
- onDone: (to) =>
155
- to.full.Published().resolve(({ target }) => target.decoded(new Published({ result: "workspace published" }))),
156
- states: {
157
- Routing: {
158
- choice: (to) =>
159
- to.branches({
160
- draft: { title: "Preferred route is draft", target: to.local.Draft() },
161
- review: { title: "Preferred route is review", target: to.local.Review.initial }
162
- }).resolve(({ containingState, select }) =>
163
- containingState.preferredRoute === "review"
164
- ? select.review.decoded(new Review({ requestedBy: "initial route" }))
165
- : select.draft.decoded(new Draft({ text: "", autosaves: 0 }))
144
+ states: {
145
+ Workspace: {
146
+ history: {
147
+ recent: { default: defaultWorkspaceSnapshot },
148
+ exact: { default: defaultWorkspaceSnapshot }
149
+ },
150
+ on: {
151
+ Pause: (to) =>
152
+ to.branch.Paused().resolve(({ event, target }) => target.decoded(new Paused({ reason: event.reason }))),
153
+ BumpWorkspace: (to) =>
154
+ to.branch.Workspace.update.resolve(({ current, owner }) =>
155
+ owner.decoded(
156
+ new Workspace({
157
+ revision: current.revision + 1,
158
+ preferredRoute: current.preferredRoute
159
+ })
160
+ )
166
161
  )
167
162
  },
168
- Draft: {
169
- on: {
170
- Edit: (to) =>
171
- to.local.Draft().resolve(({ event, state, target }) =>
172
- target.decoded(new Draft({ text: event.text, autosaves: state.autosaves }))
173
- ),
174
- Save: (to) => to.local.AutoSaving().resolve(({ target }) => target.decoded(new AutoSaving({}))),
175
- Submit: (to) =>
163
+ onDone: (to) =>
164
+ to.branch.Published().resolve(({ target }) => target.decoded(new Published({ result: "workspace published" }))),
165
+ states: {
166
+ Routing: {
167
+ choice: (to) =>
176
168
  to.branches({
177
- review: { title: "Enter the review flow", target: to.local.Review.initial },
178
- publish: { title: "Publish without review", target: to.local.Finished() }
179
- }).resolve(({ event, select }) =>
180
- event.mode === "publish"
181
- ? select.publish.decoded(new WorkspaceFinished({ result: "published directly" }))
182
- : select.review.decoded(new Review({ requestedBy: event.requestedBy }))
169
+ draft: { title: "Preferred route is draft", target: to.local.Draft() },
170
+ review: { title: "Preferred route is review", target: to.local.Review.initial }
171
+ }).resolve(({ containingState, select }) =>
172
+ containingState.preferredRoute === "review"
173
+ ? select.review.decoded(new Review({ requestedBy: "initial route" }))
174
+ : select.draft.decoded(new Draft({ text: "", autosaves: 0 }))
175
+ )
176
+ },
177
+ Draft: {
178
+ on: {
179
+ Edit: (to) =>
180
+ to.local.Draft().resolve(({ event, state, target }) =>
181
+ target.decoded(new Draft({ text: event.text, autosaves: state.autosaves }))
182
+ ),
183
+ Save: (to) => to.local.AutoSaving().resolve(({ target }) => target.decoded(new AutoSaving({}))),
184
+ Submit: (to) =>
185
+ to.branches({
186
+ review: { title: "Enter the review flow", target: to.local.Review.initial },
187
+ publish: { title: "Publish without review", target: to.local.Finished() }
188
+ }).resolve(({ event, select }) =>
189
+ event.mode === "publish"
190
+ ? select.publish.decoded(new WorkspaceFinished({ result: "published directly" }))
191
+ : select.review.decoded(new Review({ requestedBy: event.requestedBy }))
192
+ ),
193
+ Refresh: (to) => to.none.resolve(() => undefined, { reenter: true }),
194
+ Ignore: (to) => to.none,
195
+ MaybeHandle: (to) =>
196
+ to.none.resolve(({ decline, event }) => event.accept ? undefined : decline(), {
197
+ declinable: true
198
+ })
199
+ }
200
+ },
201
+ AutoSaving: {
202
+ always: (to) =>
203
+ to.local.Draft().resolve(({ target }) =>
204
+ target.decoded(new Draft({ text: "Autosaved draft", autosaves: 1 }))
205
+ )
206
+ },
207
+ Review: {
208
+ initialize: ({ builder }) => builder.decoded(new Checking({ checks: ["types", "tests"] })),
209
+ onDone: (to) =>
210
+ to.branch.Workspace.Finished().resolve(({ target }) =>
211
+ target.decoded(new WorkspaceFinished({ result: "approved review" }))
183
212
  ),
184
- Refresh: (to) => to.none.resolve(() => undefined, { reenter: true }),
185
- Ignore: (to) => to.none,
186
- MaybeHandle: (to) =>
187
- to.none.resolve(({ decline, event }) => event.accept ? undefined : decline(), {
188
- declinable: true
189
- })
190
- }
191
- },
192
- AutoSaving: {
193
- always: (to) =>
194
- to.local.Draft().resolve(({ target }) => target.decoded(new Draft({ text: "Autosaved draft", autosaves: 1 })))
195
- },
196
- Review: {
197
- initialize: ({ builder }) => builder.decoded(new Checking({ checks: ["types", "tests"] })),
198
- onDone: (to) =>
199
- to.branch.Workspace.Finished().resolve(({ target }) =>
200
- target.decoded(new WorkspaceFinished({ result: "approved review" }))
201
- ),
202
- states: {
203
- Checking: {
204
- on: {
205
- Approve: (to) =>
206
- to.local.Approved().resolve(({ event, target }) =>
207
- target.decoded(new Approved({ reviewer: event.reviewer }))
208
- ),
209
- Reject: (to) =>
210
- to.local.ChangesRequested().resolve(({ event, target }) =>
211
- target.decoded(new ChangesRequested({ reason: event.reason }))
212
- )
213
- }
214
- },
215
- ChangesRequested: {
216
- on: {
217
- Revise: (to) =>
218
- to.branch.Workspace.Draft().resolve(({ target }) =>
219
- target.decoded(new Draft({ text: "Revised draft", autosaves: 0 }))
220
- )
213
+ states: {
214
+ Checking: {
215
+ on: {
216
+ Approve: (to) =>
217
+ to.local.Approved().resolve(({ event, target }) =>
218
+ target.decoded(new Approved({ reviewer: event.reviewer }))
219
+ ),
220
+ Reject: (to) =>
221
+ to.local.ChangesRequested().resolve(({ event, target }) =>
222
+ target.decoded(new ChangesRequested({ reason: event.reason }))
223
+ )
224
+ }
225
+ },
226
+ ChangesRequested: {
227
+ on: {
228
+ Revise: (to) =>
229
+ to.branch.Workspace.Draft().resolve(({ target }) =>
230
+ target.decoded(new Draft({ text: "Revised draft", autosaves: 0 }))
231
+ )
232
+ }
221
233
  }
222
234
  }
223
235
  }
224
236
  }
237
+ },
238
+ Paused: {
239
+ on: {
240
+ Create: (to) =>
241
+ to.branch.Workspace.initial.resolve(({ event, target }) =>
242
+ target.decoded(new Workspace({ revision: 0, preferredRoute: event.route }))
243
+ ),
244
+ ResumeShallow: (to) => to.history.Workspace.recent.resolve(({ target }) => target()),
245
+ ResumeDeep: (to) => to.history.Workspace.exact.resolve(({ target }) => target()),
246
+ Restart: (to) =>
247
+ to.branch.Workspace.initial.resolve(({ target }) =>
248
+ target.decoded(new Workspace({ revision: 0, preferredRoute: "draft" }))
249
+ )
250
+ }
251
+ },
252
+ Disabled: {},
253
+ Published: {
254
+ output: ({ state }) => state.result
225
255
  }
226
- },
227
- Paused: {
228
- on: {
229
- Create: (to) =>
230
- to.full.Workspace.initial.resolve(({ event, target }) =>
231
- target.decoded(new Workspace({ revision: 0, preferredRoute: event.route }))
232
- ),
233
- ResumeShallow: (to) => to.history.Workspace.recent.resolve(({ target }) => target()),
234
- ResumeDeep: (to) => to.history.Workspace.exact.resolve(({ target }) => target()),
235
- Restart: (to) =>
236
- to.full.Workspace.initial.resolve(({ target }) =>
237
- target.decoded(new Workspace({ revision: 0, preferredRoute: "draft" }))
238
- )
239
- }
240
- },
241
- Disabled: {},
242
- Published: {
243
- output: ({ state }) => state.result
244
256
  }
245
257
  })
@@ -55,7 +55,7 @@ export interface VisualizerModel {
55
55
  }
56
56
 
57
57
  export const stateLabel = (state: VisualizationState): string =>
58
- state.title === null ? state.key : `${state.title} (${state.key})`
58
+ state.title === null ? (state.path === "" ? "(root)" : state.key) : `${state.title} (${state.key})`
59
59
 
60
60
  export const triggerLabel = (transition: VisualizationTransition): string => {
61
61
  switch (transition.trigger.type) {
@@ -74,7 +74,7 @@ export const triggerLabel = (transition: VisualizationTransition): string => {
74
74
 
75
75
  const propertyAccess = (key: string): string => /^[$A-Z_a-z][$\w]*$/.test(key) ? `.${key}` : `[${JSON.stringify(key)}]`
76
76
 
77
- const pathAccess = (path: string): string => path.split(".").map(propertyAccess).join("")
77
+ const pathAccess = (path: string): string => path === "" ? "" : path.split(".").map(propertyAccess).join("")
78
78
 
79
79
  const nearestCompoundScope = (
80
80
  document: VisualizationDocument,
@@ -97,7 +97,7 @@ const localPathAccess = (
97
97
  const scope = nearestCompoundScope(document, source)
98
98
  if (scope === undefined) return undefined
99
99
  if (path === scope) return ""
100
- const prefix = `${scope}.`
100
+ const prefix = scope === "" ? "" : `${scope}.`
101
101
  return path.startsWith(prefix) ? pathAccess(path.slice(prefix.length)) : undefined
102
102
  }
103
103
 
@@ -138,13 +138,13 @@ export const branchTargetApi = (
138
138
  switch (selection.kind) {
139
139
  case "state":
140
140
  case "choice":
141
- api = `to.branch${pathAccess(path)}()`
141
+ api = path === "" ? "to.root()" : `to.branch${pathAccess(path)}()`
142
142
  break
143
143
  case "initial":
144
- api = `to.branch${pathAccess(path)}.initial`
144
+ api = path === "" ? "to.root.initial" : `to.branch${pathAccess(path)}.initial`
145
145
  break
146
146
  case "update":
147
- api = `to.branch${pathAccess(path)}.update`
147
+ api = path === "" ? "to.root.update" : `to.branch${pathAccess(path)}.update`
148
148
  break
149
149
  case "history":
150
150
  break
@@ -165,7 +165,7 @@ export const branchTargetApi = (
165
165
  break
166
166
  }
167
167
  } else if (selection.scope === "initial") {
168
- if (selection.kind === "state") api = `to${pathAccess(path)}()`
168
+ if (selection.kind === "state") api = path === "" ? "to" : `to${pathAccess(path)}()`
169
169
  if (selection.kind === "initial") api = `to${pathAccess(path)}.initial`
170
170
  }
171
171
 
@@ -1,5 +1,6 @@
1
1
  import { type ChokidarOptions, type FSWatcher, watch } from "chokidar"
2
2
  import * as Effect from "effect/Effect"
3
+ import type * as Scope from "effect/Scope"
3
4
  import * as Stream from "effect/Stream"
4
5
  import type { IncomingMessage, ServerResponse } from "node:http"
5
6
  import { isAbsolute, relative, resolve } from "node:path"
@@ -117,11 +118,12 @@ const acquire = (
117
118
  })
118
119
  })
119
120
 
120
- const acquireWatcher = (
121
+ export const acquireWatcher = (
121
122
  options: DevServer.Options,
122
123
  registry: MachineRegistry.MachineRegistry["Service"]
123
- ): Effect.Effect<FSWatcher> =>
124
- Effect.sync(() => {
124
+ ): Effect.Effect<FSWatcher, never, Scope.Scope> =>
125
+ Effect.gen(function*() {
126
+ const scope = yield* Effect.scope
125
127
  let refreshTimer: ReturnType<typeof setTimeout> | undefined
126
128
  const watcher = watch(options.root, watcherOptions(options))
127
129
  watcher.on("all", (_event, file) => {
@@ -130,16 +132,20 @@ const acquireWatcher = (
130
132
  refreshTimer = setTimeout(() => {
131
133
  Effect.runFork(
132
134
  registry.refresh.pipe(
133
- Effect.catch((cause) => Effect.logWarning("Machine reload failed", cause))
135
+ Effect.catch((cause) => Effect.logWarning("Machine reload failed", cause)),
136
+ Effect.forkIn(scope)
134
137
  )
135
138
  )
136
139
  }, options.debounce ?? 150)
137
140
  })
138
- watcher.on("close", () => {
139
- if (refreshTimer !== undefined) clearTimeout(refreshTimer)
140
- })
141
+ yield* Effect.addFinalizer(() =>
142
+ Effect.promise(async () => {
143
+ if (refreshTimer !== undefined) clearTimeout(refreshTimer)
144
+ await watcher.close()
145
+ })
146
+ )
141
147
  watcher.on("error", (cause) => {
142
- Effect.runFork(Effect.logError("Machine file watcher failed", cause))
148
+ Effect.runFork(Effect.logError("Machine file watcher failed", cause).pipe(Effect.forkIn(scope)))
143
149
  })
144
150
  return watcher
145
151
  })
@@ -166,10 +172,7 @@ export const run = (
166
172
  acquire(ErrorType, options, registry),
167
173
  (server) => Effect.promise(() => server.close())
168
174
  )
169
- yield* Effect.acquireRelease(
170
- acquireWatcher(options, registry),
171
- (watcher) => Effect.promise(() => watcher.close())
172
- )
175
+ yield* acquireWatcher(options, registry)
173
176
  const address = server.resolvedUrls?.local[0] ?? `http://${options.host}:${options.port}/`
174
177
  yield* Effect.logInfo(`Effect Machine visualizer: ${address}`)
175
178
  return yield* Effect.never
@@ -89,28 +89,28 @@ const make = (api: PublicApi, options: MachineRegistry.Options) =>
89
89
  results: []
90
90
  })
91
91
 
92
- const refresh = SubscriptionRef.get(state).pipe(
93
- Effect.flatMap((current) =>
94
- inspector.inspect({
95
- ...options,
96
- revision: current.revision + 1,
97
- retainedCandidates: retainedCandidates(current.results)
98
- }).pipe(
99
- Effect.map((results): MachineRegistry.Snapshot => ({
92
+ const refresh = SubscriptionRef.modifyEffect(state, (current) =>
93
+ inspector.inspect({
94
+ ...options,
95
+ revision: current.revision + 1,
96
+ retainedCandidates: retainedCandidates(current.results)
97
+ }).pipe(
98
+ Effect.map((results) => {
99
+ const next: MachineRegistry.Snapshot = {
100
100
  protocolVersion: DevToolsProtocol.protocolVersion,
101
101
  revision: current.revision + 1,
102
102
  results: reconcile(current.results, results)
103
- }))
104
- )
105
- ),
106
- Effect.tap((next) => SubscriptionRef.set(state, next)),
107
- Effect.mapError((cause) =>
108
- new api.RegistryError({
109
- message: `Could not inspect Effect Machine definitions under ${options.root}`,
110
- cause
103
+ }
104
+ return [next, next] as const
111
105
  })
106
+ )).pipe(
107
+ Effect.mapError((cause) =>
108
+ new api.RegistryError({
109
+ message: `Could not inspect Effect Machine definitions under ${options.root}`,
110
+ cause
111
+ })
112
+ )
112
113
  )
113
- )
114
114
 
115
115
  yield* refresh
116
116