@typeonce/effect-machine-devtools 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +3 -0
  3. package/README.md +81 -0
  4. package/dist/DevServer.d.ts +42 -0
  5. package/dist/DevServer.d.ts.map +1 -0
  6. package/dist/DevServer.js +28 -0
  7. package/dist/DevServer.js.map +1 -0
  8. package/dist/DevToolsProtocol.d.ts +954 -0
  9. package/dist/DevToolsProtocol.d.ts.map +1 -0
  10. package/dist/DevToolsProtocol.js +96 -0
  11. package/dist/DevToolsProtocol.js.map +1 -0
  12. package/dist/MachineDocument.d.ts +396 -0
  13. package/dist/MachineDocument.d.ts.map +1 -0
  14. package/dist/MachineDocument.js +172 -0
  15. package/dist/MachineDocument.js.map +1 -0
  16. package/dist/MachineRegistry.d.ts +349 -0
  17. package/dist/MachineRegistry.d.ts.map +1 -0
  18. package/dist/MachineRegistry.js +46 -0
  19. package/dist/MachineRegistry.js.map +1 -0
  20. package/dist/MachineSimulator.d.ts +169 -0
  21. package/dist/MachineSimulator.d.ts.map +1 -0
  22. package/dist/MachineSimulator.js +98 -0
  23. package/dist/MachineSimulator.js.map +1 -0
  24. package/dist/ProjectInspector.d.ts +116 -0
  25. package/dist/ProjectInspector.d.ts.map +1 -0
  26. package/dist/ProjectInspector.js +82 -0
  27. package/dist/ProjectInspector.js.map +1 -0
  28. package/dist/bin.d.ts +3 -0
  29. package/dist/bin.d.ts.map +1 -0
  30. package/dist/bin.js +23 -0
  31. package/dist/bin.js.map +1 -0
  32. package/dist/client/assets/index-BGOhE3Ng.css +1 -0
  33. package/dist/client/assets/index-DiqGhsGR.js +14 -0
  34. package/dist/client/index.html +14 -0
  35. package/dist/index.d.ts +18 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +18 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/internal/devServer.d.ts +9 -0
  40. package/dist/internal/devServer.d.ts.map +1 -0
  41. package/dist/internal/devServer.js +119 -0
  42. package/dist/internal/devServer.js.map +1 -0
  43. package/dist/internal/evaluationWorker.d.ts +3 -0
  44. package/dist/internal/evaluationWorker.d.ts.map +1 -0
  45. package/dist/internal/evaluationWorker.js +69 -0
  46. package/dist/internal/evaluationWorker.js.map +1 -0
  47. package/dist/internal/evaluationWorkerBootstrap.d.ts +2 -0
  48. package/dist/internal/evaluationWorkerBootstrap.d.ts.map +1 -0
  49. package/dist/internal/evaluationWorkerBootstrap.js +21 -0
  50. package/dist/internal/evaluationWorkerBootstrap.js.map +1 -0
  51. package/dist/internal/machineDocument.d.ts +4 -0
  52. package/dist/internal/machineDocument.d.ts.map +1 -0
  53. package/dist/internal/machineDocument.js +122 -0
  54. package/dist/internal/machineDocument.js.map +1 -0
  55. package/dist/internal/machineRegistry.d.ts +12 -0
  56. package/dist/internal/machineRegistry.d.ts.map +1 -0
  57. package/dist/internal/machineRegistry.js +97 -0
  58. package/dist/internal/machineRegistry.js.map +1 -0
  59. package/dist/internal/machineSimulator.d.ts +5 -0
  60. package/dist/internal/machineSimulator.d.ts.map +1 -0
  61. package/dist/internal/machineSimulator.js +156 -0
  62. package/dist/internal/machineSimulator.js.map +1 -0
  63. package/dist/internal/projectInspector.d.ts +11 -0
  64. package/dist/internal/projectInspector.d.ts.map +1 -0
  65. package/dist/internal/projectInspector.js +150 -0
  66. package/dist/internal/projectInspector.js.map +1 -0
  67. package/index.html +13 -0
  68. package/package.json +69 -0
  69. package/src/DevServer.ts +45 -0
  70. package/src/DevToolsProtocol.ts +146 -0
  71. package/src/MachineDocument.ts +264 -0
  72. package/src/MachineRegistry.ts +69 -0
  73. package/src/MachineSimulator.ts +154 -0
  74. package/src/ProjectInspector.ts +140 -0
  75. package/src/bin.ts +56 -0
  76. package/src/index.ts +20 -0
  77. package/src/internal/browser/example-machine.ts +134 -0
  78. package/src/internal/browser/machine-index.ts +81 -0
  79. package/src/internal/browser/main.ts +42 -0
  80. package/src/internal/browser/styles.css +817 -0
  81. package/src/internal/browser/text-tree.ts +214 -0
  82. package/src/internal/browser/visualizer-app.ts +659 -0
  83. package/src/internal/browser/visualizer-model.ts +173 -0
  84. package/src/internal/browser/visualizer.ts +31 -0
  85. package/src/internal/devServer.ts +171 -0
  86. package/src/internal/evaluationWorker.ts +138 -0
  87. package/src/internal/evaluationWorkerBootstrap.js +21 -0
  88. package/src/internal/machineDocument.ts +138 -0
  89. package/src/internal/machineRegistry.ts +128 -0
  90. package/src/internal/machineSimulator.ts +199 -0
  91. package/src/internal/projectInspector.ts +226 -0
@@ -0,0 +1,128 @@
1
+ import * as Effect from "effect/Effect"
2
+ import * as Layer from "effect/Layer"
3
+ import * as SubscriptionRef from "effect/SubscriptionRef"
4
+ import * as DevToolsProtocol from "../DevToolsProtocol.js"
5
+ import type * as MachineRegistry from "../MachineRegistry.js"
6
+ import * as ProjectInspector from "../ProjectInspector.js"
7
+
8
+ interface PublicApi {
9
+ readonly MachineRegistry: typeof MachineRegistry.MachineRegistry
10
+ readonly RegistryError: typeof MachineRegistry.RegistryError
11
+ }
12
+
13
+ const staleDiagnostic = (failed: DevToolsProtocol.Failed): DevToolsProtocol.Diagnostic => ({
14
+ severity: "warning",
15
+ code: "stale-document",
16
+ message: "Showing the last valid machine document while this module is incomplete.",
17
+ location: failed.source === null
18
+ ? null
19
+ : { file: failed.source.file, line: null, column: null },
20
+ statePath: null
21
+ })
22
+
23
+ export const reconcile = (
24
+ previous: ReadonlyArray<DevToolsProtocol.MachineResult>,
25
+ next: ReadonlyArray<DevToolsProtocol.MachineResult>
26
+ ): ReadonlyArray<DevToolsProtocol.MachineResult> => {
27
+ const validPrevious = previous.filter((result): result is DevToolsProtocol.Ready | DevToolsProtocol.Partial =>
28
+ result._tag === "Ready" || result._tag === "Partial"
29
+ )
30
+ const previousByKey = new Map(validPrevious.map((result) => [result.key, result]))
31
+ const previousByFile = new Map<string, Array<DevToolsProtocol.Ready | DevToolsProtocol.Partial>>()
32
+ for (const result of validPrevious) {
33
+ const file = result.document.source?.file
34
+ if (file === undefined) continue
35
+ const matches = previousByFile.get(file)
36
+ if (matches === undefined) previousByFile.set(file, [result])
37
+ else matches.push(result)
38
+ }
39
+
40
+ const emitted = new Set(next.filter((result) => result._tag !== "Failed").map((result) => result.key))
41
+ return next.flatMap((result): ReadonlyArray<DevToolsProtocol.MachineResult> => {
42
+ if (result._tag !== "Failed") return [result]
43
+
44
+ const moduleFailure = result.diagnostics.some((diagnostic) =>
45
+ diagnostic.code === "module-load-failed" || diagnostic.code === "machine-export-not-found"
46
+ )
47
+ const exact = previousByKey.get(result.key)
48
+ const candidates = moduleFailure || exact === undefined
49
+ ? previousByFile.get(result.source.file) ?? []
50
+ : [exact]
51
+ const retained = candidates.filter((candidate) => !emitted.has(candidate.key))
52
+ if (retained.length === 0) return [result]
53
+
54
+ for (const candidate of retained) emitted.add(candidate.key)
55
+ return retained.map((candidate): DevToolsProtocol.Partial => ({
56
+ _tag: "Partial",
57
+ protocolVersion: DevToolsProtocol.protocolVersion,
58
+ key: candidate.key,
59
+ document: candidate.document,
60
+ diagnostics: [staleDiagnostic(result), ...result.diagnostics]
61
+ }))
62
+ })
63
+ }
64
+
65
+ const retainedCandidates = (
66
+ results: ReadonlyArray<DevToolsProtocol.MachineResult>
67
+ ): ReadonlyArray<ProjectInspector.Candidate> => {
68
+ const exportsByFile = new Map<string, Set<string>>()
69
+ for (const result of results) {
70
+ if (result._tag !== "Ready" && result._tag !== "Partial") continue
71
+ const source = result.document.source
72
+ if (source === null) continue
73
+ const exportNames = exportsByFile.get(source.file) ?? new Set<string>()
74
+ if (source.exportName !== null) exportNames.add(source.exportName)
75
+ exportsByFile.set(source.file, exportNames)
76
+ }
77
+ return [...exportsByFile].map(([file, exportNames]) => ({
78
+ file,
79
+ exportNames: [...exportNames].sort()
80
+ }))
81
+ }
82
+
83
+ const make = (api: PublicApi, options: MachineRegistry.Options) =>
84
+ Effect.gen(function*() {
85
+ const inspector = yield* ProjectInspector.ProjectInspector
86
+ const state = yield* SubscriptionRef.make<MachineRegistry.Snapshot>({
87
+ protocolVersion: DevToolsProtocol.protocolVersion,
88
+ revision: 0,
89
+ results: []
90
+ })
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 => ({
100
+ protocolVersion: DevToolsProtocol.protocolVersion,
101
+ revision: current.revision + 1,
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
111
+ })
112
+ )
113
+ )
114
+
115
+ yield* refresh
116
+
117
+ return api.MachineRegistry.of({
118
+ get: SubscriptionRef.get(state),
119
+ changes: SubscriptionRef.changes(state),
120
+ refresh
121
+ })
122
+ })
123
+
124
+ export const layer = (
125
+ api: PublicApi,
126
+ options: MachineRegistry.Options
127
+ ): Layer.Layer<MachineRegistry.MachineRegistry, MachineRegistry.RegistryError, ProjectInspector.ProjectInspector> =>
128
+ Layer.effect(api.MachineRegistry, make(api, options))
@@ -0,0 +1,199 @@
1
+ import type * as MachineDocument from "../MachineDocument.js"
2
+ import type * as MachineSimulator from "../MachineSimulator.js"
3
+
4
+ type State = MachineDocument.State
5
+ type Transition = MachineDocument.Transition
6
+
7
+ interface Model {
8
+ readonly states: ReadonlyMap<string, State>
9
+ readonly order: ReadonlyMap<string, number>
10
+ }
11
+
12
+ const makeModel = (document: MachineDocument.MachineDocument): Model => ({
13
+ states: new Map(document.states.map((state) => [state.path, state])),
14
+ order: new Map(document.states.map((state) => [state.path, state.order]))
15
+ })
16
+
17
+ const ordered = (model: Model, paths: Iterable<string>): ReadonlyArray<string> =>
18
+ [...new Set(paths)].sort((left, right) => (model.order.get(left) ?? 0) - (model.order.get(right) ?? 0))
19
+
20
+ const enter = (model: Model, path: string, active: Set<string>): void => {
21
+ const state = model.states.get(path)
22
+ if (state === undefined) return
23
+ active.add(path)
24
+ if (state.type === "parallel") {
25
+ state.children.forEach((child) => enter(model, child, active))
26
+ } else if (state.type === "compound" && state.initial !== null) {
27
+ enter(model, state.initial, active)
28
+ }
29
+ }
30
+
31
+ const ancestors = (model: Model, path: string): ReadonlyArray<string> => {
32
+ const result: Array<string> = []
33
+ let current = model.states.get(path)
34
+ while (current !== undefined) {
35
+ result.unshift(current.path)
36
+ current = current.parent === null ? undefined : model.states.get(current.parent)
37
+ }
38
+ return result
39
+ }
40
+
41
+ const leastCommonAncestor = (model: Model, left: string, right: string): string | null => {
42
+ const leftAncestors = ancestors(model, left)
43
+ const rightAncestors = ancestors(model, right)
44
+ let result: string | null = null
45
+ const length = Math.min(leftAncestors.length, rightAncestors.length)
46
+ for (let index = 0; index < length; index++) {
47
+ if (leftAncestors[index] !== rightAncestors[index]) break
48
+ result = leftAncestors[index] ?? null
49
+ }
50
+ return result
51
+ }
52
+
53
+ const isDescendant = (model: Model, path: string, ancestor: string): boolean =>
54
+ path !== ancestor && ancestors(model, path).includes(ancestor)
55
+
56
+ const candidateEvents = (
57
+ document: MachineDocument.MachineDocument,
58
+ activePaths: ReadonlyArray<string>
59
+ ): ReadonlyArray<string> => {
60
+ const active = new Set(activePaths)
61
+ return [
62
+ ...new Set(
63
+ document.transitions.flatMap((transition): ReadonlyArray<string> =>
64
+ transition.trigger.type === "event" && active.has(transition.source) ? [transition.trigger.event] : []
65
+ )
66
+ )
67
+ ].sort()
68
+ }
69
+
70
+ const snapshot = (
71
+ document: MachineDocument.MachineDocument,
72
+ model: Model,
73
+ step: number,
74
+ activePaths: Iterable<string>
75
+ ): MachineSimulator.Snapshot => {
76
+ const active = ordered(model, activePaths)
77
+ return {
78
+ step,
79
+ activePaths: active,
80
+ candidateEvents: candidateEvents(document, active)
81
+ }
82
+ }
83
+
84
+ export const start = (document: MachineDocument.MachineDocument): MachineSimulator.Session => {
85
+ const model = makeModel(document)
86
+ const active = new Set<string>()
87
+ if (document.snapshot === null) {
88
+ const targetAncestors = ancestors(model, document.initial.target)
89
+ targetAncestors.forEach((path) => active.add(path))
90
+ enter(model, document.initial.target, active)
91
+ } else {
92
+ document.snapshot.activePaths.forEach((path) => active.add(path))
93
+ }
94
+ return { document, snapshot: snapshot(document, model, 0, active) }
95
+ }
96
+
97
+ const indeterminate = (
98
+ session: MachineSimulator.Session,
99
+ event: string,
100
+ transitions: ReadonlyArray<Transition>,
101
+ reason: MachineSimulator.Indeterminate["reason"]
102
+ ): MachineSimulator.Indeterminate => ({
103
+ _tag: "Indeterminate",
104
+ event,
105
+ transitionIds: transitions.map((transition) => transition.id),
106
+ session: session.snapshot,
107
+ reason
108
+ })
109
+
110
+ const notesFor = (
111
+ document: MachineDocument.MachineDocument,
112
+ transition: Transition,
113
+ nextActive: ReadonlyArray<string>
114
+ ): ReadonlyArray<MachineSimulator.Note> => {
115
+ const notes = new Set<MachineSimulator.Note>(["runtime-effects-skipped"])
116
+ if (transition.reenter) notes.add("reentry-lifecycles-skipped")
117
+ if (transition.branches.some((branch) => branch.updates.length > 0)) notes.add("state-updates-skipped")
118
+ const active = new Set(nextActive)
119
+ if (
120
+ document.transitions.some((candidate) =>
121
+ active.has(candidate.source) &&
122
+ (candidate.trigger.type === "always" || candidate.trigger.type === "choice" || candidate.trigger.type === "done")
123
+ )
124
+ ) {
125
+ notes.add("automatic-transitions-skipped")
126
+ }
127
+ return [...notes]
128
+ }
129
+
130
+ const nextConfiguration = (
131
+ session: MachineSimulator.Session,
132
+ transition: Transition,
133
+ target: string | null
134
+ ): MachineSimulator.Session | undefined => {
135
+ const document = session.document
136
+ const model = makeModel(document)
137
+ if (target === null) {
138
+ return {
139
+ document,
140
+ snapshot: snapshot(document, model, session.snapshot.step + 1, session.snapshot.activePaths)
141
+ }
142
+ }
143
+ if (!model.states.has(target)) return undefined
144
+
145
+ const naturalBoundary = leastCommonAncestor(model, transition.source, target)
146
+ const sourceParent = model.states.get(transition.source)?.parent ?? null
147
+ const boundary = transition.reenter
148
+ ? sourceParent === null || naturalBoundary === null
149
+ ? null
150
+ : ancestors(model, naturalBoundary).length <= ancestors(model, sourceParent).length
151
+ ? naturalBoundary
152
+ : sourceParent
153
+ : naturalBoundary
154
+ const active = new Set(session.snapshot.activePaths)
155
+ for (const path of active) {
156
+ if (boundary === null || isDescendant(model, path, boundary)) active.delete(path)
157
+ }
158
+ ancestors(model, target).forEach((path) => {
159
+ if (boundary === null || path === boundary || isDescendant(model, path, boundary)) active.add(path)
160
+ })
161
+ enter(model, target, active)
162
+ return { document, snapshot: snapshot(document, model, session.snapshot.step + 1, active) }
163
+ }
164
+
165
+ export const send = (session: MachineSimulator.Session, event: string): MachineSimulator.StepResult => {
166
+ const active = new Set(session.snapshot.activePaths)
167
+ const transitions = session.document.transitions.filter((transition) =>
168
+ transition.trigger.type === "event" && transition.trigger.event === event && active.has(transition.source)
169
+ )
170
+ if (transitions.length === 0) {
171
+ return {
172
+ _tag: "Blocked",
173
+ event,
174
+ transitionIds: [],
175
+ session: session.snapshot,
176
+ reason: "event-not-enabled"
177
+ }
178
+ }
179
+ if (transitions.length > 1) return indeterminate(session, event, transitions, "multiple-transitions")
180
+ const transition = transitions[0]!
181
+ if (transition.acceptance === "declinable") {
182
+ return indeterminate(session, event, transitions, "declinable-transition")
183
+ }
184
+ if (transition.branches.length !== 1 || transition.branches[0]?.type === "branch") {
185
+ return indeterminate(session, event, transitions, "conditional-branches")
186
+ }
187
+ const branch = transition.branches[0]!
188
+ if (branch.selection.kind === "history") return indeterminate(session, event, transitions, "history-target")
189
+ if (branch.selection.kind === "choice") return indeterminate(session, event, transitions, "choice-target")
190
+ const next = nextConfiguration(session, transition, branch.target)
191
+ if (next === undefined) return indeterminate(session, event, transitions, "missing-target")
192
+ return {
193
+ _tag: "Applied",
194
+ event,
195
+ transitionIds: [transition.id],
196
+ session: next.snapshot,
197
+ notes: notesFor(session.document, transition, next.snapshot.activePaths)
198
+ }
199
+ }
@@ -0,0 +1,226 @@
1
+ import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem"
2
+ import * as NodePath from "@effect/platform-node/NodePath"
3
+ import * as NodeWorker from "@effect/platform-node/NodeWorker"
4
+ import * as Deferred from "effect/Deferred"
5
+ import * as Effect from "effect/Effect"
6
+ import * as Fiber from "effect/Fiber"
7
+ import * as FileSystem from "effect/FileSystem"
8
+ import * as Layer from "effect/Layer"
9
+ import * as Path from "effect/Path"
10
+ import * as Schema from "effect/Schema"
11
+ import * as Worker from "effect/unstable/workers/Worker"
12
+ import { Worker as NodeWorkerThread } from "node:worker_threads"
13
+ import ts from "typescript"
14
+ import * as DevToolsProtocol from "../DevToolsProtocol.js"
15
+ import type * as ProjectInspector from "../ProjectInspector.js"
16
+
17
+ const defaultInclude = "**/src/**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"
18
+
19
+ const defaultExclude = [
20
+ "**/node_modules/**",
21
+ "**/.git/**",
22
+ "**/dist/**",
23
+ "**/build/**",
24
+ "**/coverage/**",
25
+ "**/.data/**",
26
+ "**/references/**",
27
+ "**/*.d.ts"
28
+ ] as const
29
+
30
+ interface EvaluationRequest {
31
+ readonly root: string
32
+ readonly revision: number
33
+ readonly candidates: ReadonlyArray<ProjectInspector.Candidate>
34
+ }
35
+
36
+ interface EvaluationResponse {
37
+ readonly results: ReadonlyArray<unknown>
38
+ }
39
+
40
+ const scriptKind = (file: string): ts.ScriptKind => {
41
+ if (file.endsWith(".tsx")) return ts.ScriptKind.TSX
42
+ if (file.endsWith(".jsx")) return ts.ScriptKind.JSX
43
+ if (file.endsWith(".js") || file.endsWith(".mjs") || file.endsWith(".cjs")) return ts.ScriptKind.JS
44
+ return ts.ScriptKind.TS
45
+ }
46
+
47
+ const hasExportModifier = (node: ts.Node): boolean =>
48
+ ts.canHaveModifiers(node) &&
49
+ ts.getModifiers(node)?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) === true
50
+
51
+ const containsHandleCall = (node: ts.Node): boolean => {
52
+ let found = false
53
+ const visit = (current: ts.Node): void => {
54
+ if (found) return
55
+ if (
56
+ ts.isCallExpression(current) &&
57
+ ((ts.isPropertyAccessExpression(current.expression) && current.expression.name.text === "handle") ||
58
+ (ts.isElementAccessExpression(current.expression) &&
59
+ ts.isStringLiteral(current.expression.argumentExpression) &&
60
+ current.expression.argumentExpression.text === "handle"))
61
+ ) {
62
+ found = true
63
+ return
64
+ }
65
+ ts.forEachChild(current, visit)
66
+ }
67
+ visit(node)
68
+ return found
69
+ }
70
+
71
+ export const parseCandidate = (file: string, source: string): ProjectInspector.Candidate | undefined => {
72
+ const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, scriptKind(file))
73
+ if (!containsHandleCall(sourceFile)) return undefined
74
+
75
+ const exportNames = new Set<string>()
76
+ for (const statement of sourceFile.statements) {
77
+ if (ts.isExportAssignment(statement) && containsHandleCall(statement.expression)) {
78
+ exportNames.add("default")
79
+ continue
80
+ }
81
+ if (ts.isVariableStatement(statement) && hasExportModifier(statement)) {
82
+ for (const declaration of statement.declarationList.declarations) {
83
+ if (
84
+ ts.isIdentifier(declaration.name) && declaration.initializer && containsHandleCall(declaration.initializer)
85
+ ) {
86
+ exportNames.add(declaration.name.text)
87
+ }
88
+ }
89
+ }
90
+ }
91
+
92
+ return {
93
+ file,
94
+ exportNames: [...exportNames].sort()
95
+ }
96
+ }
97
+
98
+ const hasSyntacticErrors = (file: string, source: string): boolean =>
99
+ ts.transpileModule(source, {
100
+ fileName: file,
101
+ reportDiagnostics: true,
102
+ compilerOptions: {
103
+ allowJs: true,
104
+ jsx: ts.JsxEmit.Preserve,
105
+ target: ts.ScriptTarget.Latest
106
+ }
107
+ }).diagnostics?.some((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error) === true
108
+
109
+ interface PublicApi {
110
+ readonly ProjectInspector: typeof ProjectInspector.ProjectInspector
111
+ readonly DiscoveryError: typeof ProjectInspector.DiscoveryError
112
+ readonly EvaluationError: typeof ProjectInspector.EvaluationError
113
+ }
114
+
115
+ const makeDiscovery = (api: PublicApi) =>
116
+ Effect.gen(function*() {
117
+ const fs = yield* FileSystem.FileSystem
118
+ const path = yield* Path.Path
119
+
120
+ return (options: ProjectInspector.InspectOptions): Effect.Effect<
121
+ ReadonlyArray<ProjectInspector.Candidate>,
122
+ ProjectInspector.DiscoveryError
123
+ > =>
124
+ Effect.gen(function*() {
125
+ const root = path.resolve(options.root)
126
+ const retainedByFile = new Map(
127
+ (options.retainedCandidates ?? []).map((candidate) => [candidate.file, candidate])
128
+ )
129
+ const files = yield* fs.glob(options.include ?? defaultInclude, {
130
+ root,
131
+ exclude: [...defaultExclude, ...(options.exclude ?? [])]
132
+ })
133
+ const candidates = yield* Effect.forEach(
134
+ files.sort(),
135
+ (file) =>
136
+ fs.readFileString(path.join(root, file)).pipe(
137
+ Effect.map((source) => {
138
+ const candidate = parseCandidate(file, source)
139
+ if (candidate !== undefined) return candidate
140
+ const retained = retainedByFile.get(file)
141
+ return retained !== undefined && hasSyntacticErrors(file, source) ? retained : undefined
142
+ })
143
+ ),
144
+ { concurrency: "unbounded" }
145
+ )
146
+ return candidates.filter((candidate): candidate is ProjectInspector.Candidate => candidate !== undefined)
147
+ }).pipe(
148
+ Effect.mapError((cause) =>
149
+ new api.DiscoveryError({
150
+ message: `Could not discover Effect Machine definitions under ${options.root}`,
151
+ cause
152
+ })
153
+ )
154
+ )
155
+ })
156
+
157
+ const workerBootstrap = new URL("./evaluationWorkerBootstrap.js", import.meta.url)
158
+ const workerRuntime = new URL(
159
+ import.meta.url.endsWith(".ts") ? "./evaluationWorker.ts" : "./evaluationWorker.js",
160
+ import.meta.url
161
+ )
162
+
163
+ const WorkerLayer = NodeWorker.layer(() =>
164
+ new NodeWorkerThread(workerBootstrap, {
165
+ execArgv: [],
166
+ workerData: {
167
+ runtimeModuleUrl: workerRuntime.href
168
+ }
169
+ })
170
+ )
171
+
172
+ const evaluate = (
173
+ api: PublicApi,
174
+ candidates: ReadonlyArray<ProjectInspector.Candidate>,
175
+ options: ProjectInspector.InspectOptions
176
+ ): Effect.Effect<ReadonlyArray<DevToolsProtocol.MachineResult>, ProjectInspector.EvaluationError> => {
177
+ if (candidates.length === 0) return Effect.succeed([])
178
+
179
+ const request: EvaluationRequest = {
180
+ root: options.root,
181
+ revision: options.revision ?? 0,
182
+ candidates
183
+ }
184
+
185
+ return Effect.scoped(
186
+ Effect.gen(function*() {
187
+ const platform = yield* Worker.WorkerPlatform
188
+ const worker = yield* platform.spawn<EvaluationResponse, EvaluationRequest>(0)
189
+ const response = yield* Deferred.make<EvaluationResponse>()
190
+ const runner = yield* Effect.forkScoped(
191
+ worker.run((message) => Deferred.succeed(response, message))
192
+ )
193
+ yield* worker.send(request)
194
+ const message = yield* Effect.raceFirst(Deferred.await(response), Fiber.join(runner))
195
+ return yield* Effect.forEach(
196
+ message.results,
197
+ (result) => Schema.decodeUnknownEffect(DevToolsProtocol.MachineResult)(result)
198
+ )
199
+ })
200
+ ).pipe(
201
+ Effect.provide(WorkerLayer),
202
+ Effect.mapError((cause) =>
203
+ new api.EvaluationError({
204
+ message: "The isolated machine evaluator failed",
205
+ cause
206
+ })
207
+ )
208
+ )
209
+ }
210
+
211
+ const make = (api: PublicApi) =>
212
+ Effect.gen(function*() {
213
+ const discover = yield* makeDiscovery(api)
214
+ const inspect = (options: ProjectInspector.InspectOptions) =>
215
+ Effect.flatMap(discover(options), (candidates) => evaluate(api, candidates, options))
216
+ return api.ProjectInspector.of({
217
+ discover,
218
+ evaluate: (candidates, options) => evaluate(api, candidates, options),
219
+ inspect
220
+ })
221
+ })
222
+
223
+ const PlatformLayer = Layer.merge(NodeFileSystem.layer, NodePath.layer)
224
+
225
+ export const layer = (api: PublicApi): Layer.Layer<ProjectInspector.ProjectInspector> =>
226
+ Layer.effect(api.ProjectInspector, make(api)).pipe(Layer.provide(PlatformLayer))