diff --git a/.changeset/native-worker-threads.md b/.changeset/native-worker-threads.md new file mode 100644 index 000000000000..1aec8050ecab --- /dev/null +++ b/.changeset/native-worker-threads.md @@ -0,0 +1,7 @@ +--- +"@cloudflare/unenv-preset": minor +--- + +Add support for native `node:worker_threads` module from workerd when the `enable_nodejs_worker_threads_module` compatibility flag is enabled. + +This feature is currently experimental and requires `nodejs_compat`, `experimental`, and `enable_nodejs_worker_threads_module` compatibility flags to be set. diff --git a/packages/unenv-preset/package.json b/packages/unenv-preset/package.json index 7f1f211aaf2c..49b8fd91cdf7 100644 --- a/packages/unenv-preset/package.json +++ b/packages/unenv-preset/package.json @@ -49,7 +49,7 @@ }, "peerDependencies": { "unenv": "2.0.0-rc.24", - "workerd": "^1.20260213.0" + "workerd": "^1.20260214.0" }, "peerDependenciesMeta": { "workerd": { diff --git a/packages/unenv-preset/src/preset.ts b/packages/unenv-preset/src/preset.ts index b3ac78682092..a3ca8ed18e05 100644 --- a/packages/unenv-preset/src/preset.ts +++ b/packages/unenv-preset/src/preset.ts @@ -83,6 +83,7 @@ export function getCloudflarePreset({ const v8Overrides = getV8Overrides(compat); const ttyOverrides = getTtyOverrides(compat); const childProcessOverrides = getChildProcessOverrides(compat); + const workerThreadsOverrides = getWorkerThreadsOverrides(compat); // "dynamic" as they depend on the compatibility date and flags const dynamicNativeModules = [ @@ -107,6 +108,7 @@ export function getCloudflarePreset({ ...v8Overrides.nativeModules, ...ttyOverrides.nativeModules, ...childProcessOverrides.nativeModules, + ...workerThreadsOverrides.nativeModules, ]; // "dynamic" as they depend on the compatibility date and flags @@ -131,6 +133,7 @@ export function getCloudflarePreset({ ...v8Overrides.hybridModules, ...ttyOverrides.hybridModules, ...childProcessOverrides.hybridModules, + ...workerThreadsOverrides.hybridModules, ]; return { @@ -1017,3 +1020,40 @@ function getChildProcessOverrides({ hybridModules: [], }; } + +/** + * Returns the overrides for `node:worker_threads` (unenv or workerd) + * + * The native worker_threads implementation: + * - can be enabled with the "enable_nodejs_worker_threads_module" flag + * - can be disabled with the "disable_nodejs_worker_threads_module" flag + * - is experimental (no default enable date) + */ +function getWorkerThreadsOverrides({ + compatibilityFlags, +}: { + compatibilityDate: string; + compatibilityFlags: string[]; +}): { nativeModules: string[]; hybridModules: string[] } { + const disabledByFlag = compatibilityFlags.includes( + "disable_nodejs_worker_threads_module" + ); + + const enabledByFlag = + compatibilityFlags.includes("enable_nodejs_worker_threads_module") && + compatibilityFlags.includes("experimental"); + + // worker_threads is experimental, no default enable date + const enabled = enabledByFlag && !disabledByFlag; + + // When enabled, use the native `worker_threads` module from workerd + return enabled + ? { + nativeModules: ["worker_threads"], + hybridModules: [], + } + : { + nativeModules: [], + hybridModules: [], + }; +} diff --git a/packages/wrangler/e2e/unenv-preset/preset.test.ts b/packages/wrangler/e2e/unenv-preset/preset.test.ts index 658bf294d587..1b87c48305b6 100644 --- a/packages/wrangler/e2e/unenv-preset/preset.test.ts +++ b/packages/wrangler/e2e/unenv-preset/preset.test.ts @@ -613,6 +613,30 @@ const localTestConfigs: TestConfig[] = [ }, }, ], + // node:worker_threads (experimental - no default enable date) + [ + // TODO: add test for disabled by date (no date defined yet) + // TODO: add test for enabled by date (no date defined yet) + { + name: "worker_threads disabled by default", + compatibilityDate: "2024-09-23", + compatibilityFlags: ["experimental"], + expectRuntimeFlags: { + enable_nodejs_worker_threads_module: false, + }, + }, + { + name: "worker_threads enabled by flag", + compatibilityDate: "2024-09-23", + compatibilityFlags: [ + "enable_nodejs_worker_threads_module", + "experimental", + ], + expectRuntimeFlags: { + enable_nodejs_worker_threads_module: true, + }, + }, + ], // node:repl (experimental, no default enable date) [ // TODO: add test for disabled by date (no date defined yet) @@ -745,15 +769,18 @@ describe.each(localTestConfigs)( test.for(Object.keys(WorkerdTests))( "%s", - { timeout: 5_000 }, + { timeout: 20_000 }, async (testName) => { // Retries the callback until it succeeds or times out. // Useful for the i.e. DNS tests where underlying requests might error/timeout. - await vi.waitFor(async () => { - const response = await fetch(`${url}/${testName}`); - const body = await response.text(); - expect(body).toMatch("passed"); - }); + await vi.waitFor( + async () => { + const response = await fetch(`${url}/${testName}`); + const body = await response.text(); + expect(body).toMatch("passed"); + }, + { timeout: 19_000, interval: 200 } + ); } ); } @@ -804,11 +831,14 @@ describe.runIf(Boolean(CLOUDFLARE_ACCOUNT_ID))( async (testName) => { // Retries the callback until it succeeds or times out. // Useful for the i.e. DNS tests where underlying requests might error/timeout. - await vi.waitFor(async () => { - const response = await fetch(`${url}/${testName}`); - const body = await response.text(); - expect(body).toMatch("passed"); - }); + await vi.waitFor( + async () => { + const response = await fetch(`${url}/${testName}`); + const body = await response.text(); + expect(body).toMatch("passed"); + }, + { timeout: 19_000, interval: 200 } + ); } ); } @@ -860,11 +890,14 @@ describe.runIf(Boolean(CLOUDFLARE_ACCOUNT_ID))( async (testName) => { // Retries the callback until it succeeds or times out. // Useful for the i.e. DNS tests where underlying requests might error/timeout. - await vi.waitFor(async () => { - const response = await fetch(`${url}/${testName}`); - const body = await response.text(); - expect(body).toMatch("passed"); - }); + await vi.waitFor( + async () => { + const response = await fetch(`${url}/${testName}`); + const body = await response.text(); + expect(body).toMatch("passed"); + }, + { timeout: 19_000, interval: 200 } + ); } ); } diff --git a/packages/wrangler/e2e/unenv-preset/worker/index.ts b/packages/wrangler/e2e/unenv-preset/worker/index.ts index ae767c2fdc8b..1eef830a4388 100644 --- a/packages/wrangler/e2e/unenv-preset/worker/index.ts +++ b/packages/wrangler/e2e/unenv-preset/worker/index.ts @@ -952,6 +952,87 @@ export const WorkerdTests: Record void> = { /not implemented|ERR_METHOD_NOT_IMPLEMENTED/ ); }, + + async testWorkerThreads() { + const workerThreads = await import("node:worker_threads"); + + // Common exports available in both unenv stub and native workerd + for (const target of [workerThreads, workerThreads.default]) { + assertTypeOfProperties(target, { + SHARE_ENV: "symbol", + getEnvironmentData: "function", + isMainThread: "boolean", + isMarkedAsUntransferable: "function", + markAsUntransferable: "function", + markAsUncloneable: "function", + moveMessagePortToContext: "function", + receiveMessageOnPort: "function", + setEnvironmentData: "function", + postMessageToThread: "function", + isInternalThread: "boolean", + BroadcastChannel: "function", + MessageChannel: "function", + Worker: "function", + }); + + // These are values, not functions + assert.strictEqual(target.parentPort, null, "parentPort should be null"); + assert.strictEqual(target.threadId, 0, "threadId should be 0"); + assert.strictEqual(target.workerData, null, "workerData should be null"); + assert.deepStrictEqual( + target.resourceLimits, + {}, + "resourceLimits should be empty object" + ); + assert.strictEqual( + target.isMainThread, + true, + "isMainThread should be true" + ); + assert.strictEqual( + (target as Record).isInternalThread, + false, + "isInternalThread should be false" + ); + } + + // Test SHARE_ENV symbol value + assert.strictEqual( + workerThreads.SHARE_ENV, + Symbol.for("nodejs.worker_threads.SHARE_ENV"), + "SHARE_ENV should be the correct symbol" + ); + + // Test getEnvironmentData/setEnvironmentData + workerThreads.setEnvironmentData("test-key", "test-value"); + assert.strictEqual( + workerThreads.getEnvironmentData("test-key"), + "test-value", + "getEnvironmentData should return the set value" + ); + + // Test MessageChannel creates ports + const channel = new workerThreads.MessageChannel(); + assert.ok(channel.port1, "MessageChannel should have port1"); + assert.ok(channel.port2, "MessageChannel should have port2"); + + // Test behavioral differences between native workerd and unenv stub + const isNative = + getRuntimeFlagValue("enable_nodejs_worker_threads_module") === true; + if (isNative) { + // Native workerd: Worker and BroadcastChannel constructors throw + assert.throws( + () => new workerThreads.Worker("test.js"), + /ERR_METHOD_NOT_IMPLEMENTED/, + "Worker constructor should throw ERR_METHOD_NOT_IMPLEMENTED" + ); + assert.throws( + () => new workerThreads.BroadcastChannel("test"), + /ERR_METHOD_NOT_IMPLEMENTED/, + "BroadcastChannel constructor should throw ERR_METHOD_NOT_IMPLEMENTED" + ); + } + }, }; /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 34626df02752..bd31ddad99a3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2540,8 +2540,8 @@ importers: specifier: 2.0.0-rc.24 version: 2.0.0-rc.24 workerd: - specifier: ^1.20260213.0 - version: 1.20260213.0 + specifier: ^1.20260214.0 + version: 1.20260217.0 devDependencies: '@types/node-unenv': specifier: npm:@types/node@^22.14.0 @@ -5221,12 +5221,6 @@ packages: cpu: [x64] os: [darwin] - '@cloudflare/workerd-darwin-64@1.20260213.0': - resolution: {integrity: sha512-eJZTKoID4uMvLG9LOuoAQzf7t1wZu1rVZSn61nSJgeeJnNUjjLkJlmbHdOdIfDiSVUl7aKMwEt5UySCgqVb1MA==} - engines: {node: '>=16'} - cpu: [x64] - os: [darwin] - '@cloudflare/workerd-darwin-64@1.20260217.0': resolution: {integrity: sha512-t1KRT0j4gwLntixMoNujv/UaS89Q7+MPRhkklaSup5tNhl3zBZOIlasBUSir69eXetqLZu8sypx3i7zE395XXA==} engines: {node: '>=16'} @@ -5239,12 +5233,6 @@ packages: cpu: [arm64] os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20260213.0': - resolution: {integrity: sha512-zwzUP271ibtILdm2JbU/TeEqroKCNE2+aOPcCcMcFwOPWIKR5c94+23Q3hKBk5lQf93aUfs8uvd6bHzh3I1Lug==} - engines: {node: '>=16'} - cpu: [arm64] - os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20260217.0': resolution: {integrity: sha512-9pEZ15BmELt0Opy79LTxUvbo55QAI4GnsnsvmgBxaQlc4P0dC8iycBGxbOpegkXnRx/LFj51l2zunfTo0EdATg==} engines: {node: '>=16'} @@ -5257,12 +5245,6 @@ packages: cpu: [x64] os: [linux] - '@cloudflare/workerd-linux-64@1.20260213.0': - resolution: {integrity: sha512-eVqObKq+xcHK6wLqZJ2bLNuv3d5moBWKtZ0ALW1zD1qYZqVvXN6EWfBw44are9Ph0XW0Ag6dxr4cifB7R7bkeQ==} - engines: {node: '>=16'} - cpu: [x64] - os: [linux] - '@cloudflare/workerd-linux-64@1.20260217.0': resolution: {integrity: sha512-IrZfxQ4b/4/RDQCJsyoxKrCR+cEqKl81yZOirMOKoRrDOmTjn4evYXaHoLBh2PjUKY1Imly7ZiC6G1p0xNIOwg==} engines: {node: '>=16'} @@ -5275,12 +5257,6 @@ packages: cpu: [arm64] os: [linux] - '@cloudflare/workerd-linux-arm64@1.20260213.0': - resolution: {integrity: sha512-X5zcLq20c0msaTxHJU+XWJyiEaGgJgFz+JQ7WLVatfh99RI6s+CdlQNeZKcJxwU1Jui2Epm576iKhgc5Ou/Qvg==} - engines: {node: '>=16'} - cpu: [arm64] - os: [linux] - '@cloudflare/workerd-linux-arm64@1.20260217.0': resolution: {integrity: sha512-RGU1wq69ym4sFBVWhQeddZrRrG0hJM/SlZ5DwVDga/zBJ3WXxcDsFAgg1dToDfildTde5ySXN7jAasSmWko9rg==} engines: {node: '>=16'} @@ -5293,12 +5269,6 @@ packages: cpu: [x64] os: [win32] - '@cloudflare/workerd-windows-64@1.20260213.0': - resolution: {integrity: sha512-VHKfx3Q9A10MDZBc3fAhCfbL2BCYjuF81zEm838eFFZsDu+EbSj1x48+PFQt1bl25aJ7T+a3qTq3OXBGiG+BLQ==} - engines: {node: '>=16'} - cpu: [x64] - os: [win32] - '@cloudflare/workerd-windows-64@1.20260217.0': resolution: {integrity: sha512-4T65u1321z1Zet9n7liQsSW7g3EXM5SWIT7kJ/uqkEtkPnIzZBIowMQgkvL5W9SpGZks9t3mTQj7hiUia8Gq9Q==} engines: {node: '>=16'} @@ -14819,11 +14789,6 @@ packages: engines: {node: '>=16'} hasBin: true - workerd@1.20260213.0: - resolution: {integrity: sha512-+TJv8CBfYfmfUggtTcOqNG85oGWO8GAKZtlwsUuaFyYvv4R6REMd0OPPLj7FsV3AxgcqlslelQAYg4lNMDJd1w==} - engines: {node: '>=16'} - hasBin: true - workerd@1.20260217.0: resolution: {integrity: sha512-6jVisS6wB6KbF+F9DVoDUy9p7MON8qZCFSaL8OcDUioMwknsUPFojUISu3/c30ZOZ24D4h7oqaahFc5C6huilw==} engines: {node: '>=16'} @@ -16347,45 +16312,30 @@ snapshots: '@cloudflare/workerd-darwin-64@1.20251210.0': optional: true - '@cloudflare/workerd-darwin-64@1.20260213.0': - optional: true - '@cloudflare/workerd-darwin-64@1.20260217.0': optional: true '@cloudflare/workerd-darwin-arm64@1.20251210.0': optional: true - '@cloudflare/workerd-darwin-arm64@1.20260213.0': - optional: true - '@cloudflare/workerd-darwin-arm64@1.20260217.0': optional: true '@cloudflare/workerd-linux-64@1.20251210.0': optional: true - '@cloudflare/workerd-linux-64@1.20260213.0': - optional: true - '@cloudflare/workerd-linux-64@1.20260217.0': optional: true '@cloudflare/workerd-linux-arm64@1.20251210.0': optional: true - '@cloudflare/workerd-linux-arm64@1.20260213.0': - optional: true - '@cloudflare/workerd-linux-arm64@1.20260217.0': optional: true '@cloudflare/workerd-windows-64@1.20251210.0': optional: true - '@cloudflare/workerd-windows-64@1.20260213.0': - optional: true - '@cloudflare/workerd-windows-64@1.20260217.0': optional: true @@ -26764,14 +26714,6 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20251210.0 '@cloudflare/workerd-windows-64': 1.20251210.0 - workerd@1.20260213.0: - optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20260213.0 - '@cloudflare/workerd-darwin-arm64': 1.20260213.0 - '@cloudflare/workerd-linux-64': 1.20260213.0 - '@cloudflare/workerd-linux-arm64': 1.20260213.0 - '@cloudflare/workerd-windows-64': 1.20260213.0 - workerd@1.20260217.0: optionalDependencies: '@cloudflare/workerd-darwin-64': 1.20260217.0