@wasm-oj/server 0.2.2 → 0.2.4

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.
@@ -0,0 +1,191 @@
1
+ import "./toolchains-uz3XbZGI.js";
2
+ import { assertLanguageIdentifier } from "@wasm-oj/contracts";
3
+ //#region src/core/project-files.ts
4
+ var PROJECT_SOURCE_LIMITS = Object.freeze({
5
+ files: 256,
6
+ bytesPerFile: 4194304,
7
+ totalBytes: 16777216
8
+ });
9
+ var UTF8_ENCODER = new TextEncoder();
10
+ /** Locale-independent ordering used anywhere file order can affect build output. */
11
+ function compareCanonicalPaths(left, right) {
12
+ return left < right ? -1 : left > right ? 1 : 0;
13
+ }
14
+ function assertSafeRelativePath(path, label = "Project path") {
15
+ if (typeof path !== "string" || !path || path !== path.trim() || path.length > 4096) throw new Error(`${label} must be a non-empty, trimmed string of at most 4096 characters.`);
16
+ if (path.startsWith("/") || path.includes("\\") || path.includes("\0") || path.split("/").some((segment) => !segment || segment === "." || segment === "..")) throw new Error(`${label} '${path}' must be a normalized relative path that cannot escape the project.`);
17
+ }
18
+ /**
19
+ * Validates a project file set and returns a fresh, locale-independently sorted
20
+ * array. Build hosts must use this order for filesystem creation and argv.
21
+ */
22
+ function canonicalProjectFiles(files) {
23
+ if (!Array.isArray(files) || files.length === 0) throw new Error("A project must contain at least one source file.");
24
+ if (files.length > PROJECT_SOURCE_LIMITS.files) throw new Error(`A project cannot contain more than ${PROJECT_SOURCE_LIMITS.files} source files.`);
25
+ const seen = /* @__PURE__ */ new Set();
26
+ let totalBytes = 0;
27
+ return files.map((file, index) => {
28
+ if (!file || typeof file !== "object") throw new Error(`Project file ${index} is invalid.`);
29
+ assertSafeRelativePath(file.path, `Project file ${index} path`);
30
+ assertLanguageIdentifier(file.language);
31
+ if (typeof file.content !== "string") throw new Error(`Project file '${file.path}' content must be a string.`);
32
+ if (file.content.length > PROJECT_SOURCE_LIMITS.bytesPerFile) throw new Error(`Project file '${file.path}' exceeds the ${PROJECT_SOURCE_LIMITS.bytesPerFile} byte source limit.`);
33
+ const contentBytes = UTF8_ENCODER.encode(file.content).byteLength;
34
+ if (contentBytes > PROJECT_SOURCE_LIMITS.bytesPerFile) throw new Error(`Project file '${file.path}' exceeds the ${PROJECT_SOURCE_LIMITS.bytesPerFile} byte source limit.`);
35
+ totalBytes += contentBytes;
36
+ if (totalBytes > PROJECT_SOURCE_LIMITS.totalBytes) throw new Error(`Project sources exceed the ${PROJECT_SOURCE_LIMITS.totalBytes} byte total limit.`);
37
+ if (seen.has(file.path)) throw new Error(`Duplicate project path '${file.path}'.`);
38
+ seen.add(file.path);
39
+ return {
40
+ path: file.path,
41
+ language: file.language,
42
+ content: file.content
43
+ };
44
+ }).sort((left, right) => compareCanonicalPaths(left.path, right.path));
45
+ }
46
+ function canonicalFileEntries(files) {
47
+ if (!files || typeof files !== "object" || Array.isArray(files)) throw new Error("Runtime bundle files must be a record.");
48
+ return Object.entries(files).map(([path, contents]) => {
49
+ assertSafeRelativePath(path, "Runtime bundle path");
50
+ return [path, contents];
51
+ }).sort(([left], [right]) => compareCanonicalPaths(left, right));
52
+ }
53
+ function canonicalFileRecord(files) {
54
+ return Object.fromEntries(canonicalFileEntries(files));
55
+ }
56
+ //#endregion
57
+ //#region src/core/dependencies.ts
58
+ var MIB = 1048576;
59
+ /** Contract-level admission limits shared by dependency hosts and compilers. */
60
+ var DEPENDENCY_RESOLUTION_LIMITS = Object.freeze({
61
+ requirements: 128,
62
+ sourceFiles: 128,
63
+ sourceTextBytes: 8 * MIB,
64
+ hosts: 32,
65
+ roots: 512,
66
+ packages: 512,
67
+ referencesPerPackage: 512,
68
+ concurrency: 16,
69
+ metadataBytes: 8 * MIB,
70
+ packageBytes: 256 * MIB,
71
+ totalDownloadBytes: 512 * MIB,
72
+ archiveFiles: 16384,
73
+ unpackedBytes: 512 * MIB
74
+ });
75
+ Object.freeze({
76
+ packages: DEPENDENCY_RESOLUTION_LIMITS.packages,
77
+ filesPerPackage: DEPENDENCY_RESOLUTION_LIMITS.archiveFiles,
78
+ bytesPerFile: 64 * MIB,
79
+ totalBytes: DEPENDENCY_RESOLUTION_LIMITS.unpackedBytes
80
+ });
81
+ /**
82
+ * Read stdout until `parse` recognizes one complete self-delimiting message.
83
+ *
84
+ * `@wasmer/sdk` 0.10 `Instance.wait()` joins stdout EOF, stderr EOF and the exit code, and its
85
+ * thread-pool teardown can drop either EOF after all output arrived, so `wait()` never settles.
86
+ * Completion therefore comes from the message itself. Either EOF means the guest exited, so the
87
+ * other stream then gets `idleGraceMs` without new bytes before the read stops. If both EOFs are
88
+ * lost before the message completes, the caller's own deadline still bounds the read.
89
+ */
90
+ async function readProcessMessage(child, parse, options = {}) {
91
+ const idleGraceMs = options.idleGraceMs ?? 2e3;
92
+ await child.stdin?.close().catch(() => void 0);
93
+ const stdoutReader = child.stdout.getReader();
94
+ const stderrReader = child.stderr.getReader();
95
+ const nextStdout = () => stdoutReader.read().then(({ done, value }) => ({
96
+ source: "stdout",
97
+ chunk: done ? void 0 : value
98
+ }));
99
+ const nextStderr = () => stderrReader.read().then(({ done, value }) => ({
100
+ source: "stderr",
101
+ chunk: done ? void 0 : value
102
+ })).catch(() => ({ source: "stderr" }));
103
+ const stdout = byteBuffer();
104
+ const stderr = byteBuffer();
105
+ let stdoutRead = nextStdout();
106
+ let stderrRead = nextStderr();
107
+ let message;
108
+ try {
109
+ while (true) {
110
+ const complete = message !== void 0;
111
+ const pending = (complete ? [options.collectStderr ? stderrRead : void 0] : [stdoutRead, stderrRead]).filter((read) => read !== void 0);
112
+ if (pending.length === 0) break;
113
+ let timer;
114
+ if (complete || pending.length === 1) pending.push(new Promise((resolve) => {
115
+ timer = setTimeout(() => resolve({ source: "idle" }), idleGraceMs);
116
+ }));
117
+ const event = await Promise.race(pending).finally(() => clearTimeout(timer));
118
+ if (event.source === "idle") break;
119
+ if (event.source === "stdout") {
120
+ if (!event.chunk) {
121
+ stdoutRead = void 0;
122
+ continue;
123
+ }
124
+ stdout.append(event.chunk);
125
+ message = parse(stdout.bytes());
126
+ stdoutRead = message === void 0 ? nextStdout() : void 0;
127
+ } else if (!event.chunk) stderrRead = void 0;
128
+ else {
129
+ stderr.append(event.chunk);
130
+ stderrRead = nextStderr();
131
+ }
132
+ }
133
+ } finally {
134
+ await Promise.allSettled([stdoutReader.cancel(), stderrReader.cancel()]);
135
+ }
136
+ return {
137
+ message,
138
+ stderr: new TextDecoder().decode(stderr.bytes())
139
+ };
140
+ }
141
+ var JSON_WHITESPACE = /* @__PURE__ */ new Set([
142
+ 9,
143
+ 10,
144
+ 13,
145
+ 32
146
+ ]);
147
+ /** Parse stdout once it holds one complete JSON object, optionally followed by whitespace. */
148
+ function completeJsonObject(stdout) {
149
+ let end = stdout.byteLength;
150
+ while (end > 0 && JSON_WHITESPACE.has(stdout[end - 1])) end -= 1;
151
+ if (end === 0 || stdout[end - 1] !== 125) return void 0;
152
+ try {
153
+ return JSON.parse(new TextDecoder().decode(stdout.subarray(0, end)));
154
+ } catch {
155
+ return;
156
+ }
157
+ }
158
+ /** An append-only byte buffer that grows geometrically, so appending chunks stays linear. */
159
+ function byteBuffer() {
160
+ let buffer = /* @__PURE__ */ new Uint8Array(0);
161
+ let length = 0;
162
+ return {
163
+ append(chunk) {
164
+ if (length + chunk.byteLength > buffer.byteLength) {
165
+ const grown = new Uint8Array(Math.max(length + chunk.byteLength, buffer.byteLength * 2));
166
+ grown.set(buffer.subarray(0, length));
167
+ buffer = grown;
168
+ }
169
+ buffer.set(chunk, length);
170
+ length += chunk.byteLength;
171
+ },
172
+ bytes: () => buffer.subarray(0, length)
173
+ };
174
+ }
175
+ //#endregion
176
+ //#region src/server/process-keepalive.mjs
177
+ /**
178
+ * Keep Node alive while a promise is backed only by a foreign async runtime.
179
+ * Top-level await does not itself create a referenced libuv handle, so Node can
180
+ * otherwise exit with code 13 while a Wasmer SDK operation is still pending.
181
+ */
182
+ async function withProcessKeepalive(promise) {
183
+ const keepalive = setInterval(() => void 0, 6e4);
184
+ try {
185
+ return await promise;
186
+ } finally {
187
+ clearInterval(keepalive);
188
+ }
189
+ }
190
+ //#endregion
191
+ export { canonicalFileRecord as a, assertSafeRelativePath as i, completeJsonObject as n, canonicalProjectFiles as o, readProcessMessage as r, withProcessKeepalive as t };
package/dist/index.js CHANGED
@@ -775,8 +775,8 @@ Object.freeze({
775
775
  });
776
776
  new TextEncoder();
777
777
  Object.freeze({
778
- runtimeCoreWasmSha256: "1e297ad276694e9fb8a23afdc6ccc0b2ae56a223ced19d5cae26fe4661daa11e",
779
- runtimeSourceRootSha256: "27c52cb1887aa8a7e0d8a238514fc8881cf86c68267e2a1691f110423692e2b6",
778
+ runtimeCoreWasmSha256: "0ae00fc646f529aafb5d1a68758220e6428f6c565e8e621eeb6c8f14ac5d4a10",
779
+ runtimeSourceRootSha256: "a7e9071511d127e3b14920558c7c38c27a1fa19d98906c8476c1df8041a9c3bb",
780
780
  wasmerNativeVersion: "7.2.1",
781
781
  wasmerSdkVersion: "0.10.0",
782
782
  wasmerSdkWasmSha256: "49a6646209f5ab5e7c737eac33407d87d9a9959ac83e5ecaaab9261b2323589e",
@@ -787,7 +787,7 @@ Object.freeze({
787
787
  * Release verification independently checks the component bytes before this
788
788
  * identity is admitted into a calibrated release.
789
789
  */
790
- var WASM_OJ_RUNTIME_IDENTITY_SHA256 = "d3e44cafdc1c17a7e6bd4591ef04ec8161af805a519b5bbff437b15e5accd442";
790
+ var WASM_OJ_RUNTIME_IDENTITY_SHA256 = "e5680beae9bd832ed2b679267250ce1efc7d104dd005bb155135664b40f961fb";
791
791
  //#endregion
792
792
  //#region src/core/cost-profile.ts
793
793
  function coordinates(language, target, optimization) {
@@ -1858,6 +1858,100 @@ function failedBuild(project, stdout, stderr, code, source, providedDiagnostics)
1858
1858
  cacheHit: false
1859
1859
  };
1860
1860
  }
1861
+ /**
1862
+ * Read stdout until `parse` recognizes one complete self-delimiting message.
1863
+ *
1864
+ * `@wasmer/sdk` 0.10 `Instance.wait()` joins stdout EOF, stderr EOF and the exit code, and its
1865
+ * thread-pool teardown can drop either EOF after all output arrived, so `wait()` never settles.
1866
+ * Completion therefore comes from the message itself. Either EOF means the guest exited, so the
1867
+ * other stream then gets `idleGraceMs` without new bytes before the read stops. If both EOFs are
1868
+ * lost before the message completes, the caller's own deadline still bounds the read.
1869
+ */
1870
+ async function readProcessMessage(child, parse, options = {}) {
1871
+ const idleGraceMs = options.idleGraceMs ?? 2e3;
1872
+ await child.stdin?.close().catch(() => void 0);
1873
+ const stdoutReader = child.stdout.getReader();
1874
+ const stderrReader = child.stderr.getReader();
1875
+ const nextStdout = () => stdoutReader.read().then(({ done, value }) => ({
1876
+ source: "stdout",
1877
+ chunk: done ? void 0 : value
1878
+ }));
1879
+ const nextStderr = () => stderrReader.read().then(({ done, value }) => ({
1880
+ source: "stderr",
1881
+ chunk: done ? void 0 : value
1882
+ })).catch(() => ({ source: "stderr" }));
1883
+ const stdout = byteBuffer();
1884
+ const stderr = byteBuffer();
1885
+ let stdoutRead = nextStdout();
1886
+ let stderrRead = nextStderr();
1887
+ let message;
1888
+ try {
1889
+ while (true) {
1890
+ const complete = message !== void 0;
1891
+ const pending = (complete ? [options.collectStderr ? stderrRead : void 0] : [stdoutRead, stderrRead]).filter((read) => read !== void 0);
1892
+ if (pending.length === 0) break;
1893
+ let timer;
1894
+ if (complete || pending.length === 1) pending.push(new Promise((resolve) => {
1895
+ timer = setTimeout(() => resolve({ source: "idle" }), idleGraceMs);
1896
+ }));
1897
+ const event = await Promise.race(pending).finally(() => clearTimeout(timer));
1898
+ if (event.source === "idle") break;
1899
+ if (event.source === "stdout") {
1900
+ if (!event.chunk) {
1901
+ stdoutRead = void 0;
1902
+ continue;
1903
+ }
1904
+ stdout.append(event.chunk);
1905
+ message = parse(stdout.bytes());
1906
+ stdoutRead = message === void 0 ? nextStdout() : void 0;
1907
+ } else if (!event.chunk) stderrRead = void 0;
1908
+ else {
1909
+ stderr.append(event.chunk);
1910
+ stderrRead = nextStderr();
1911
+ }
1912
+ }
1913
+ } finally {
1914
+ await Promise.allSettled([stdoutReader.cancel(), stderrReader.cancel()]);
1915
+ }
1916
+ return {
1917
+ message,
1918
+ stderr: new TextDecoder().decode(stderr.bytes())
1919
+ };
1920
+ }
1921
+ var JSON_WHITESPACE = /* @__PURE__ */ new Set([
1922
+ 9,
1923
+ 10,
1924
+ 13,
1925
+ 32
1926
+ ]);
1927
+ /** Parse stdout once it holds one complete JSON object, optionally followed by whitespace. */
1928
+ function completeJsonObject(stdout) {
1929
+ let end = stdout.byteLength;
1930
+ while (end > 0 && JSON_WHITESPACE.has(stdout[end - 1])) end -= 1;
1931
+ if (end === 0 || stdout[end - 1] !== 125) return void 0;
1932
+ try {
1933
+ return JSON.parse(new TextDecoder().decode(stdout.subarray(0, end)));
1934
+ } catch {
1935
+ return;
1936
+ }
1937
+ }
1938
+ /** An append-only byte buffer that grows geometrically, so appending chunks stays linear. */
1939
+ function byteBuffer() {
1940
+ let buffer = /* @__PURE__ */ new Uint8Array(0);
1941
+ let length = 0;
1942
+ return {
1943
+ append(chunk) {
1944
+ if (length + chunk.byteLength > buffer.byteLength) {
1945
+ const grown = new Uint8Array(Math.max(length + chunk.byteLength, buffer.byteLength * 2));
1946
+ grown.set(buffer.subarray(0, length));
1947
+ buffer = grown;
1948
+ }
1949
+ buffer.set(chunk, length);
1950
+ length += chunk.byteLength;
1951
+ },
1952
+ bytes: () => buffer.subarray(0, length)
1953
+ };
1954
+ }
1861
1955
  new TextEncoder();
1862
1956
  Object.freeze({
1863
1957
  python: "python",
@@ -2244,7 +2338,7 @@ async function transpileScriptProject(project, requestId) {
2244
2338
  if (!entrypoint) throw new Error("The TypeScript/WASI compiler has no executable entrypoint.");
2245
2339
  const outputPaths = emittedFiles.map((file) => emittedScriptPath(file.path));
2246
2340
  const declarationPath = "/project/.wasm-oj/quickjs.d.ts";
2247
- const output = await (await entrypoint.run({ stdin: JSON.stringify({
2341
+ const instance = await entrypoint.run({ stdin: JSON.stringify({
2248
2342
  files: {
2249
2343
  ...Object.fromEntries(project.files.map((file) => [`/project/${file.path}`, file.content])),
2250
2344
  ...Object.fromEntries(Object.entries(dependencyFiles).filter(([, contents]) => typeof contents === "string").map(([path, contents]) => [`/project/${path}`, contents])),
@@ -2256,13 +2350,9 @@ async function transpileScriptProject(project, requestId) {
2256
2350
  ...Object.entries(dependencyFiles).filter(([path, contents]) => path.endsWith(".d.ts") && typeof contents === "string").map(([path]) => `/project/${path}`)
2257
2351
  ],
2258
2352
  outputs: outputPaths.map((path) => `/project/build/${path}`)
2259
- }) })).wait();
2260
- let response;
2261
- if (output.ok) try {
2262
- response = JSON.parse(output.stdout);
2263
- } catch {
2264
- response = void 0;
2265
- }
2353
+ }) });
2354
+ const output = await readProcessMessage(instance, completeJsonObject, { collectStderr: true }).finally(() => instance.free());
2355
+ const response = output.message;
2266
2356
  const files = {};
2267
2357
  if (response) for (const outputPath of outputPaths) {
2268
2358
  const contents = response.files[`/project/build/${outputPath}`];
@@ -2271,7 +2361,7 @@ async function transpileScriptProject(project, requestId) {
2271
2361
  Object.assign(files, dependencyFiles);
2272
2362
  return {
2273
2363
  files,
2274
- output,
2364
+ stderr: output.stderr,
2275
2365
  response
2276
2366
  };
2277
2367
  }
@@ -2304,10 +2394,10 @@ async function buildScript(project, cacheKey, requestId) {
2304
2394
  progress(requestId, "compiling", "Compiling TypeScript with TypeScript/WASI", .5);
2305
2395
  const transpiled = await transpileScriptProject(project, requestId);
2306
2396
  files = transpiled.files;
2307
- stderr = transpiled.output.stderr;
2397
+ stderr = transpiled.stderr;
2308
2398
  diagnostics = parseTypeScriptDiagnostics(transpiled.response?.diagnostics ?? "");
2309
2399
  const emittedOutputsPresent = emittedSourceFiles(project).every((file) => Object.hasOwn(files, emittedScriptPath(file.path)));
2310
- if (!transpiled.output.ok || !transpiled.response || transpiled.response.status !== 0 || !emittedOutputsPresent || diagnostics.some((diagnostic) => diagnostic.severity === "error")) return {
2400
+ if (!transpiled.response || transpiled.response.status !== 0 || !emittedOutputsPresent || diagnostics.some((diagnostic) => diagnostic.severity === "error")) return {
2311
2401
  success: false,
2312
2402
  diagnostics: ensureFailureDiagnostic(diagnostics, {
2313
2403
  file: project.config.entry,
@@ -3287,7 +3377,6 @@ var ServerRunner = class {
3287
3377
  this.assertArtifactToolchain(contestantArtifact);
3288
3378
  this.assertArtifactToolchain(interactorArtifact);
3289
3379
  if (this.activeOperation || this.cacheClearActive) throw new Error("ServerRunner accepts one active operation at a time.");
3290
- if (interactorArtifact.kind !== "wasm") throw new Error("Interactive judge artifacts must be standalone Wasm modules.");
3291
3380
  const operation = createServerRunOperation(this.generation);
3292
3381
  this.activeOperation = operation;
3293
3382
  this.inFlightRuns.add(operation);
@@ -3610,7 +3699,7 @@ var ServerRunner = class {
3610
3699
  const cleanup = (ignoreLateChildError) => {
3611
3700
  child.off("close", onClose);
3612
3701
  child.off("error", onChildError);
3613
- child.stdin.off("error", onStdinError);
3702
+ child.stdin.off("error", onStdinError).on("error", () => void 0);
3614
3703
  child.stdout.off("data", onStdout);
3615
3704
  child.stderr.off("data", onStderr);
3616
3705
  this.activePreparationStages.delete(active);
@@ -3749,7 +3838,7 @@ var ServerRunner = class {
3749
3838
  clearTimeout(timer);
3750
3839
  child.off("close", onClose);
3751
3840
  child.off("error", onChildError);
3752
- child.stdin.off("error", onStdinError);
3841
+ child.stdin.off("error", onStdinError).on("error", () => void 0);
3753
3842
  child.stdout.off("data", onStdout);
3754
3843
  child.stderr.off("data", onStderr);
3755
3844
  this.activeNativeRuns.delete(active);
@@ -3847,7 +3936,7 @@ var ServerRunner = class {
3847
3936
  clearTimeout(timer);
3848
3937
  child.off("close", onClose);
3849
3938
  child.off("error", onChildError);
3850
- child.stdin.off("error", onStdinError);
3939
+ child.stdin.off("error", onStdinError).on("error", () => void 0);
3851
3940
  child.stdout.off("data", onStdout);
3852
3941
  child.stderr.off("data", onStderr);
3853
3942
  this.activeNativeRuns.delete(active);
@@ -1,7 +1,7 @@
1
1
  import { A as QUICKJS_PACKAGE, B as toolchainPackageIdentities, C as JAVA_COMPILE_CLASSLIB_SHA256, E as JAVA_VERSION, I as RUST_VERSION, L as TYPESCRIPT_ASSET_PATH, O as PYTHON_PACKAGE, R as TYPESCRIPT_VERSION, S as JAVA_COMPILE_CLASSLIB_ASSET_PATH, T as JAVA_RUNTIME_CLASSLIB_SHA256, a as CLANG_PACKAGE_ASSET_PATH, b as JAVA_COMPILER_PACKAGE, i as CLANG_LIBCXX_PCH_MANIFEST_SHA256, n as CLANG_CC1_PINS_SHA256, o as CLANG_PACKAGE_SHA256, r as CLANG_LIBCXX_PCH_MANIFEST_ASSET_PATH, s as CLANG_VERSION, t as CLANG_CC1_PINS_ASSET_PATH, v as GO_VERSION, w as JAVA_RUNTIME_CLASSLIB_ASSET_PATH, x as JAVA_COMPILER_SHA256, y as JAVA_COMPILER_ASSET_PATH, z as toolchainContentIdentity } from "./chunks/toolchains-uz3XbZGI.js";
2
+ import { a as canonicalFileRecord, i as assertSafeRelativePath, n as completeJsonObject, o as canonicalProjectFiles, r as readProcessMessage, t as withProcessKeepalive } from "./chunks/process-keepalive-BaXpMTap.js";
2
3
  import { f as MountedOutputStabilityObserver, n as RUST_TOOLCHAIN, t as RUST_COMPILE_TIMEOUT_MS } from "./chunks/rust-toolchain-CK5h2WT8.js";
3
4
  import { _ as pythonDependencyFiles, g as npmDependencyFiles, h as goDependencyInput, i as GO_TOOLCHAIN, m as cppDependencyInput, n as GO_COMPILE_TIMEOUT_MS, p as assertProjectDependencyEcosystem, v as rustDependencyInput } from "./chunks/go-toolchain-DRQSILaC.js";
4
- import { t as withProcessKeepalive } from "./chunks/process-keepalive-BWLUz55N.js";
5
5
  import { constants, writeFileSync } from "node:fs";
6
6
  import { deserialize, serialize } from "node:v8";
7
7
  import { access, lstat, mkdtemp, open, readFile, rm, writeFile } from "node:fs/promises";
@@ -15,60 +15,6 @@ import { assertCompilerCacheKey, assertValidProject, toolchainAssetSource, toolc
15
15
  import { WASM_OJ_CONTRACT_VERSION, WASM_OJ_SCHEMAS, assertLanguageIdentifier, isBuiltinLanguage } from "@wasm-oj/contracts";
16
16
  import { Directory as Directory$1, Wasmer as Wasmer$1 } from "@wasmer/sdk";
17
17
  import { fileURLToPath } from "node:url";
18
- //#region src/core/project-files.ts
19
- var PROJECT_SOURCE_LIMITS = Object.freeze({
20
- files: 256,
21
- bytesPerFile: 4194304,
22
- totalBytes: 16777216
23
- });
24
- var UTF8_ENCODER = new TextEncoder();
25
- /** Locale-independent ordering used anywhere file order can affect build output. */
26
- function compareCanonicalPaths(left, right) {
27
- return left < right ? -1 : left > right ? 1 : 0;
28
- }
29
- function assertSafeRelativePath(path, label = "Project path") {
30
- if (typeof path !== "string" || !path || path !== path.trim() || path.length > 4096) throw new Error(`${label} must be a non-empty, trimmed string of at most 4096 characters.`);
31
- if (path.startsWith("/") || path.includes("\\") || path.includes("\0") || path.split("/").some((segment) => !segment || segment === "." || segment === "..")) throw new Error(`${label} '${path}' must be a normalized relative path that cannot escape the project.`);
32
- }
33
- /**
34
- * Validates a project file set and returns a fresh, locale-independently sorted
35
- * array. Build hosts must use this order for filesystem creation and argv.
36
- */
37
- function canonicalProjectFiles(files) {
38
- if (!Array.isArray(files) || files.length === 0) throw new Error("A project must contain at least one source file.");
39
- if (files.length > PROJECT_SOURCE_LIMITS.files) throw new Error(`A project cannot contain more than ${PROJECT_SOURCE_LIMITS.files} source files.`);
40
- const seen = /* @__PURE__ */ new Set();
41
- let totalBytes = 0;
42
- return files.map((file, index) => {
43
- if (!file || typeof file !== "object") throw new Error(`Project file ${index} is invalid.`);
44
- assertSafeRelativePath(file.path, `Project file ${index} path`);
45
- assertLanguageIdentifier(file.language);
46
- if (typeof file.content !== "string") throw new Error(`Project file '${file.path}' content must be a string.`);
47
- if (file.content.length > PROJECT_SOURCE_LIMITS.bytesPerFile) throw new Error(`Project file '${file.path}' exceeds the ${PROJECT_SOURCE_LIMITS.bytesPerFile} byte source limit.`);
48
- const contentBytes = UTF8_ENCODER.encode(file.content).byteLength;
49
- if (contentBytes > PROJECT_SOURCE_LIMITS.bytesPerFile) throw new Error(`Project file '${file.path}' exceeds the ${PROJECT_SOURCE_LIMITS.bytesPerFile} byte source limit.`);
50
- totalBytes += contentBytes;
51
- if (totalBytes > PROJECT_SOURCE_LIMITS.totalBytes) throw new Error(`Project sources exceed the ${PROJECT_SOURCE_LIMITS.totalBytes} byte total limit.`);
52
- if (seen.has(file.path)) throw new Error(`Duplicate project path '${file.path}'.`);
53
- seen.add(file.path);
54
- return {
55
- path: file.path,
56
- language: file.language,
57
- content: file.content
58
- };
59
- }).sort((left, right) => compareCanonicalPaths(left.path, right.path));
60
- }
61
- function canonicalFileEntries(files) {
62
- if (!files || typeof files !== "object" || Array.isArray(files)) throw new Error("Runtime bundle files must be a record.");
63
- return Object.entries(files).map(([path, contents]) => {
64
- assertSafeRelativePath(path, "Runtime bundle path");
65
- return [path, contents];
66
- }).sort(([left], [right]) => compareCanonicalPaths(left, right));
67
- }
68
- function canonicalFileRecord(files) {
69
- return Object.fromEntries(canonicalFileEntries(files));
70
- }
71
- //#endregion
72
18
  //#region src/core/sha256.ts
73
19
  async function sha256Hex(value) {
74
20
  const source = typeof value === "string" ? new TextEncoder().encode(value) : value;
@@ -78,31 +24,6 @@ async function sha256Hex(value) {
78
24
  return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
79
25
  }
80
26
  //#endregion
81
- //#region src/core/dependencies.ts
82
- var MIB = 1048576;
83
- /** Contract-level admission limits shared by dependency hosts and compilers. */
84
- var DEPENDENCY_RESOLUTION_LIMITS = Object.freeze({
85
- requirements: 128,
86
- sourceFiles: 128,
87
- sourceTextBytes: 8 * MIB,
88
- hosts: 32,
89
- roots: 512,
90
- packages: 512,
91
- referencesPerPackage: 512,
92
- concurrency: 16,
93
- metadataBytes: 8 * MIB,
94
- packageBytes: 256 * MIB,
95
- totalDownloadBytes: 512 * MIB,
96
- archiveFiles: 16384,
97
- unpackedBytes: 512 * MIB
98
- });
99
- Object.freeze({
100
- packages: DEPENDENCY_RESOLUTION_LIMITS.packages,
101
- filesPerPackage: DEPENDENCY_RESOLUTION_LIMITS.archiveFiles,
102
- bytesPerFile: 64 * MIB,
103
- totalBytes: DEPENDENCY_RESOLUTION_LIMITS.unpackedBytes
104
- });
105
- //#endregion
106
27
  //#region src/compiler/clang-pins.ts
107
28
  var decoder$3 = new TextDecoder();
108
29
  async function decodeClangPins(bytes) {
@@ -561,8 +482,8 @@ Object.freeze({
561
482
  });
562
483
  new TextEncoder();
563
484
  Object.freeze({
564
- runtimeCoreWasmSha256: "1e297ad276694e9fb8a23afdc6ccc0b2ae56a223ced19d5cae26fe4661daa11e",
565
- runtimeSourceRootSha256: "27c52cb1887aa8a7e0d8a238514fc8881cf86c68267e2a1691f110423692e2b6",
485
+ runtimeCoreWasmSha256: "0ae00fc646f529aafb5d1a68758220e6428f6c565e8e621eeb6c8f14ac5d4a10",
486
+ runtimeSourceRootSha256: "a7e9071511d127e3b14920558c7c38c27a1fa19d98906c8476c1df8041a9c3bb",
566
487
  wasmerNativeVersion: "7.2.1",
567
488
  wasmerSdkVersion: "0.10.0",
568
489
  wasmerSdkWasmSha256: "49a6646209f5ab5e7c737eac33407d87d9a9959ac83e5ecaaab9261b2323589e",
@@ -573,7 +494,7 @@ Object.freeze({
573
494
  * Release verification independently checks the component bytes before this
574
495
  * identity is admitted into a calibrated release.
575
496
  */
576
- var WASM_OJ_RUNTIME_IDENTITY_SHA256 = "d3e44cafdc1c17a7e6bd4591ef04ec8161af805a519b5bbff437b15e5accd442";
497
+ var WASM_OJ_RUNTIME_IDENTITY_SHA256 = "e5680beae9bd832ed2b679267250ce1efc7d104dd005bb155135664b40f961fb";
577
498
  //#endregion
578
499
  //#region src/core/cost-profile.ts
579
500
  function coordinates(language, target, optimization) {
@@ -1753,7 +1674,7 @@ async function transpileScriptProject(project, requestId) {
1753
1674
  if (!entrypoint) throw new Error("The TypeScript/WASI compiler has no executable entrypoint.");
1754
1675
  const outputPaths = emittedFiles.map((file) => emittedScriptPath(file.path));
1755
1676
  const declarationPath = "/project/.wasm-oj/quickjs.d.ts";
1756
- const output = await (await entrypoint.run({ stdin: JSON.stringify({
1677
+ const instance = await entrypoint.run({ stdin: JSON.stringify({
1757
1678
  files: {
1758
1679
  ...Object.fromEntries(project.files.map((file) => [`/project/${file.path}`, file.content])),
1759
1680
  ...Object.fromEntries(Object.entries(dependencyFiles).filter(([, contents]) => typeof contents === "string").map(([path, contents]) => [`/project/${path}`, contents])),
@@ -1765,13 +1686,9 @@ async function transpileScriptProject(project, requestId) {
1765
1686
  ...Object.entries(dependencyFiles).filter(([path, contents]) => path.endsWith(".d.ts") && typeof contents === "string").map(([path]) => `/project/${path}`)
1766
1687
  ],
1767
1688
  outputs: outputPaths.map((path) => `/project/build/${path}`)
1768
- }) })).wait();
1769
- let response;
1770
- if (output.ok) try {
1771
- response = JSON.parse(output.stdout);
1772
- } catch {
1773
- response = void 0;
1774
- }
1689
+ }) });
1690
+ const output = await readProcessMessage(instance, completeJsonObject, { collectStderr: true }).finally(() => instance.free());
1691
+ const response = output.message;
1775
1692
  const files = {};
1776
1693
  if (response) for (const outputPath of outputPaths) {
1777
1694
  const contents = response.files[`/project/build/${outputPath}`];
@@ -1780,7 +1697,7 @@ async function transpileScriptProject(project, requestId) {
1780
1697
  Object.assign(files, dependencyFiles);
1781
1698
  return {
1782
1699
  files,
1783
- output,
1700
+ stderr: output.stderr,
1784
1701
  response
1785
1702
  };
1786
1703
  }
@@ -1813,10 +1730,10 @@ async function buildScript(project, cacheKey, requestId) {
1813
1730
  progress(requestId, "compiling", "Compiling TypeScript with TypeScript/WASI", .5);
1814
1731
  const transpiled = await transpileScriptProject(project, requestId);
1815
1732
  files = transpiled.files;
1816
- stderr = transpiled.output.stderr;
1733
+ stderr = transpiled.stderr;
1817
1734
  diagnostics = parseTypeScriptDiagnostics(transpiled.response?.diagnostics ?? "");
1818
1735
  const emittedOutputsPresent = emittedSourceFiles(project).every((file) => Object.hasOwn(files, emittedScriptPath(file.path)));
1819
- if (!transpiled.output.ok || !transpiled.response || transpiled.response.status !== 0 || !emittedOutputsPresent || diagnostics.some((diagnostic) => diagnostic.severity === "error")) return {
1736
+ if (!transpiled.response || transpiled.response.status !== 0 || !emittedOutputsPresent || diagnostics.some((diagnostic) => diagnostic.severity === "error")) return {
1820
1737
  success: false,
1821
1738
  diagnostics: ensureFailureDiagnostic(diagnostics, {
1822
1739
  file: project.config.entry,
@@ -1,5 +1,5 @@
1
1
  import { D as PYTHON_COMPRESSED_PACKAGE_SHA256, O as PYTHON_PACKAGE, k as PYTHON_PACKAGE_SHA256 } from "./chunks/toolchains-uz3XbZGI.js";
2
- import { t as withProcessKeepalive } from "./chunks/process-keepalive-BWLUz55N.js";
2
+ import { r as readProcessMessage, t as withProcessKeepalive } from "./chunks/process-keepalive-BaXpMTap.js";
3
3
  import { writeFileSync } from "node:fs";
4
4
  import { serialize } from "node:v8";
5
5
  import { readFile } from "node:fs/promises";
@@ -7,6 +7,71 @@ import { createHash } from "node:crypto";
7
7
  import path from "node:path";
8
8
  import { gunzipSync } from "node:zlib";
9
9
  import { Runtime, Wasmer, init } from "@wasmer/sdk/node";
10
+ import { WASM_OJ_STORAGE } from "@wasm-oj/contracts";
11
+ //#region src/runner/runtime-files.ts
12
+ var MAGIC = new TextEncoder().encode("WOJFS002");
13
+ var HEADER_BYTES = 12;
14
+ `${WASM_OJ_STORAGE.runtimeFilesCache}`;
15
+ String.raw`
16
+ import io
17
+ import os
18
+ import sys
19
+ import zipfile
20
+
21
+ source_root = "/usr/local/lib/python3.14"
22
+ guest_path = "/cpython/lib/python314.zip"
23
+ output = sys.stdout.buffer
24
+ output.write(b"WOJFS002")
25
+
26
+ archive_buffer = io.BytesIO()
27
+ with zipfile.ZipFile(archive_buffer, "w", compression=zipfile.ZIP_STORED) as archive:
28
+ for root, directories, files in os.walk(source_root):
29
+ directories[:] = sorted(
30
+ name for name in directories
31
+ if name != "__pycache__" and not os.path.islink(os.path.join(root, name))
32
+ )
33
+ for name in sorted(files):
34
+ source_path = os.path.join(root, name)
35
+ if os.path.islink(source_path) or name.endswith((".pyc", ".pyo")):
36
+ continue
37
+ archive_path = os.path.relpath(source_path, source_root).replace(os.sep, "/")
38
+ info = zipfile.ZipInfo(archive_path, date_time=(1980, 1, 1, 0, 0, 0))
39
+ info.compress_type = zipfile.ZIP_STORED
40
+ info.external_attr = 0o100644 << 16
41
+ with open(source_path, "rb") as source:
42
+ archive.writestr(info, source.read())
43
+
44
+ encoded_path = guest_path.encode("utf-8")
45
+ archive_data = archive_buffer.getvalue()
46
+ output.write(len(encoded_path).to_bytes(4, "little"))
47
+ output.write(len(archive_data).to_bytes(8, "little"))
48
+ output.write(encoded_path)
49
+ output.write(archive_data)
50
+
51
+ output.write((0).to_bytes(4, "little"))
52
+ output.write((0).to_bytes(8, "little"))
53
+ `;
54
+ /**
55
+ * Read an export until stdout holds a complete archive and return exactly its framed bytes.
56
+ * `@wasmer/sdk` `Instance.wait()` can hang after all output arrived; see readProcessMessage.
57
+ */
58
+ async function readRuntimeFilesExport(exporter, idleGraceMs) {
59
+ const { message, stderr } = await readProcessMessage(exporter, completeArchive, { idleGraceMs });
60
+ if (message === void 0) throw new Error(`The runtime file export ended before its archive was complete: ${stderr}`);
61
+ return message;
62
+ }
63
+ function completeArchive(bytes) {
64
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
65
+ let offset = MAGIC.byteLength;
66
+ while (offset + HEADER_BYTES <= bytes.byteLength) {
67
+ const pathLength = view.getUint32(offset, true);
68
+ const dataLength = Number(view.getBigUint64(offset + 4, true));
69
+ offset += HEADER_BYTES;
70
+ if (pathLength === 0 && dataLength === 0) return bytes.slice(0, offset);
71
+ offset += pathLength + dataLength;
72
+ }
73
+ }
74
+ //#endregion
10
75
  //#region src/server/server-runner-stage.mjs
11
76
  var MAX_INPUT_BYTES = 1048576;
12
77
  var MAX_RESULT_BYTES = 268435456;
@@ -43,9 +108,13 @@ try {
43
108
  PYTHONDONTWRITEBYTECODE: "1"
44
109
  }
45
110
  }));
46
- const output = await withProcessKeepalive(instance.wait());
47
- if (!output.ok) throw new Error(`Unable to export runtime files from ${input.request.packageSpecifier}: exit ${output.code}: ${boundedText(output.stderr)}`);
48
- bytes = output.stdoutBytes.slice();
111
+ try {
112
+ bytes = await withProcessKeepalive(readRuntimeFilesExport(instance));
113
+ } catch (error) {
114
+ throw new Error(`Unable to export runtime files from ${input.request.packageSpecifier}: ${boundedText(errorText(error))}`);
115
+ } finally {
116
+ instance.free();
117
+ }
49
118
  }
50
119
  if (bytes.byteLength > MAX_RESULT_BYTES) throw new Error(`Runner stage result is ${bytes.byteLength} bytes; the limit is ${MAX_RESULT_BYTES} bytes.`);
51
120
  response = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wasm-oj/server",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Node.js compiler and runner host for the WASM-OJ SDK.",
5
5
  "license": "MIT",
6
6
  "author": "JacobLinCool",
@@ -58,8 +58,8 @@
58
58
  "node": ">=24.18.0 <25"
59
59
  },
60
60
  "dependencies": {
61
- "@wasm-oj/contracts": "0.2.2",
62
- "@wasm-oj/core": "0.2.2",
61
+ "@wasm-oj/contracts": "0.2.4",
62
+ "@wasm-oj/core": "0.2.4",
63
63
  "@wasmer/sdk": "0.10.0",
64
64
  "fflate": "0.8.3"
65
65
  },