Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,9 @@ declare module "bun" {
ref?: boolean;

/**
* In Bun, this does nothing.
* `"classic"` or `"module"`. Any other value throws a `TypeError`, as in browsers.
*
* Otherwise this has no effect in Bun: every worker runs as an ES module.
*/
type?: Bun.WorkerType | undefined;

Expand All @@ -683,7 +685,9 @@ declare module "bun" {
env?: Record<string, string> | (typeof import("node:worker_threads"))["SHARE_ENV"] | undefined;

/**
* In Bun, this does nothing.
* `"omit"`, `"same-origin"` or `"include"`. Any other value throws a `TypeError`, as in browsers.
*
* Otherwise this has no effect in Bun: worker scripts are not fetched with credentials.
*/
credentials?: import("undici-types").RequestCredentials | undefined;

Expand Down
31 changes: 31 additions & 0 deletions src/jsc/bindings/webcore/JSWorker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,22 @@ STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSWorkerPrototype, JSWorkerPrototype::Base);

using JSWorkerDOMConstructor = JSDOMConstructor<JSWorker>;

// A WebIDL enumeration member of WorkerOptions: absent or undefined passes, anything else must
// stringify to one of `allowed` or a TypeError is thrown and false returned.
Comment thread
robobun marked this conversation as resolved.
static bool validateEnumerationOption(JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& throwScope, JSValue value, ASCIILiteral name, std::span<const ASCIILiteral> allowed)
{
if (!value || value.isUndefined())
return true;
auto string = value.toWTFString(lexicalGlobalObject);
RETURN_IF_EXCEPTION(throwScope, false);
for (auto literal : allowed) {
if (string == literal)
return true;
}
Bun::ERR::INVALID_ARG_VALUE(throwScope, lexicalGlobalObject, name, "must be one of: "_s, value, allowed);
return false;
}

template<> __attribute__((minsize)) JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSWorkerDOMConstructor::construct(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame)
{
auto& vm = JSC::getVM(lexicalGlobalObject);
Expand Down Expand Up @@ -183,6 +199,21 @@ template<> __attribute__((minsize)) JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES
}
}

// Valid values have no further effect (every worker is an ES module, nothing is fetched with
// credentials). node:worker_threads has neither option and keeps ignoring them.
Comment thread
robobun marked this conversation as resolved.
if (options.kind == WorkerOptions::Kind::Web) {
static constexpr ASCIILiteral workerTypes[] = { "classic"_s, "module"_s };
static constexpr ASCIILiteral requestCredentials[] = { "omit"_s, "same-origin"_s, "include"_s };
auto typeValue = optionsObject->getIfPropertyExists(lexicalGlobalObject, vm.propertyNames->type);
RETURN_IF_EXCEPTION(throwScope, {});
if (!validateEnumerationOption(lexicalGlobalObject, throwScope, typeValue, "options.type"_s, workerTypes))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return {};
auto credentialsValue = optionsObject->getIfPropertyExists(lexicalGlobalObject, Identifier::fromString(vm, "credentials"_s));
RETURN_IF_EXCEPTION(throwScope, {});
if (!validateEnumerationOption(lexicalGlobalObject, throwScope, credentialsValue, "options.credentials"_s, requestCredentials))
return {};
}

auto miniModeValue = optionsObject->getIfPropertyExists(lexicalGlobalObject, Identifier::fromString(vm, "smol"_s));
RETURN_IF_EXCEPTION(throwScope, {});
if (miniModeValue) {
Expand Down
45 changes: 44 additions & 1 deletion test/js/web/workers/worker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,47 @@ describe("web worker", () => {
});
});

// WebIDL: WorkerOptions.type is a WorkerType and .credentials a RequestCredentials enumeration, so
// any other value is a TypeError at construction. Valid values have no further effect in Bun.
describe("type and credentials options", () => {
test.each([
[{ type: "zzz" }, "options.type"],
[{ type: null }, "options.type"],
[{ type: 1 }, "options.type"],
[{ credentials: "zzz" }, "options.credentials"],
])("new Worker(url, %j) throws a TypeError", (options, name) => {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
let error: any;
try {
new Worker("data:text/javascript,", options as any);
} catch (e) {
error = e;
}
expect(error).toBeInstanceOf(TypeError);
expect(error.message).toStartWith(`The property '${name}' must be one of:`);
});

test("valid or undefined values are accepted", async () => {
for (const options of [
{ type: "module" },
{ type: "classic" },
{ type: undefined },
{ credentials: "omit" },
{ credentials: "same-origin" },
{ credentials: "include" },
] satisfies WorkerOptions[]) {
const worker = new Worker("data:text/javascript,", options);
await once(worker, "close");
}
});

// node:worker_threads has neither option. Unknown keys are ignored there, as in Node.
test("worker_threads.Worker does not validate them", async () => {
const worker = new wt.Worker(new URL("data:text/javascript,"), { type: "zzz", credentials: "zzz" } as any);
const [code] = await once(worker, "exit");
expect(code).toBe(0);
});
});

describe("error event", () => {
test("is fired with a string of the error", async () => {
const worker = new Worker("data:text/javascript,throw 5");
Expand Down Expand Up @@ -679,6 +720,8 @@ describe("web worker", () => {
// after the request must release, not build script values under it.
test("terminate() while fs.readFile completions keep arriving", async () => {
using dir = tempDir("worker-readfile-churn", { "f.bin": Buffer.alloc(65536, 7) });
// Each round boots 4 workers; a debug build spends ~0.5s per round, so it runs fewer.
const rounds = isDebug ? 4 : 12;
await using proc = Bun.spawn({
cmd: [
bunExe(),
Expand All @@ -687,7 +730,7 @@ describe("web worker", () => {
let n = 0; (function pump(){ while (n < 16) { n++; readFile(\${JSON.stringify(process.argv[1])}, () => { n--; setImmediate(pump) }) } })();
postMessage("busy")\`;
const url = URL.createObjectURL(new Blob([src]));
for (let r = 0; r < 12; r++) await Promise.all(Array.from({ length: 4 }, (_, i) => new Promise(res => {
for (let r = 0; r < ${rounds}; r++) await Promise.all(Array.from({ length: 4 }, (_, i) => new Promise(res => {
const w = new Worker(url); w.addEventListener("close", res); w.onmessage = () => setTimeout(() => w.terminate(), (r + i) % 10) })));
console.log("PASS");`,
path.join(String(dir), "f.bin"),
Expand Down
Loading