// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // deno-lint-ignore-file no-console // Requires to be run with `--allow-net` flag import { assert, assertEquals, assertMatch, assertThrows } from "@std/assert"; import { toFileUrl } from "@std/path/to-file-url"; function resolveWorker(worker: string): string { return import.meta.resolve(`../testdata/workers/${worker}`); } Deno.test( { permissions: { read: true } }, function utimeSyncFileSuccess() { const w = new Worker( resolveWorker("worker_types.ts"), { type: "module" }, ); assert(w); w.terminate(); }, ); Deno.test({ name: "worker terminate", fn: async function () { const jsWorker = new Worker( resolveWorker("test_worker.js"), { type: "module" }, ); const tsWorker = new Worker( resolveWorker("test_worker.ts"), { type: "module", name: "tsWorker" }, ); const deferred1 = Promise.withResolvers(); jsWorker.onmessage = (e) => { deferred1.resolve(e.data); }; const deferred2 = Promise.withResolvers(); tsWorker.onmessage = (e) => { deferred2.resolve(e.data); }; jsWorker.postMessage("Hello World"); assertEquals(await deferred1.promise, "Hello World"); tsWorker.postMessage("Hello World"); assertEquals(await deferred2.promise, "Hello World"); tsWorker.terminate(); jsWorker.terminate(); }, }); Deno.test({ name: "worker from data url", async fn() { const tsWorker = new Worker( "data:application/typescript;base64,aWYgKHNlbGYubmFtZSAhPT0gInRzV29ya2VyIikgewogIHRocm93IEVycm9yKGBJbnZhbGlkIHdvcmtlciBuYW1lOiAke3NlbGYubmFtZX0sIGV4cGVjdGVkIHRzV29ya2VyYCk7Cn0KCm9ubWVzc2FnZSA9IGZ1bmN0aW9uIChlKTogdm9pZCB7CiAgcG9zdE1lc3NhZ2UoZS5kYXRhKTsKICBjbG9zZSgpOwp9Owo=", { type: "module", name: "tsWorker" }, ); const { promise, resolve } = Promise.withResolvers(); tsWorker.onmessage = (e) => { resolve(e.data); }; tsWorker.postMessage("Hello World"); assertEquals(await promise, "Hello World"); tsWorker.terminate(); }, }); Deno.test({ name: "worker nested", fn: async function () { const nestedWorker = new Worker( resolveWorker("nested_worker.js"), { type: "module", name: "nested" }, ); // deno-lint-ignore no-explicit-any const { promise, resolve } = Promise.withResolvers(); nestedWorker.onmessage = (e) => { resolve(e.data); }; nestedWorker.postMessage("Hello World"); assertEquals(await promise, { type: "msg", text: "Hello World" }); nestedWorker.terminate(); }, }); Deno.test({ name: "worker throws when executing", fn: async function () { const throwingWorker = new Worker( resolveWorker("throwing_worker.js"), { type: "module" }, ); const { promise, resolve } = Promise.withResolvers(); // deno-lint-ignore no-explicit-any throwingWorker.onerror = (e: any) => { e.preventDefault(); resolve(e.message); }; assertMatch( await promise as string, /Uncaught \(in promise\) Error: Thrown error/, ); throwingWorker.terminate(); }, }); Deno.test({ name: "worker globals", fn: async function () { const workerOptions: WorkerOptions = { type: "module" }; const w = new Worker( resolveWorker("worker_globals.ts"), workerOptions, ); const { promise, resolve } = Promise.withResolvers(); w.onmessage = (e) => { resolve(e.data); }; w.postMessage("Hello, world!"); assertEquals(await promise, "true, true, true, true"); w.terminate(); }, }); Deno.test({ name: "worker navigator", fn: async function () { const workerOptions: WorkerOptions = { type: "module" }; const w = new Worker( resolveWorker("worker_navigator.ts"), workerOptions, ); const { promise, resolve } = Promise.withResolvers(); w.onmessage = (e) => { resolve(e.data); }; w.postMessage("Hello, world!"); assertEquals(await promise, "string, object, string, number"); w.terminate(); }, }); Deno.test({ name: "worker fetch API", fn: async function () { const fetchingWorker = new Worker( resolveWorker("fetching_worker.js"), { type: "module" }, ); const { promise, resolve, reject } = Promise.withResolvers(); // deno-lint-ignore no-explicit-any fetchingWorker.onerror = (e: any) => { e.preventDefault(); reject(e.message); }; // Defer promise.resolve() to allow worker to shut down fetchingWorker.onmessage = (e) => { resolve(e.data); }; assertEquals(await promise, "Done!"); fetchingWorker.terminate(); }, }); Deno.test({ name: "worker terminate busy loop", fn: async function () { const { promise, resolve } = Promise.withResolvers(); const busyWorker = new Worker( resolveWorker("busy_worker.js"), { type: "module" }, ); let testResult = 0; busyWorker.onmessage = (e) => { testResult = e.data; if (testResult >= 10000) { busyWorker.terminate(); busyWorker.onmessage = (_e) => { throw new Error("unreachable"); }; setTimeout(() => { resolve(testResult); }, 100); } }; busyWorker.postMessage("ping"); assertEquals(await promise, 10000); }, }); Deno.test({ name: "worker race condition", fn: async function () { // See issue for details // https://github.com/denoland/deno/issues/4080 const { promise, resolve } = Promise.withResolvers(); const racyWorker = new Worker( resolveWorker("racy_worker.js"), { type: "module" }, ); racyWorker.onmessage = (_e) => { setTimeout(() => { resolve(); }, 100); }; racyWorker.postMessage("START"); await promise; }, }); Deno.test({ name: "worker is event listener", fn: async function () { let messageHandlersCalled = 0; let errorHandlersCalled = 0; const deferred1 = Promise.withResolvers(); const deferred2 = Promise.withResolvers(); const worker = new Worker( resolveWorker("event_worker.js"), { type: "module" }, ); worker.onmessage = (_e: Event) => { messageHandlersCalled++; }; worker.addEventListener("message", (_e: Event) => { messageHandlersCalled++; }); worker.addEventListener("message", (_e: Event) => { messageHandlersCalled++; deferred1.resolve(); }); worker.onerror = (e) => { errorHandlersCalled++; e.preventDefault(); }; worker.addEventListener("error", (_e: Event) => { errorHandlersCalled++; }); worker.addEventListener("error", (_e: Event) => { errorHandlersCalled++; deferred2.resolve(); }); worker.postMessage("ping"); await deferred1.promise; assertEquals(messageHandlersCalled, 3); worker.postMessage("boom"); await deferred2.promise; assertEquals(errorHandlersCalled, 3); worker.terminate(); }, }); Deno.test({ name: "worker scope is event listener", fn: async function () { const worker = new Worker( resolveWorker("event_worker_scope.js"), { type: "module" }, ); // deno-lint-ignore no-explicit-any const { promise, resolve } = Promise.withResolvers(); worker.onmessage = (e: MessageEvent) => { resolve(e.data); }; worker.onerror = (_e) => { throw new Error("unreachable"); }; worker.postMessage("boom"); worker.postMessage("ping"); assertEquals(await promise, { messageHandlersCalled: 4, errorHandlersCalled: 4, }); worker.terminate(); }, }); Deno.test({ name: "worker with Deno namespace", fn: async function () { const denoWorker = new Worker( resolveWorker("deno_worker.ts"), { type: "module", deno: { permissions: "inherit" } }, ); const { promise, resolve } = Promise.withResolvers(); denoWorker.onmessage = (e) => { denoWorker.terminate(); resolve(e.data); }; denoWorker.postMessage("Hello World"); assertEquals(await promise, "Hello World"); }, }); Deno.test({ name: "worker with crypto in scope", fn: async function () { const w = new Worker( resolveWorker("worker_crypto.js"), { type: "module" }, ); const { promise, resolve } = Promise.withResolvers(); w.onmessage = (e) => { resolve(e.data); }; w.postMessage(null); assertEquals(await promise, true); w.terminate(); }, }); Deno.test({ name: "Worker event handler order", fn: async function () { const { promise, resolve } = Promise.withResolvers(); const w = new Worker( resolveWorker("test_worker.ts"), { type: "module", name: "tsWorker" }, ); const arr: number[] = []; w.addEventListener("message", () => arr.push(1)); w.onmessage = (_e) => { arr.push(2); }; w.addEventListener("message", () => arr.push(3)); w.addEventListener("message", () => { resolve(); }); w.postMessage("Hello World"); await promise; assertEquals(arr, [1, 2, 3]); w.terminate(); }, }); Deno.test({ name: "Worker immediate close", fn: async function () { const { promise, resolve } = Promise.withResolvers(); const w = new Worker( resolveWorker("immediately_close_worker.js"), { type: "module" }, ); setTimeout(() => { resolve(); }, 1000); await promise; w.terminate(); }, }); Deno.test({ name: "Worker post undefined", fn: async function () { const { promise, resolve } = Promise.withResolvers(); const worker = new Worker( resolveWorker("post_undefined.ts"), { type: "module" }, ); const handleWorkerMessage = (e: MessageEvent) => { console.log("main <- worker:", e.data); worker.terminate(); resolve(); }; worker.addEventListener("messageerror", () => console.log("message error")); worker.addEventListener("error", () => console.log("error")); worker.addEventListener("message", handleWorkerMessage); console.log("\npost from parent"); worker.postMessage(undefined); await promise; }, }); Deno.test("Worker inherits permissions", async function () { const worker = new Worker( resolveWorker("read_check_worker.js"), { type: "module", deno: { permissions: "inherit" } }, ); const { promise, resolve } = Promise.withResolvers(); worker.onmessage = (e) => { resolve(e.data); }; worker.postMessage(null); assertEquals(await promise, true); worker.terminate(); }); Deno.test("Worker limit children permissions", async function () { const worker = new Worker( resolveWorker("read_check_worker.js"), { type: "module", deno: { permissions: { read: false } } }, ); const { promise, resolve } = Promise.withResolvers(); worker.onmessage = (e) => { resolve(e.data); }; worker.postMessage(null); assertEquals(await promise, false); worker.terminate(); }); function setupReadCheckGranularWorkerTest() { const tempDir = Deno.realPathSync(Deno.makeTempDirSync()); const initialPath = Deno.env.get("PATH")!; const initialCwd = Deno.cwd(); Deno.chdir(tempDir); const envSep = Deno.build.os === "windows" ? ";" : ":"; Deno.env.set("PATH", initialPath + envSep + tempDir); // create executables that will be resolved when doing `which` const ext = Deno.build.os === "windows" ? ".exe" : ""; Deno.copyFileSync(Deno.execPath(), tempDir + "/bar" + ext); return { tempDir, runFooFilePath: tempDir + "/foo" + ext, [Symbol.dispose]() { Deno.removeSync(tempDir, { recursive: true }); Deno.env.set("PATH", initialPath); Deno.chdir(initialCwd); }, }; } Deno.test("Worker limit children permissions granularly", async function () { const ctx = setupReadCheckGranularWorkerTest(); const workerUrl = resolveWorker("read_check_granular_worker.js"); const worker = new Worker( workerUrl, { type: "module", deno: { permissions: { env: ["foo"], net: ["foo", "bar:8000"], ffi: [new URL("foo", workerUrl), "bar"], read: [new URL("foo", workerUrl), "bar", ctx.tempDir], run: [ toFileUrl(ctx.runFooFilePath), "bar", "./baz", "unresolved-exec", ], write: [new URL("foo", workerUrl), "bar"], }, }, }, ); // deno-lint-ignore no-explicit-any const { promise, resolve } = Promise.withResolvers(); worker.onmessage = ({ data }) => resolve(data); assertEquals(await promise, { envGlobal: "prompt", envFoo: "granted", envAbsent: "prompt", netGlobal: "prompt", netFoo: "granted", netFoo8000: "granted", netBar: "prompt", netBar8000: "granted", ffiGlobal: "prompt", ffiFoo: "granted", ffiBar: "granted", ffiAbsent: "prompt", readGlobal: "prompt", readFoo: "granted", readBar: "granted", readAbsent: "prompt", runGlobal: "prompt", runFoo: "granted", runFooPath: "granted", runBar: "granted", runBaz: "granted", runUnresolved: "prompt", // unresolved binaries remain as "prompt" runAbsent: "prompt", writeGlobal: "prompt", writeFoo: "granted", writeBar: "granted", writeAbsent: "prompt", }); worker.terminate(); }); Deno.test("Nested worker limit children permissions", async function () { const _cleanup = setupReadCheckGranularWorkerTest(); /** This worker has permissions but doesn't grant them to its children */ const worker = new Worker( resolveWorker("parent_read_check_worker.js"), { type: "module", deno: { permissions: "inherit" } }, ); // deno-lint-ignore no-explicit-any const { promise, resolve } = Promise.withResolvers(); worker.onmessage = ({ data }) => resolve(data); assertEquals(await promise, { envGlobal: "prompt", envFoo: "prompt", envAbsent: "prompt", netGlobal: "prompt", netFoo: "prompt", netFoo8000: "prompt", netBar: "prompt", netBar8000: "prompt", ffiGlobal: "prompt", ffiFoo: "prompt", ffiBar: "prompt", ffiAbsent: "prompt", readGlobal: "prompt", readFoo: "prompt", readBar: "prompt", readAbsent: "prompt", runGlobal: "prompt", runFoo: "prompt", runFooPath: "prompt", runBar: "prompt", runBaz: "prompt", runUnresolved: "prompt", runAbsent: "prompt", writeGlobal: "prompt", writeFoo: "prompt", writeBar: "prompt", writeAbsent: "prompt", }); worker.terminate(); }); // This test relies on env permissions not being granted on main thread Deno.test({ name: "Worker initialization throws on worker permissions greater than parent thread permissions", permissions: { env: false }, fn: function () { assertThrows( () => { const worker = new Worker( resolveWorker("deno_worker.ts"), { type: "module", deno: { permissions: { env: true } } }, ); worker.terminate(); }, Deno.errors.NotCapable, "Can't escalate parent thread permissions", ); }, }); Deno.test("Worker with disabled permissions", async function () { const worker = new Worker( resolveWorker("no_permissions_worker.js"), { type: "module", deno: { permissions: "none" } }, ); const { promise, resolve } = Promise.withResolvers(); worker.onmessage = (e) => { resolve(e.data); }; worker.postMessage(null); assertEquals(await promise, true); worker.terminate(); }); Deno.test("Worker permissions are not inherited with empty permission object", async function () { const worker = new Worker( resolveWorker("permission_echo.js"), { type: "module", deno: { permissions: {} } }, ); // deno-lint-ignore no-explicit-any const { promise, resolve } = Promise.withResolvers(); worker.onmessage = (e) => { resolve(e.data); }; worker.postMessage(null); assertEquals(await promise, { env: "prompt", net: "prompt", ffi: "prompt", read: "prompt", run: "prompt", write: "prompt", }); worker.terminate(); }); Deno.test("Worker permissions are not inherited with single specified permission", async function () { const worker = new Worker( resolveWorker("permission_echo.js"), { type: "module", deno: { permissions: { net: true } } }, ); // deno-lint-ignore no-explicit-any const { promise, resolve } = Promise.withResolvers(); worker.onmessage = (e) => { resolve(e.data); }; worker.postMessage(null); assertEquals(await promise, { env: "prompt", net: "granted", ffi: "prompt", read: "prompt", run: "prompt", write: "prompt", }); worker.terminate(); }); Deno.test("Worker with invalid permission arg", function () { assertThrows( () => new Worker(`data:,close();`, { type: "module", // @ts-expect-error invalid env value deno: { permissions: { env: "foo" } }, }), TypeError, '(deno.permissions.env) invalid value: string "foo", expected "inherit" or boolean or string[]', ); }); Deno.test({ name: "worker location", fn: async function () { const { promise, resolve } = Promise.withResolvers(); const workerModuleHref = resolveWorker("worker_location.ts"); const w = new Worker(workerModuleHref, { type: "module" }); w.onmessage = (e) => { resolve(e.data); }; w.postMessage("Hello, world!"); assertEquals(await promise, `${workerModuleHref}, true`); w.terminate(); }, }); Deno.test({ name: "Worker with top-level-await", fn: async function () { const { promise, resolve, reject } = Promise.withResolvers(); const worker = new Worker( resolveWorker("worker_with_top_level_await.ts"), { type: "module" }, ); worker.onmessage = (e) => { if (e.data == "ready") { worker.postMessage("trigger worker handler"); } else if (e.data == "triggered worker handler") { resolve(); } else { reject(new Error("Handler didn't run during top-level delay.")); } }; await promise; worker.terminate(); }, }); Deno.test({ name: "Worker with native HTTP", fn: async function () { const { promise, resolve } = Promise.withResolvers(); const worker = new Worker( resolveWorker("http_worker.js"), { type: "module", deno: { permissions: "inherit" } }, ); worker.onmessage = () => { resolve(); }; await promise; assert(worker); const response = await fetch("http://localhost:4506"); assert(await response.bytes()); worker.terminate(); }, }); Deno.test({ name: "structured cloning postMessage", fn: async function () { const worker = new Worker( resolveWorker("worker_structured_cloning.ts"), { type: "module" }, ); // deno-lint-ignore no-explicit-any const { promise, resolve } = Promise.withResolvers(); worker.onmessage = (e) => { resolve(e.data); }; worker.postMessage("START"); const data = await promise; // self field should reference itself (circular ref) assert(data === data.self); // fields a and b refer to the same array assertEquals(data.a, ["a", true, 432]); assertEquals(data.b, ["a", true, 432]); data.b[0] = "b"; data.a[2] += 5; assertEquals(data.a, ["b", true, 437]); assertEquals(data.b, ["b", true, 437]); // c is a set const len = data.c.size; data.c.add(1); // This value is already in the set. data.c.add(2); assertEquals(len + 1, data.c.size); worker.terminate(); }, }); Deno.test({ name: "worker with relative specifier", fn: async function () { assertEquals(location.href, "http://127.0.0.1:4545/"); const w = new Worker( "./workers/test_worker.ts", { type: "module", name: "tsWorker" }, ); const { promise, resolve } = Promise.withResolvers(); w.onmessage = (e) => { resolve(e.data); }; w.postMessage("Hello, world!"); assertEquals(await promise, "Hello, world!"); w.terminate(); }, }); Deno.test({ name: "worker SharedArrayBuffer", fn: async function () { const { promise, resolve } = Promise.withResolvers(); const workerOptions: WorkerOptions = { type: "module" }; const w = new Worker( resolveWorker("shared_array_buffer.ts"), workerOptions, ); const sab1 = new SharedArrayBuffer(1); const sab2 = new SharedArrayBuffer(1); const bytes1 = new Uint8Array(sab1); const bytes2 = new Uint8Array(sab2); assertEquals(bytes1[0], 0); assertEquals(bytes2[0], 0); w.onmessage = () => { w.postMessage([sab1, sab2]); w.onmessage = () => { resolve(); }; }; await promise; assertEquals(bytes1[0], 1); assertEquals(bytes2[0], 2); w.terminate(); }, }); Deno.test({ name: "Send MessagePorts from / to workers", fn: async function () { const worker = new Worker( resolveWorker("message_port.ts"), { type: "module" }, ); const channel = new MessageChannel(); // deno-lint-ignore no-explicit-any const deferred1 = Promise.withResolvers(); const deferred2 = Promise.withResolvers(); const deferred3 = Promise.withResolvers(); const result = Promise.withResolvers(); worker.onmessage = (e) => { deferred1.resolve([e.data, e.ports.length]); const port1 = e.ports[0]; port1.onmessage = (e) => { deferred2.resolve(e.data); port1.close(); worker.postMessage("3", [channel.port1]); }; port1.postMessage("2"); }; channel.port2.onmessage = (e) => { deferred3.resolve(e.data); channel.port2.close(); result.resolve(); }; assertEquals(await deferred1.promise, ["1", 1]); assertEquals(await deferred2.promise, true); assertEquals(await deferred3.promise, true); await result.promise; worker.terminate(); }, }); Deno.test({ name: "worker Deno.memoryUsage", fn: async function () { const w = new Worker( /** * Source code * self.onmessage = function() {self.postMessage(Deno.memoryUsage())} */ "data:application/typescript;base64,c2VsZi5vbm1lc3NhZ2UgPSBmdW5jdGlvbigpIHtzZWxmLnBvc3RNZXNzYWdlKERlbm8ubWVtb3J5VXNhZ2UoKSl9", { type: "module", name: "tsWorker" }, ); w.postMessage(null); // deno-lint-ignore no-explicit-any const { promise, resolve } = Promise.withResolvers(); w.onmessage = function (evt) { resolve(evt.data); }; assertEquals( Object.keys( await promise as unknown as Record, ), ["rss", "heapTotal", "heapUsed", "external"], ); w.terminate(); }, });