@manifest-configuration-language/browser 0.1.6 → 0.2.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/README.md +6 -2
- package/dist/mcl.wasm +2 -2
- package/package.json +1 -1
- package/src/index.d.ts +68 -8
- package/src/index.js +309 -51
- package/src/limits.js +2 -0
- package/src/protocol.d.ts +44 -6
- package/src/worker.js +68 -8
package/README.md
CHANGED
|
@@ -21,13 +21,17 @@ try {
|
|
|
21
21
|
|
|
22
22
|
Call `runtime.warmUp()` to load the WebAssembly runtime before the first project operation. Pass `signal` and `timeoutMs` to warm-up or an operation to cancel it or set an upper bound for that call. Warm-up's timeout and `initializationTimeoutMs` bound worker startup; an operation's `timeoutMs` bounds execution after startup. Cancelling or timing out worker startup terminates that worker; the next queued operation can create a fresh worker. Cancelling or timing out an operation terminates its worker. Operations and warm-up are serialized because the current Go runtime uses one in-memory project root.
|
|
23
23
|
|
|
24
|
-
Project files can be UTF-8 strings or `Uint8Array` values. UTF-8 files remain inline in
|
|
24
|
+
Project files can be UTF-8 strings or `Uint8Array` values. UTF-8 files remain inline in project updates. The Browser SDK copies each changed `Uint8Array` and transfers its buffer to the Worker; the Worker sends only `{ "encoding": "binary" }` metadata in the update JSON and passes a path-to-`Uint8Array` map to the Go/WASM project session. The Go runtime copies those bytes into its bounded project filesystem. The SDK-owned and caller-owned arrays remain reusable after each update.
|
|
25
|
+
|
|
26
|
+
An open project is a Worker-backed session. Call `project.update(files)` with the complete next snapshot when project files change, then use the returned project handle for subsequent operations. The SDK validates changed files, sends only changed, added, and removed files to the Worker, and applies those deltas to a bounded in-memory Go/WASM filesystem. Each operation sends its entrypoint source and session ID; the project filesystem is not resent or rebuilt. If a canceled or timed-out operation terminates the Worker, the next operation restores the complete snapshot before running. Existing `runtime.parse`, `runtime.render`, and other source-based methods continue to use per-request files.
|
|
27
|
+
|
|
28
|
+
`runtime.render` returns a `sourceMap` alongside the YAML. Each entry identifies a YAML document, a JSON Pointer within that document, and whether the output range covers a key or value. Source ranges use zero-based UTF-16 line and character positions with an exclusive end, matching LSP and CodeMirror positions; the engine converts its internal UTF-8 byte offsets before returning them. Source paths are project-relative. The source map is limited by `maxSourceMapBytes`; `sourceMap.complete` is false when the map was truncated or an MCL source range could not be resolved.
|
|
25
29
|
|
|
26
30
|
`project.complete({ entrypoint, position })` returns the MCL language-service candidates for a zero-based UTF-16 position. Completion uses the same local project files as parsing and rendering, including schemas and Helm values schemas. It does not access a Kubernetes API server or any other network service.
|
|
27
31
|
|
|
28
32
|
`project.definition({ entrypoint, position })` returns a project-relative declaration path and zero-based UTF-16 range for local and imported MCL declarations. `project.hover({ entrypoint, position })` returns declaration syntax, including explicit type annotations and function or rule signatures. Hover does not infer runtime value types, and both operations use only project files already supplied to the browser runtime.
|
|
29
33
|
|
|
30
|
-
The package exports the completion, definition, and
|
|
34
|
+
The package exports the completion, definition, hover, rename, and source map result types, `maxSourceBytes`, `maxSourceMapBytes`, `maxRenameNameLength`, `maxProjectFiles`, `maxProjectFileBytes`, and `maxProjectBytes` so browser interfaces can consume the runtime's public types and limits. `isValidProjectFileSet(paths)` checks path validity, duplicate paths, and file/directory conflicts before a UI creates or renames project files. `rename()` returns source edits for local and non-exported top-level declarations within the active file; exported and cross-file symbols are left unchanged so the result never omits external references.
|
|
31
35
|
|
|
32
36
|
MCL decodes the request into a root-relative `io/fs` filesystem for project files, module imports, schemas, and dependency locks. Kubernetes resource scope checks use built-in metadata and CRDs in the project; unknown resource kinds can still be rendered. The runtime limits are defined in the [protocol schema](https://github.com/manifest-configuration-language/mcl/blob/main/cmd/mcl-wasm/protocol.schema.json). The worker blocks `fetch` after loading the WASM runtime assets, so MCL project evaluation cannot make external network requests.
|
|
33
37
|
|
package/dist/mcl.wasm
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: package/dist/mcl.wasm
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 96317496 bytes
|
|
4
|
+
sha256: 58f90cac0af9f6b4608f787528617729b99a6fcb65db52f168f95a1a6703e98f
|
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -14,21 +14,29 @@ import type {
|
|
|
14
14
|
ParseResult,
|
|
15
15
|
RelatedLocation as ProtocolRelatedLocation,
|
|
16
16
|
RenderResult,
|
|
17
|
+
RenameEdit as ProtocolRenameEdit,
|
|
18
|
+
RenameResult as ProtocolRenameResult,
|
|
17
19
|
Request as WasmRequest,
|
|
18
20
|
SourceRange as ProtocolSourceRange,
|
|
21
|
+
SourceMap as ProtocolSourceMap,
|
|
22
|
+
SourceMapEntry as ProtocolSourceMapEntry,
|
|
23
|
+
SourceMapOrigin as ProtocolSourceMapOrigin,
|
|
19
24
|
} from "./protocol.js";
|
|
20
25
|
|
|
21
26
|
export type MclOperation = WasmRequest["operation"];
|
|
22
27
|
|
|
23
28
|
export declare const maxSourceBytes: number;
|
|
29
|
+
export declare const maxSourceMapBytes: number;
|
|
30
|
+
export declare const maxRenameNameLength: number;
|
|
24
31
|
export declare const maxProjectFiles: number;
|
|
25
32
|
export declare const maxProjectFileBytes: number;
|
|
26
33
|
export declare const maxProjectBytes: number;
|
|
27
34
|
export declare const maxProjectPathLength: number;
|
|
28
35
|
export declare const maxProjectPathDepth: number;
|
|
29
36
|
export declare function isValidProjectPath(path: unknown): path is string;
|
|
37
|
+
export declare function isValidProjectFileSet(paths: readonly string[]): boolean;
|
|
30
38
|
|
|
31
|
-
/** Root-relative project files passed to MCL's
|
|
39
|
+
/** Root-relative project files passed to MCL's in-memory session filesystem. Uint8Array values are copied before transfer so callers can reuse them. */
|
|
32
40
|
export type ProjectFile = string | Uint8Array;
|
|
33
41
|
export type ProjectFiles = Readonly<Record<string, ProjectFile>>;
|
|
34
42
|
|
|
@@ -42,7 +50,12 @@ export type DefinitionResult = ProtocolDefinitionResult;
|
|
|
42
50
|
export type DefinitionLocation = ProtocolDefinitionLocation;
|
|
43
51
|
export type HoverResult = ProtocolHoverResult;
|
|
44
52
|
export type HoverInfo = ProtocolHoverInfo;
|
|
53
|
+
export type RenameEdit = ProtocolRenameEdit;
|
|
54
|
+
export type RenameResult = ProtocolRenameResult;
|
|
45
55
|
export type SourceRange = ProtocolSourceRange;
|
|
56
|
+
export type SourceMap = ProtocolSourceMap;
|
|
57
|
+
export type SourceMapEntry = ProtocolSourceMapEntry;
|
|
58
|
+
export type SourceMapOrigin = ProtocolSourceMapOrigin;
|
|
46
59
|
|
|
47
60
|
export type MclResult =
|
|
48
61
|
| ({ operation: "parse" } & ParseResult)
|
|
@@ -50,6 +63,7 @@ export type MclResult =
|
|
|
50
63
|
| ({ operation: "completion" } & ProtocolCompletionResult)
|
|
51
64
|
| ({ operation: "definition" } & ProtocolDefinitionResult)
|
|
52
65
|
| ({ operation: "hover" } & ProtocolHoverResult)
|
|
66
|
+
| ({ operation: "rename" } & ProtocolRenameResult)
|
|
53
67
|
| ({ operation: "format" } & FormatResult)
|
|
54
68
|
| ({ operation: "check" } & CheckResult)
|
|
55
69
|
| ({ operation: "evaluate" } & EvaluateResult)
|
|
@@ -67,22 +81,60 @@ export interface MclRuntime {
|
|
|
67
81
|
parse(source: string, options?: RequestOptions): Promise<MclResult & { operation: "parse" }>;
|
|
68
82
|
lint(source: string, options?: RequestOptions): Promise<MclResult & { operation: "lint" }>;
|
|
69
83
|
format(source: string, options?: RequestOptions): Promise<MclResult & { operation: "format" }>;
|
|
70
|
-
check(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
84
|
+
check(
|
|
85
|
+
source: string,
|
|
86
|
+
files?: ProjectFiles,
|
|
87
|
+
options?: RequestOptions,
|
|
88
|
+
): Promise<MclResult & { operation: "check" }>;
|
|
89
|
+
evaluate(
|
|
90
|
+
source: string,
|
|
91
|
+
files?: ProjectFiles,
|
|
92
|
+
options?: RequestOptions,
|
|
93
|
+
): Promise<MclResult & { operation: "evaluate" }>;
|
|
94
|
+
render(
|
|
95
|
+
source: string,
|
|
96
|
+
files?: ProjectFiles,
|
|
97
|
+
options?: RequestOptions,
|
|
98
|
+
): Promise<MclResult & { operation: "render" }>;
|
|
99
|
+
definition(
|
|
100
|
+
source: string,
|
|
101
|
+
options: PositionRequestOptions,
|
|
102
|
+
): Promise<MclResult & { operation: "definition" }>;
|
|
103
|
+
definition(
|
|
104
|
+
source: string,
|
|
105
|
+
files: ProjectFiles | undefined,
|
|
106
|
+
options: PositionRequestOptions,
|
|
107
|
+
): Promise<MclResult & { operation: "definition" }>;
|
|
108
|
+
hover(
|
|
109
|
+
source: string,
|
|
110
|
+
options: PositionRequestOptions,
|
|
111
|
+
): Promise<MclResult & { operation: "hover" }>;
|
|
112
|
+
hover(
|
|
113
|
+
source: string,
|
|
114
|
+
files: ProjectFiles | undefined,
|
|
115
|
+
options: PositionRequestOptions,
|
|
116
|
+
): Promise<MclResult & { operation: "hover" }>;
|
|
117
|
+
rename(
|
|
118
|
+
source: string,
|
|
119
|
+
options: RenameRequestOptions,
|
|
120
|
+
): Promise<MclResult & { operation: "rename" }>;
|
|
121
|
+
rename(
|
|
122
|
+
source: string,
|
|
123
|
+
files: ProjectFiles | undefined,
|
|
124
|
+
options: RenameRequestOptions,
|
|
125
|
+
): Promise<MclResult & { operation: "rename" }>;
|
|
77
126
|
dispose(): void;
|
|
78
127
|
}
|
|
79
128
|
|
|
80
129
|
export interface MclProject {
|
|
130
|
+
/** Returns a new complete snapshot handle for this Worker session. Only changed files are sent on the next operation. */
|
|
131
|
+
update(files: ProjectFiles): MclProject;
|
|
81
132
|
parse(options?: ProjectOperationOptions): Promise<MclResult & { operation: "parse" }>;
|
|
82
133
|
lint(options?: ProjectOperationOptions): Promise<MclResult & { operation: "lint" }>;
|
|
83
134
|
complete(options: ProjectCompletionOptions): Promise<MclResult & { operation: "completion" }>;
|
|
84
135
|
definition(options: ProjectNavigationOptions): Promise<MclResult & { operation: "definition" }>;
|
|
85
136
|
hover(options: ProjectNavigationOptions): Promise<MclResult & { operation: "hover" }>;
|
|
137
|
+
rename(options: ProjectRenameOptions): Promise<MclResult & { operation: "rename" }>;
|
|
86
138
|
format(options?: ProjectOperationOptions): Promise<MclResult & { operation: "format" }>;
|
|
87
139
|
check(options?: ProjectOperationOptions): Promise<MclResult & { operation: "check" }>;
|
|
88
140
|
evaluate(options?: ProjectOperationOptions): Promise<MclResult & { operation: "evaluate" }>;
|
|
@@ -106,11 +158,19 @@ export interface ProjectNavigationOptions extends ProjectOperationOptions {
|
|
|
106
158
|
position: CompletionPosition;
|
|
107
159
|
}
|
|
108
160
|
|
|
161
|
+
export interface ProjectRenameOptions extends ProjectNavigationOptions {
|
|
162
|
+
newName: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
109
165
|
export interface PositionRequestOptions extends RequestOptions {
|
|
110
166
|
entrypoint?: string;
|
|
111
167
|
position: CompletionPosition;
|
|
112
168
|
}
|
|
113
169
|
|
|
170
|
+
export interface RenameRequestOptions extends PositionRequestOptions {
|
|
171
|
+
newName: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
114
174
|
export declare class MclRuntimeError extends Error {
|
|
115
175
|
readonly code: string;
|
|
116
176
|
readonly path?: string;
|
package/src/index.js
CHANGED
|
@@ -4,14 +4,31 @@ import {
|
|
|
4
4
|
maxProjectFiles,
|
|
5
5
|
maxProjectPathDepth,
|
|
6
6
|
maxProjectPathLength,
|
|
7
|
+
maxRenameNameLength,
|
|
7
8
|
maxSourceBytes,
|
|
9
|
+
maxSourceMapBytes,
|
|
8
10
|
} from "./limits.js";
|
|
9
11
|
|
|
10
|
-
export {
|
|
12
|
+
export {
|
|
13
|
+
maxProjectBytes,
|
|
14
|
+
maxProjectFileBytes,
|
|
15
|
+
maxProjectFiles,
|
|
16
|
+
maxProjectPathDepth,
|
|
17
|
+
maxProjectPathLength,
|
|
18
|
+
maxRenameNameLength,
|
|
19
|
+
maxSourceBytes,
|
|
20
|
+
maxSourceMapBytes,
|
|
21
|
+
};
|
|
11
22
|
|
|
12
23
|
export function isValidProjectPath(value) {
|
|
13
|
-
if (
|
|
14
|
-
|
|
24
|
+
if (
|
|
25
|
+
typeof value !== "string" ||
|
|
26
|
+
value.length === 0 ||
|
|
27
|
+
value.startsWith("/") ||
|
|
28
|
+
/^[a-z]:/i.test(value) ||
|
|
29
|
+
value.includes("\\") ||
|
|
30
|
+
value.includes("\0")
|
|
31
|
+
) {
|
|
15
32
|
return false;
|
|
16
33
|
}
|
|
17
34
|
let pathLength = 0;
|
|
@@ -23,6 +40,25 @@ export function isValidProjectPath(value) {
|
|
|
23
40
|
return value.split("/").every((part) => part !== "" && part !== "." && part !== "..");
|
|
24
41
|
}
|
|
25
42
|
|
|
43
|
+
export function isValidProjectFileSet(paths) {
|
|
44
|
+
if (!Array.isArray(paths)) return false;
|
|
45
|
+
const files = new Set();
|
|
46
|
+
const directories = new Set();
|
|
47
|
+
for (const filePath of paths) {
|
|
48
|
+
if (!isValidProjectPath(filePath) || files.has(filePath) || directories.has(filePath))
|
|
49
|
+
return false;
|
|
50
|
+
let separator = filePath.indexOf("/");
|
|
51
|
+
while (separator !== -1) {
|
|
52
|
+
const directory = filePath.slice(0, separator);
|
|
53
|
+
if (files.has(directory)) return false;
|
|
54
|
+
directories.add(directory);
|
|
55
|
+
separator = filePath.indexOf("/", separator + 1);
|
|
56
|
+
}
|
|
57
|
+
files.add(filePath);
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
26
62
|
const defaultInitializationTimeoutMs = 30_000;
|
|
27
63
|
const defaultOperationTimeoutMs = 10_000;
|
|
28
64
|
const wasmURL = new URL("../dist/mcl.wasm", import.meta.url).href;
|
|
@@ -43,7 +79,9 @@ export class MclRuntimeError extends Error {
|
|
|
43
79
|
}
|
|
44
80
|
|
|
45
81
|
export function createMclRuntime(options = {}) {
|
|
46
|
-
const workerFactory =
|
|
82
|
+
const workerFactory =
|
|
83
|
+
options.workerFactory ??
|
|
84
|
+
(() => new Worker(new URL("./worker.js", import.meta.url), { type: "module" }));
|
|
47
85
|
const initializationTimeoutMs = options.initializationTimeoutMs ?? defaultInitializationTimeoutMs;
|
|
48
86
|
let worker;
|
|
49
87
|
let ready;
|
|
@@ -51,6 +89,7 @@ export function createMclRuntime(options = {}) {
|
|
|
51
89
|
let queue = Promise.resolve();
|
|
52
90
|
let sequence = 0;
|
|
53
91
|
let workerGeneration = 0;
|
|
92
|
+
let workerProject;
|
|
54
93
|
let stopCurrent;
|
|
55
94
|
let stopInitialization;
|
|
56
95
|
|
|
@@ -60,6 +99,7 @@ export function createMclRuntime(options = {}) {
|
|
|
60
99
|
const currentWorker = workerFactory();
|
|
61
100
|
worker = currentWorker;
|
|
62
101
|
workerGeneration++;
|
|
102
|
+
workerProject = undefined;
|
|
63
103
|
let resolveReady;
|
|
64
104
|
let rejectReady;
|
|
65
105
|
const initialization = new Promise((resolve, reject) => {
|
|
@@ -67,7 +107,10 @@ export function createMclRuntime(options = {}) {
|
|
|
67
107
|
rejectReady = reject;
|
|
68
108
|
});
|
|
69
109
|
ready = initialization;
|
|
70
|
-
const timer = setTimeout(
|
|
110
|
+
const timer = setTimeout(
|
|
111
|
+
() => fail(new Error("MCL WebAssembly initialization timed out")),
|
|
112
|
+
initializationTimeoutMs,
|
|
113
|
+
);
|
|
71
114
|
const onMessage = (event) => {
|
|
72
115
|
if (event.data?.type === "ready") {
|
|
73
116
|
clearTimeout(timer);
|
|
@@ -122,14 +165,111 @@ export function createMclRuntime(options = {}) {
|
|
|
122
165
|
}
|
|
123
166
|
}
|
|
124
167
|
|
|
125
|
-
async function
|
|
168
|
+
async function syncProjectToWorker(project, requestOptions, currentWorker, currentGeneration) {
|
|
169
|
+
const previous = workerProject;
|
|
170
|
+
const reset = !previous || previous.id !== project.id;
|
|
171
|
+
const previousFiles = reset ? Object.create(null) : previous.snapshot.files;
|
|
172
|
+
const updates = Object.create(null);
|
|
173
|
+
const binaryFiles = Object.create(null);
|
|
174
|
+
const transferables = [];
|
|
175
|
+
const deleted = [];
|
|
176
|
+
for (const [filePath, content] of Object.entries(project.snapshot.files)) {
|
|
177
|
+
if (
|
|
178
|
+
!reset &&
|
|
179
|
+
Object.hasOwn(previousFiles, filePath) &&
|
|
180
|
+
sameContent(previousFiles[filePath], content)
|
|
181
|
+
)
|
|
182
|
+
continue;
|
|
183
|
+
if (typeof content === "string") {
|
|
184
|
+
updates[filePath] = { encoding: "utf8", content };
|
|
185
|
+
} else {
|
|
186
|
+
const copy = new Uint8Array(content);
|
|
187
|
+
updates[filePath] = { encoding: "binary" };
|
|
188
|
+
binaryFiles[filePath] = copy;
|
|
189
|
+
transferables.push(copy.buffer);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (!reset) {
|
|
193
|
+
for (const filePath of Object.keys(previousFiles)) {
|
|
194
|
+
if (!Object.hasOwn(project.snapshot.files, filePath)) deleted.push(filePath);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (!reset && Object.keys(updates).length === 0 && deleted.length === 0) return;
|
|
198
|
+
|
|
199
|
+
const requestId = ++sequence;
|
|
200
|
+
const update = JSON.stringify({ reset, files: updates, deleted });
|
|
201
|
+
await new Promise((resolve, reject) => {
|
|
202
|
+
let settled = false;
|
|
203
|
+
const cleanup = () => {
|
|
204
|
+
currentWorker.removeEventListener("message", onMessage);
|
|
205
|
+
currentWorker.removeEventListener("error", onError);
|
|
206
|
+
requestOptions.signal?.removeEventListener("abort", onAbort);
|
|
207
|
+
};
|
|
208
|
+
const finish = (callback, value) => {
|
|
209
|
+
if (settled) return;
|
|
210
|
+
settled = true;
|
|
211
|
+
cleanup();
|
|
212
|
+
if (stopCurrent === stop) stopCurrent = undefined;
|
|
213
|
+
callback(value);
|
|
214
|
+
};
|
|
215
|
+
const onMessage = (event) => {
|
|
216
|
+
if (worker !== currentWorker || workerGeneration !== currentGeneration) return;
|
|
217
|
+
const response = event.data;
|
|
218
|
+
if (response?.type !== "project-updated" || response.requestId !== requestId) return;
|
|
219
|
+
if (!response.value.ok) finish(reject, new MclRuntimeError(response.value.error));
|
|
220
|
+
else {
|
|
221
|
+
workerProject = { id: project.id, snapshot: project.snapshot };
|
|
222
|
+
finish(resolve);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
const stop = (error) => {
|
|
226
|
+
finish(reject, error);
|
|
227
|
+
currentWorker.terminate();
|
|
228
|
+
if (worker === currentWorker) {
|
|
229
|
+
worker = undefined;
|
|
230
|
+
ready = undefined;
|
|
231
|
+
workerProject = undefined;
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
const onError = (event) => stop(new Error(event.message || "MCL worker failed"));
|
|
235
|
+
const onAbort = () => stop(new DOMException("The operation was aborted", "AbortError"));
|
|
236
|
+
currentWorker.addEventListener("message", onMessage);
|
|
237
|
+
currentWorker.addEventListener("error", onError);
|
|
238
|
+
requestOptions.signal?.addEventListener("abort", onAbort, { once: true });
|
|
239
|
+
stopCurrent = stop;
|
|
240
|
+
if (requestOptions.signal?.aborted) return onAbort();
|
|
241
|
+
try {
|
|
242
|
+
currentWorker.postMessage(
|
|
243
|
+
{ type: "project-update", requestId, projectId: project.id, update, binaryFiles },
|
|
244
|
+
transferables,
|
|
245
|
+
);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
stop(error);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function execute(
|
|
253
|
+
operation,
|
|
254
|
+
source,
|
|
255
|
+
files,
|
|
256
|
+
requestOptions = {},
|
|
257
|
+
entrypoint = "main.mcl",
|
|
258
|
+
position,
|
|
259
|
+
project,
|
|
260
|
+
) {
|
|
126
261
|
const signal = requestOptions.signal;
|
|
127
262
|
if (signal?.aborted) throw new DOMException("The operation was aborted", "AbortError");
|
|
128
263
|
const run = async () => {
|
|
129
264
|
if (disposed) throw new Error("MCL runtime has been disposed");
|
|
130
|
-
if (requestOptions.signal?.aborted)
|
|
131
|
-
|
|
132
|
-
const
|
|
265
|
+
if (requestOptions.signal?.aborted)
|
|
266
|
+
throw new DOMException("The operation was aborted", "AbortError");
|
|
267
|
+
const timeoutMs =
|
|
268
|
+
requestOptions.timeoutMs ?? options.operationTimeoutMs ?? defaultOperationTimeoutMs;
|
|
269
|
+
const timeoutError = new MclRuntimeError({
|
|
270
|
+
code: "MCL-WASM-TIMEOUT",
|
|
271
|
+
message: "MCL operation timed out",
|
|
272
|
+
});
|
|
133
273
|
let timer;
|
|
134
274
|
try {
|
|
135
275
|
if (timeoutMs <= 0) throw timeoutError;
|
|
@@ -138,11 +278,23 @@ export function createMclRuntime(options = {}) {
|
|
|
138
278
|
else stopInitialization?.(timeoutError);
|
|
139
279
|
}, timeoutMs);
|
|
140
280
|
await waitUntilReady(requestOptions.signal);
|
|
141
|
-
if (requestOptions.signal?.aborted)
|
|
142
|
-
|
|
143
|
-
const requestId = ++sequence;
|
|
281
|
+
if (requestOptions.signal?.aborted)
|
|
282
|
+
throw new DOMException("The operation was aborted", "AbortError");
|
|
144
283
|
const currentWorker = worker;
|
|
145
284
|
const currentGeneration = workerGeneration;
|
|
285
|
+
if (project) {
|
|
286
|
+
await syncProjectToWorker(
|
|
287
|
+
project,
|
|
288
|
+
requestOptions,
|
|
289
|
+
currentWorker,
|
|
290
|
+
currentGeneration,
|
|
291
|
+
timeoutError,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
const preparedFiles = project
|
|
295
|
+
? undefined
|
|
296
|
+
: prepareFilesForTransfer(source, files, entrypoint);
|
|
297
|
+
const requestId = ++sequence;
|
|
146
298
|
return await new Promise((resolve, reject) => {
|
|
147
299
|
let settled = false;
|
|
148
300
|
const cleanup = () => {
|
|
@@ -163,7 +315,8 @@ export function createMclRuntime(options = {}) {
|
|
|
163
315
|
const response = event.data;
|
|
164
316
|
if (response?.type !== "response" || response.requestId !== requestId) return;
|
|
165
317
|
if (!response.value.ok) finish(reject, new MclRuntimeError(response.value.error));
|
|
166
|
-
else if (operation === "lint")
|
|
318
|
+
else if (operation === "lint")
|
|
319
|
+
finish(resolve, { operation, diagnostics: response.value.result });
|
|
167
320
|
else finish(resolve, { operation, ...response.value.result });
|
|
168
321
|
};
|
|
169
322
|
const onError = (event) => stop(new Error(event.message || "MCL worker failed"));
|
|
@@ -181,7 +334,18 @@ export function createMclRuntime(options = {}) {
|
|
|
181
334
|
requestOptions.signal?.addEventListener("abort", onAbort, { once: true });
|
|
182
335
|
stopCurrent = stop;
|
|
183
336
|
try {
|
|
184
|
-
|
|
337
|
+
const message = {
|
|
338
|
+
type: "run",
|
|
339
|
+
requestId,
|
|
340
|
+
operation,
|
|
341
|
+
source,
|
|
342
|
+
entrypoint,
|
|
343
|
+
position,
|
|
344
|
+
newName: requestOptions.newName,
|
|
345
|
+
};
|
|
346
|
+
if (project) message.projectId = project.id;
|
|
347
|
+
else message.files = preparedFiles.files;
|
|
348
|
+
currentWorker.postMessage(message, preparedFiles?.transferables ?? []);
|
|
185
349
|
} catch (error) {
|
|
186
350
|
stop(error);
|
|
187
351
|
}
|
|
@@ -201,17 +365,23 @@ export function createMclRuntime(options = {}) {
|
|
|
201
365
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
202
366
|
if (signal.aborted) onAbort();
|
|
203
367
|
});
|
|
204
|
-
return Promise.race([result, aborted]).finally(() =>
|
|
368
|
+
return Promise.race([result, aborted]).finally(() =>
|
|
369
|
+
signal.removeEventListener("abort", onAbort),
|
|
370
|
+
);
|
|
205
371
|
}
|
|
206
372
|
|
|
207
373
|
function warmUp(requestOptions = {}) {
|
|
208
374
|
const signal = requestOptions.signal;
|
|
209
|
-
if (signal?.aborted)
|
|
375
|
+
if (signal?.aborted)
|
|
376
|
+
return Promise.reject(new DOMException("The operation was aborted", "AbortError"));
|
|
210
377
|
const run = async () => {
|
|
211
378
|
if (disposed) throw new Error("MCL runtime has been disposed");
|
|
212
379
|
if (signal?.aborted) throw new DOMException("The operation was aborted", "AbortError");
|
|
213
380
|
const timeoutMs = requestOptions.timeoutMs ?? initializationTimeoutMs;
|
|
214
|
-
const timeoutError = new MclRuntimeError({
|
|
381
|
+
const timeoutError = new MclRuntimeError({
|
|
382
|
+
code: "MCL-WASM-TIMEOUT",
|
|
383
|
+
message: "MCL WebAssembly initialization timed out",
|
|
384
|
+
});
|
|
215
385
|
let timer;
|
|
216
386
|
try {
|
|
217
387
|
if (timeoutMs <= 0) throw timeoutError;
|
|
@@ -230,48 +400,114 @@ export function createMclRuntime(options = {}) {
|
|
|
230
400
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
231
401
|
if (signal.aborted) onAbort();
|
|
232
402
|
});
|
|
233
|
-
return Promise.race([result, aborted]).finally(() =>
|
|
403
|
+
return Promise.race([result, aborted]).finally(() =>
|
|
404
|
+
signal.removeEventListener("abort", onAbort),
|
|
405
|
+
);
|
|
234
406
|
}
|
|
235
407
|
|
|
236
408
|
function openProject(files) {
|
|
409
|
+
const snapshot = createProjectSnapshot(files);
|
|
410
|
+
const id = `mcl-project-${++sequence}`;
|
|
411
|
+
return makeProject(id, snapshot);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function sameContent(left, right) {
|
|
415
|
+
if (left === right) return true;
|
|
416
|
+
if (typeof left !== typeof right) return false;
|
|
417
|
+
if (typeof left === "string") return left === right;
|
|
418
|
+
if (left.byteLength !== right.byteLength) return false;
|
|
419
|
+
for (let index = 0; index < left.byteLength; index++) {
|
|
420
|
+
if (left[index] !== right[index]) return false;
|
|
421
|
+
}
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function createProjectSnapshot(files, previous) {
|
|
237
426
|
const entries = Object.entries(files ?? {});
|
|
238
|
-
if (entries.length === 0)
|
|
239
|
-
|
|
427
|
+
if (entries.length === 0)
|
|
428
|
+
throw new TypeError("Project must contain at least one MCL source file");
|
|
429
|
+
if (entries.length > maxProjectFiles)
|
|
430
|
+
throw new RangeError(`Project has more than ${maxProjectFiles} files`);
|
|
431
|
+
if (!isValidProjectFileSet(entries.map(([filePath]) => filePath)))
|
|
432
|
+
throw new TypeError(
|
|
433
|
+
"Project file paths must be valid and cannot conflict with a file or directory path",
|
|
434
|
+
);
|
|
240
435
|
let totalBytes = 0;
|
|
241
|
-
// Project paths are user controlled; a null prototype keeps names such as
|
|
242
|
-
// `__proto__` as ordinary file paths instead of mutating the file map.
|
|
243
436
|
const projectFiles = Object.create(null);
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (typeof
|
|
247
|
-
|
|
248
|
-
|
|
437
|
+
const fileSizes = Object.create(null);
|
|
438
|
+
for (const [filePath, input] of entries) {
|
|
439
|
+
if (typeof input !== "string" && !(input instanceof Uint8Array))
|
|
440
|
+
throw new TypeError(`Project file ${filePath} must be a string or Uint8Array`);
|
|
441
|
+
const previousContent = previous?.files[filePath];
|
|
442
|
+
const unchanged = previousContent !== undefined && sameContent(previousContent, input);
|
|
443
|
+
const content = unchanged
|
|
444
|
+
? previousContent
|
|
445
|
+
: typeof input === "string"
|
|
446
|
+
? input
|
|
447
|
+
: new Uint8Array(input);
|
|
448
|
+
const fileBytes = unchanged
|
|
449
|
+
? previous.sizes[filePath]
|
|
450
|
+
: typeof content === "string"
|
|
451
|
+
? new TextEncoder().encode(content).byteLength
|
|
452
|
+
: content.byteLength;
|
|
453
|
+
if (!unchanged && fileBytes > maxProjectFileBytes)
|
|
454
|
+
throw new RangeError(
|
|
455
|
+
`Project file ${filePath} exceeds the ${maxProjectFileBytes}-byte limit`,
|
|
456
|
+
);
|
|
249
457
|
totalBytes += fileBytes;
|
|
250
|
-
if (totalBytes > maxProjectBytes)
|
|
251
|
-
|
|
458
|
+
if (totalBytes > maxProjectBytes)
|
|
459
|
+
throw new RangeError(`Project files exceed the ${maxProjectBytes}-byte limit`);
|
|
460
|
+
projectFiles[filePath] = content;
|
|
461
|
+
fileSizes[filePath] = fileBytes;
|
|
252
462
|
}
|
|
463
|
+
const sourcePaths = Object.keys(projectFiles)
|
|
464
|
+
.filter((filePath) => filePath.endsWith(".mcl"))
|
|
465
|
+
.sort();
|
|
466
|
+
if (sourcePaths.length === 0)
|
|
467
|
+
throw new TypeError("Project must contain at least one MCL source file");
|
|
468
|
+
const defaultEntrypoint = Object.hasOwn(projectFiles, "main.mcl")
|
|
469
|
+
? "main.mcl"
|
|
470
|
+
: sourcePaths.length === 1
|
|
471
|
+
? sourcePaths[0]
|
|
472
|
+
: undefined;
|
|
473
|
+
return Object.freeze({
|
|
474
|
+
files: Object.freeze(projectFiles),
|
|
475
|
+
sizes: Object.freeze(fileSizes),
|
|
476
|
+
defaultEntrypoint,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
253
479
|
|
|
254
|
-
|
|
255
|
-
const defaultEntrypoint = Object.hasOwn(projectFiles, "main.mcl") ? "main.mcl" : sourcePaths.length === 1 ? sourcePaths[0] : undefined;
|
|
480
|
+
function makeProject(id, snapshot) {
|
|
256
481
|
const runProjectOperation = async (operation, requestOptions = {}) => {
|
|
257
|
-
const entrypoint = requestOptions.entrypoint ?? defaultEntrypoint;
|
|
258
|
-
if (!entrypoint)
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
482
|
+
const entrypoint = requestOptions.entrypoint ?? snapshot.defaultEntrypoint;
|
|
483
|
+
if (!entrypoint)
|
|
484
|
+
throw new TypeError("Set entrypoint when the project has multiple MCL source files");
|
|
485
|
+
if (!isSafeRelativePath(entrypoint) || !Object.hasOwn(snapshot.files, entrypoint))
|
|
486
|
+
throw new TypeError(`Project entrypoint does not exist: ${entrypoint}`);
|
|
487
|
+
const entry = snapshot.files[entrypoint];
|
|
488
|
+
const source =
|
|
489
|
+
typeof entry === "string" ? entry : new TextDecoder("utf-8", { fatal: true }).decode(entry);
|
|
262
490
|
const sourceBytes = new TextEncoder().encode(source).byteLength;
|
|
263
|
-
if (sourceBytes > maxSourceBytes)
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
491
|
+
if (sourceBytes > maxSourceBytes)
|
|
492
|
+
throw new RangeError(`Source exceeds the ${maxSourceBytes}-byte limit`);
|
|
493
|
+
return execute(
|
|
494
|
+
operation,
|
|
495
|
+
source,
|
|
496
|
+
undefined,
|
|
497
|
+
requestOptions,
|
|
498
|
+
entrypoint,
|
|
499
|
+
requestOptions.position,
|
|
500
|
+
{ id, snapshot },
|
|
501
|
+
);
|
|
268
502
|
};
|
|
269
503
|
return Object.freeze({
|
|
504
|
+
update: (nextFiles) => makeProject(id, createProjectSnapshot(nextFiles, snapshot)),
|
|
270
505
|
parse: (requestOptions) => runProjectOperation("parse", requestOptions),
|
|
271
506
|
lint: (requestOptions) => runProjectOperation("lint", requestOptions),
|
|
272
507
|
complete: (requestOptions) => runProjectOperation("completion", requestOptions),
|
|
273
508
|
definition: (requestOptions) => runProjectOperation("definition", requestOptions),
|
|
274
509
|
hover: (requestOptions) => runProjectOperation("hover", requestOptions),
|
|
510
|
+
rename: (requestOptions) => runProjectOperation("rename", requestOptions),
|
|
275
511
|
format: (requestOptions) => runProjectOperation("format", requestOptions),
|
|
276
512
|
check: (requestOptions) => runProjectOperation("check", requestOptions),
|
|
277
513
|
evaluate: (requestOptions) => runProjectOperation("evaluate", requestOptions),
|
|
@@ -284,23 +520,33 @@ export function createMclRuntime(options = {}) {
|
|
|
284
520
|
}
|
|
285
521
|
|
|
286
522
|
function prepareFilesForTransfer(source, files, entrypoint) {
|
|
287
|
-
if (!isSafeRelativePath(entrypoint))
|
|
523
|
+
if (!isSafeRelativePath(entrypoint))
|
|
524
|
+
throw new TypeError(`Invalid project entrypoint: ${entrypoint}`);
|
|
288
525
|
const sourceBytes = new TextEncoder().encode(source).byteLength;
|
|
289
|
-
if (sourceBytes > maxSourceBytes)
|
|
526
|
+
if (sourceBytes > maxSourceBytes)
|
|
527
|
+
throw new RangeError(`Source exceeds the ${maxSourceBytes}-byte limit`);
|
|
290
528
|
const entries = Object.entries(files ?? {});
|
|
291
|
-
if (entries.length + 1 > maxProjectFiles)
|
|
529
|
+
if (entries.length + 1 > maxProjectFiles)
|
|
530
|
+
throw new RangeError(`Project has more than ${maxProjectFiles} files`);
|
|
292
531
|
let totalBytes = sourceBytes;
|
|
293
532
|
const transferredFiles = Object.create(null);
|
|
294
533
|
const transferables = [];
|
|
295
534
|
for (const [filePath, content] of entries) {
|
|
296
|
-
if (!isSafeRelativePath(filePath) || filePath === entrypoint)
|
|
535
|
+
if (!isSafeRelativePath(filePath) || filePath === entrypoint)
|
|
536
|
+
throw new TypeError(`Invalid project file path: ${filePath}`);
|
|
297
537
|
if (typeof content === "string") {
|
|
298
538
|
const fileBytes = new TextEncoder().encode(content).byteLength;
|
|
299
|
-
if (fileBytes > maxProjectFileBytes)
|
|
539
|
+
if (fileBytes > maxProjectFileBytes)
|
|
540
|
+
throw new RangeError(
|
|
541
|
+
`Project file ${filePath} exceeds the ${maxProjectFileBytes}-byte limit`,
|
|
542
|
+
);
|
|
300
543
|
totalBytes += fileBytes;
|
|
301
544
|
transferredFiles[filePath] = content;
|
|
302
545
|
} else if (content instanceof Uint8Array) {
|
|
303
|
-
if (content.byteLength > maxProjectFileBytes)
|
|
546
|
+
if (content.byteLength > maxProjectFileBytes)
|
|
547
|
+
throw new RangeError(
|
|
548
|
+
`Project file ${filePath} exceeds the ${maxProjectFileBytes}-byte limit`,
|
|
549
|
+
);
|
|
304
550
|
totalBytes += content.byteLength;
|
|
305
551
|
const copy = new Uint8Array(content);
|
|
306
552
|
transferredFiles[filePath] = copy;
|
|
@@ -308,7 +554,8 @@ export function createMclRuntime(options = {}) {
|
|
|
308
554
|
} else {
|
|
309
555
|
throw new TypeError(`Project file ${filePath} must be a string or Uint8Array`);
|
|
310
556
|
}
|
|
311
|
-
if (totalBytes > maxProjectBytes)
|
|
557
|
+
if (totalBytes > maxProjectBytes)
|
|
558
|
+
throw new RangeError(`Project files exceed the ${maxProjectBytes}-byte limit`);
|
|
312
559
|
}
|
|
313
560
|
return { files: transferredFiles, transferables };
|
|
314
561
|
}
|
|
@@ -317,7 +564,14 @@ export function createMclRuntime(options = {}) {
|
|
|
317
564
|
const files = requestOptions === undefined ? undefined : filesOrOptions;
|
|
318
565
|
const options = requestOptions ?? filesOrOptions;
|
|
319
566
|
if (!options?.position) throw new TypeError("A source position is required");
|
|
320
|
-
return execute(
|
|
567
|
+
return execute(
|
|
568
|
+
operation,
|
|
569
|
+
source,
|
|
570
|
+
files,
|
|
571
|
+
options,
|
|
572
|
+
options.entrypoint ?? "main.mcl",
|
|
573
|
+
options.position,
|
|
574
|
+
);
|
|
321
575
|
}
|
|
322
576
|
|
|
323
577
|
return {
|
|
@@ -329,8 +583,12 @@ export function createMclRuntime(options = {}) {
|
|
|
329
583
|
check: (source, files, requestOptions) => execute("check", source, files, requestOptions),
|
|
330
584
|
evaluate: (source, files, requestOptions) => execute("evaluate", source, files, requestOptions),
|
|
331
585
|
render: (source, files, requestOptions) => execute("render", source, files, requestOptions),
|
|
332
|
-
definition: (source, filesOrOptions, requestOptions) =>
|
|
333
|
-
|
|
586
|
+
definition: (source, filesOrOptions, requestOptions) =>
|
|
587
|
+
runSourceNavigation("definition", source, filesOrOptions, requestOptions),
|
|
588
|
+
hover: (source, filesOrOptions, requestOptions) =>
|
|
589
|
+
runSourceNavigation("hover", source, filesOrOptions, requestOptions),
|
|
590
|
+
rename: (source, filesOrOptions, requestOptions) =>
|
|
591
|
+
runSourceNavigation("rename", source, filesOrOptions, requestOptions),
|
|
334
592
|
dispose() {
|
|
335
593
|
if (disposed) return;
|
|
336
594
|
disposed = true;
|
package/src/limits.js
CHANGED
|
@@ -4,6 +4,8 @@ export const maxProjectFiles = 4096;
|
|
|
4
4
|
export const maxProjectFileBytes = 16777216;
|
|
5
5
|
export const maxProjectBytes = 67108864;
|
|
6
6
|
export const maxOutputBytes = 1048576;
|
|
7
|
+
export const maxSourceMapBytes = 1048576;
|
|
8
|
+
export const maxRenameNameLength = 256;
|
|
7
9
|
export const evaluationTimeoutMillis = 4000;
|
|
8
10
|
export const maxProjectPathLength = 512;
|
|
9
11
|
export const maxProjectPathDepth = 64;
|
package/src/protocol.d.ts
CHANGED
|
@@ -4,8 +4,10 @@ export type MCLWebAssemblyRequestAndResponseProtocol = Request | Response;
|
|
|
4
4
|
export type Request = {
|
|
5
5
|
requestId?: number;
|
|
6
6
|
revision?: number;
|
|
7
|
-
operation:
|
|
7
|
+
operation:
|
|
8
|
+
"parse" | "lint" | "format" | "check" | "evaluate" | "render" | "completion" | "definition" | "hover" | "rename";
|
|
8
9
|
position?: CompletionPosition;
|
|
10
|
+
newName?: string;
|
|
9
11
|
entrypoint?: ProjectPath;
|
|
10
12
|
/**
|
|
11
13
|
* UTF-8 entrypoint source, limited by the runtime.
|
|
@@ -30,6 +32,8 @@ export type FileInput = {
|
|
|
30
32
|
content?: string;
|
|
31
33
|
};
|
|
32
34
|
export type LintResult = LintDiagnostic[];
|
|
35
|
+
export type ProjectPath1 = ProjectPath2;
|
|
36
|
+
export type ProjectPath2 = string;
|
|
33
37
|
|
|
34
38
|
export interface CompletionPosition {
|
|
35
39
|
line: number;
|
|
@@ -48,7 +52,8 @@ export interface Response {
|
|
|
48
52
|
| RenderResult
|
|
49
53
|
| CompletionResult
|
|
50
54
|
| DefinitionResult
|
|
51
|
-
| HoverResult
|
|
55
|
+
| HoverResult
|
|
56
|
+
| RenameResult;
|
|
52
57
|
error?: Error;
|
|
53
58
|
}
|
|
54
59
|
export interface ParseResult {
|
|
@@ -78,6 +83,35 @@ export interface EvaluateResult {
|
|
|
78
83
|
export interface RenderResult {
|
|
79
84
|
yaml: string;
|
|
80
85
|
artifacts: number;
|
|
86
|
+
sourceMap: SourceMap;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Source map entries are limited by maxSourceMapBytes. Ranges use zero-based UTF-16 line and character positions with an exclusive end.
|
|
90
|
+
*/
|
|
91
|
+
export interface SourceMap {
|
|
92
|
+
complete: boolean;
|
|
93
|
+
entries: SourceMapEntry[];
|
|
94
|
+
}
|
|
95
|
+
export interface SourceMapEntry {
|
|
96
|
+
document: number;
|
|
97
|
+
/**
|
|
98
|
+
* JSON Pointer into the rendered YAML document.
|
|
99
|
+
*/
|
|
100
|
+
path: string;
|
|
101
|
+
kind: "key" | "value";
|
|
102
|
+
output: SourceRange;
|
|
103
|
+
sources: SourceMapOrigin[];
|
|
104
|
+
}
|
|
105
|
+
export interface SourceRange {
|
|
106
|
+
start: CompletionPosition;
|
|
107
|
+
end: CompletionPosition;
|
|
108
|
+
}
|
|
109
|
+
export interface SourceMapOrigin {
|
|
110
|
+
path: ProjectPath1;
|
|
111
|
+
range: SourceRange;
|
|
112
|
+
kind: "mcl" | "mcl-overlay-base" | "mcl-overlay-patch";
|
|
113
|
+
multiplicity: "unique" | "repeated" | "ambiguous";
|
|
114
|
+
occurrences: number;
|
|
81
115
|
}
|
|
82
116
|
export interface CompletionResult {
|
|
83
117
|
isIncomplete: boolean;
|
|
@@ -95,10 +129,6 @@ export interface DefinitionLocation {
|
|
|
95
129
|
path: string;
|
|
96
130
|
range: SourceRange;
|
|
97
131
|
}
|
|
98
|
-
export interface SourceRange {
|
|
99
|
-
start: CompletionPosition;
|
|
100
|
-
end: CompletionPosition;
|
|
101
|
-
}
|
|
102
132
|
export interface HoverResult {
|
|
103
133
|
hover: HoverInfo | null;
|
|
104
134
|
}
|
|
@@ -106,6 +136,14 @@ export interface HoverInfo {
|
|
|
106
136
|
contents: string;
|
|
107
137
|
range: SourceRange;
|
|
108
138
|
}
|
|
139
|
+
export interface RenameResult {
|
|
140
|
+
edits: RenameEdit[];
|
|
141
|
+
}
|
|
142
|
+
export interface RenameEdit {
|
|
143
|
+
path: ProjectPath1;
|
|
144
|
+
range: SourceRange;
|
|
145
|
+
newText: string;
|
|
146
|
+
}
|
|
109
147
|
export interface Error {
|
|
110
148
|
code: string;
|
|
111
149
|
message: string;
|
package/src/worker.js
CHANGED
|
@@ -21,7 +21,8 @@ const originalNetworkAPIs = new Map(
|
|
|
21
21
|
);
|
|
22
22
|
|
|
23
23
|
function denyNetworkAccess() {
|
|
24
|
-
globalThis.fetch = () =>
|
|
24
|
+
globalThis.fetch = () =>
|
|
25
|
+
Promise.reject(new TypeError("Network access is disabled in the MCL runtime"));
|
|
25
26
|
for (const [name] of originalNetworkAPIs) {
|
|
26
27
|
Object.defineProperty(globalThis, name, {
|
|
27
28
|
configurable: true,
|
|
@@ -43,16 +44,35 @@ async function probeNetworkAccess(url) {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
const availableAPIs = ["fetch", ...originalNetworkAPIs.keys()];
|
|
46
|
-
const blockedAPIs = availableAPIs.filter(
|
|
47
|
-
|
|
47
|
+
const blockedAPIs = availableAPIs.filter((name) =>
|
|
48
|
+
name === "fetch"
|
|
49
|
+
? globalThis.fetch !== originalFetch
|
|
50
|
+
: globalThis[name] !== originalNetworkAPIs.get(name),
|
|
48
51
|
);
|
|
49
52
|
return { fetchBlocked, availableAPIs, blockedAPIs };
|
|
50
53
|
}
|
|
51
54
|
|
|
55
|
+
function waitForWasmReady() {
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
Object.defineProperty(globalThis, "mclWasmReady", {
|
|
58
|
+
configurable: true,
|
|
59
|
+
set(value) {
|
|
60
|
+
Object.defineProperty(globalThis, "mclWasmReady", {
|
|
61
|
+
configurable: true,
|
|
62
|
+
writable: true,
|
|
63
|
+
value,
|
|
64
|
+
});
|
|
65
|
+
if (value === true) resolve();
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
52
71
|
async function initialize() {
|
|
53
72
|
if (initialized) return initialized;
|
|
54
73
|
initialized = (async () => {
|
|
55
|
-
if (!runtimeAssets?.wasmURL || !runtimeAssets?.wasmExecURL)
|
|
74
|
+
if (!runtimeAssets?.wasmURL || !runtimeAssets?.wasmExecURL)
|
|
75
|
+
throw new Error("MCL runtime assets were not configured");
|
|
56
76
|
await import(runtimeAssets.wasmExecURL);
|
|
57
77
|
const go = new Go();
|
|
58
78
|
const response = await runtimeFetch(runtimeAssets.wasmURL);
|
|
@@ -65,8 +85,13 @@ async function initialize() {
|
|
|
65
85
|
} catch {
|
|
66
86
|
({ instance } = await WebAssembly.instantiate(await response.arrayBuffer(), go.importObject));
|
|
67
87
|
}
|
|
68
|
-
|
|
69
|
-
|
|
88
|
+
const wasmReady = waitForWasmReady();
|
|
89
|
+
await Promise.race([
|
|
90
|
+
wasmReady,
|
|
91
|
+
go.run(instance).then(() => {
|
|
92
|
+
throw new Error("MCL WebAssembly exited before initialization completed");
|
|
93
|
+
}),
|
|
94
|
+
]);
|
|
70
95
|
postMessage({ type: "ready" });
|
|
71
96
|
})().catch((error) => {
|
|
72
97
|
postMessage({ type: "fatal", message: error instanceof Error ? error.message : String(error) });
|
|
@@ -105,6 +130,29 @@ self.addEventListener("message", async (event) => {
|
|
|
105
130
|
initialize().catch(() => {});
|
|
106
131
|
return;
|
|
107
132
|
}
|
|
133
|
+
if (message?.type === "project-update") {
|
|
134
|
+
try {
|
|
135
|
+
await initialize();
|
|
136
|
+
const binaryFiles = message.binaryFiles ?? Object.create(null);
|
|
137
|
+
const result = JSON.parse(
|
|
138
|
+
globalThis.mclWasm.updateProject(message.projectId, message.update, binaryFiles),
|
|
139
|
+
);
|
|
140
|
+
postMessage({ type: "project-updated", requestId: message.requestId, value: result });
|
|
141
|
+
} catch (error) {
|
|
142
|
+
postMessage({
|
|
143
|
+
type: "project-updated",
|
|
144
|
+
requestId: message.requestId,
|
|
145
|
+
value: {
|
|
146
|
+
ok: false,
|
|
147
|
+
error: {
|
|
148
|
+
code: "MCL-WASM-PROJECT",
|
|
149
|
+
message: error instanceof Error ? error.message : String(error),
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
108
156
|
if (message?.type !== "run") return;
|
|
109
157
|
try {
|
|
110
158
|
await initialize();
|
|
@@ -114,11 +162,20 @@ self.addEventListener("message", async (event) => {
|
|
|
114
162
|
revision: message.requestId,
|
|
115
163
|
operation: message.operation,
|
|
116
164
|
entrypoint: message.entrypoint,
|
|
165
|
+
newName: message.newName,
|
|
117
166
|
position: message.position,
|
|
118
167
|
source: message.source,
|
|
119
168
|
files: encodedFiles.files,
|
|
120
169
|
};
|
|
121
|
-
const value = JSON.parse(
|
|
170
|
+
const value = JSON.parse(
|
|
171
|
+
message.projectId === undefined
|
|
172
|
+
? globalThis.mclWasm.run(JSON.stringify(request), encodedFiles.binaryFiles)
|
|
173
|
+
: globalThis.mclWasm.run(
|
|
174
|
+
JSON.stringify(request),
|
|
175
|
+
encodedFiles.binaryFiles,
|
|
176
|
+
message.projectId,
|
|
177
|
+
),
|
|
178
|
+
);
|
|
122
179
|
postMessage({
|
|
123
180
|
type: "response",
|
|
124
181
|
requestId: message.requestId,
|
|
@@ -130,7 +187,10 @@ self.addEventListener("message", async (event) => {
|
|
|
130
187
|
requestId: message.requestId,
|
|
131
188
|
value: {
|
|
132
189
|
ok: false,
|
|
133
|
-
error: {
|
|
190
|
+
error: {
|
|
191
|
+
code: "MCL-WASM-INPUT",
|
|
192
|
+
message: error instanceof Error ? error.message : String(error),
|
|
193
|
+
},
|
|
134
194
|
},
|
|
135
195
|
});
|
|
136
196
|
}
|