@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 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 the JSON request. The Browser SDK copies each `Uint8Array` and transfers its buffer to the Worker; the Worker sends only `{ "encoding": "binary" }` metadata in the request JSON and passes a path-to-`Uint8Array` map as the second argument to `mclWasm.run(requestJSON, binaryFiles)`. The Go runtime copies those bytes into its project filesystem. The SDK-owned and caller-owned arrays remain reusable after each operation.
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 hover result types, `maxSourceBytes`, `maxProjectFiles`, `maxProjectFileBytes`, and `maxProjectBytes` so browser interfaces can consume the runtime's public types and limits.
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: 96170429 bytes
4
- sha256: d08ad3914d8a2862e52db7dbc3ec43c38c1143dea99dda6c87383c7d59212c7d
3
+ size: 96317496 bytes
4
+ sha256: 58f90cac0af9f6b4608f787528617729b99a6fcb65db52f168f95a1a6703e98f
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manifest-configuration-language/browser",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "description": "Run MCL projects in a browser Web Worker using the Go WebAssembly runtime.",
5
5
  "repository": {
6
6
  "type": "git",
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 per-request filesystem. Uint8Array values are copied before transfer so callers can reuse them. */
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(source: string, files?: ProjectFiles, options?: RequestOptions): Promise<MclResult & { operation: "check" }>;
71
- evaluate(source: string, files?: ProjectFiles, options?: RequestOptions): Promise<MclResult & { operation: "evaluate" }>;
72
- render(source: string, files?: ProjectFiles, options?: RequestOptions): Promise<MclResult & { operation: "render" }>;
73
- definition(source: string, options: PositionRequestOptions): Promise<MclResult & { operation: "definition" }>;
74
- definition(source: string, files: ProjectFiles | undefined, options: PositionRequestOptions): Promise<MclResult & { operation: "definition" }>;
75
- hover(source: string, options: PositionRequestOptions): Promise<MclResult & { operation: "hover" }>;
76
- hover(source: string, files: ProjectFiles | undefined, options: PositionRequestOptions): Promise<MclResult & { operation: "hover" }>;
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 { maxProjectBytes, maxProjectFileBytes, maxProjectFiles, maxProjectPathDepth, maxProjectPathLength, maxSourceBytes };
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 (typeof value !== "string" || value.length === 0 || value.startsWith("/") ||
14
- /^[a-z]:/i.test(value) || value.includes("\\") || value.includes("\0")) {
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 = options.workerFactory ?? (() => new Worker(new URL("./worker.js", import.meta.url), { type: "module" }));
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(() => fail(new Error("MCL WebAssembly initialization timed out")), initializationTimeoutMs);
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 execute(operation, source, files, requestOptions = {}, entrypoint = "main.mcl", position) {
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) throw new DOMException("The operation was aborted", "AbortError");
131
- const timeoutMs = requestOptions.timeoutMs ?? options.operationTimeoutMs ?? defaultOperationTimeoutMs;
132
- const timeoutError = new MclRuntimeError({ code: "MCL-WASM-TIMEOUT", message: "MCL operation timed out" });
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) throw new DOMException("The operation was aborted", "AbortError");
142
- const preparedFiles = prepareFilesForTransfer(source, files, entrypoint);
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") finish(resolve, { operation, diagnostics: response.value.result });
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
- currentWorker.postMessage({ type: "run", requestId, operation, source, files: preparedFiles.files, entrypoint, position }, preparedFiles.transferables);
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(() => signal.removeEventListener("abort", onAbort));
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) return Promise.reject(new DOMException("The operation was aborted", "AbortError"));
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({ code: "MCL-WASM-TIMEOUT", message: "MCL WebAssembly initialization timed out" });
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(() => signal.removeEventListener("abort", onAbort));
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) throw new TypeError("Project must contain at least one MCL source file");
239
- if (entries.length > maxProjectFiles) throw new RangeError(`Project has more than ${maxProjectFiles} files`);
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
- for (const [filePath, content] of entries) {
245
- if (!isSafeRelativePath(filePath)) throw new TypeError(`Invalid project file path: ${filePath}`);
246
- if (typeof content !== "string" && !(content instanceof Uint8Array)) throw new TypeError(`Project file ${filePath} must be a string or Uint8Array`);
247
- const fileBytes = typeof content === "string" ? new TextEncoder().encode(content).byteLength : content.byteLength;
248
- if (fileBytes > maxProjectFileBytes) throw new RangeError(`Project file ${filePath} exceeds the ${maxProjectFileBytes}-byte limit`);
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) throw new RangeError(`Project files exceed the ${maxProjectBytes}-byte limit`);
251
- projectFiles[filePath] = typeof content === "string" ? content : new Uint8Array(content);
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
- const sourcePaths = Object.keys(projectFiles).filter((filePath) => filePath.endsWith(".mcl")).sort();
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) throw new TypeError("Set entrypoint when the project has multiple MCL source files");
259
- if (!isSafeRelativePath(entrypoint) || !Object.hasOwn(projectFiles, entrypoint)) throw new TypeError(`Project entrypoint does not exist: ${entrypoint}`);
260
- const entry = projectFiles[entrypoint];
261
- const source = typeof entry === "string" ? entry : new TextDecoder("utf-8", { fatal: true }).decode(entry);
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) throw new RangeError(`Source exceeds the ${maxSourceBytes}-byte limit`);
264
- const projectBytes = Object.values(projectFiles).reduce((total, file) => total + (typeof file === "string" ? new TextEncoder().encode(file).byteLength : file.byteLength), 0);
265
- if (projectBytes > maxProjectBytes) throw new RangeError(`Project files exceed the ${maxProjectBytes}-byte limit`);
266
- const otherFiles = Object.fromEntries(Object.entries(projectFiles).filter(([filePath]) => filePath !== entrypoint));
267
- return execute(operation, source, otherFiles, requestOptions, entrypoint, requestOptions.position);
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)) throw new TypeError(`Invalid project entrypoint: ${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) throw new RangeError(`Source exceeds the ${maxSourceBytes}-byte limit`);
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) throw new RangeError(`Project has more than ${maxProjectFiles} files`);
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) throw new TypeError(`Invalid project file path: ${filePath}`);
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) throw new RangeError(`Project file ${filePath} exceeds the ${maxProjectFileBytes}-byte limit`);
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) throw new RangeError(`Project file ${filePath} exceeds the ${maxProjectFileBytes}-byte limit`);
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) throw new RangeError(`Project files exceed the ${maxProjectBytes}-byte limit`);
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(operation, source, files, options, options.entrypoint ?? "main.mcl", options.position);
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) => runSourceNavigation("definition", source, filesOrOptions, requestOptions),
333
- hover: (source, filesOrOptions, requestOptions) => runSourceNavigation("hover", source, filesOrOptions, requestOptions),
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: "parse" | "lint" | "format" | "check" | "evaluate" | "render" | "completion" | "definition" | "hover";
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 = () => Promise.reject(new TypeError("Network access is disabled in the MCL runtime"));
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
- (name) => name === "fetch" ? globalThis.fetch !== originalFetch : globalThis[name] !== originalNetworkAPIs.get(name),
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) throw new Error("MCL runtime assets were not configured");
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
- void go.run(instance);
69
- while (!globalThis.mclWasmReady) await new Promise((resolve) => setTimeout(resolve, 0));
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(globalThis.mclWasm.run(JSON.stringify(request), encodedFiles.binaryFiles));
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: { code: "MCL-WASM-INPUT", message: error instanceof Error ? error.message : String(error) },
190
+ error: {
191
+ code: "MCL-WASM-INPUT",
192
+ message: error instanceof Error ? error.message : String(error),
193
+ },
134
194
  },
135
195
  });
136
196
  }