Skip to content

Turbopack: create worker loaders late so self-spawning workers don't deadlock - #97956

Open
sokra wants to merge 26 commits into
canaryfrom
fix/worker-availability-cycle
Open

sokra wants to merge 26 commits into
canaryfrom
fix/worker-availability-cycle

Conversation

@sokra

@sokra sokra commented Aug 26, 2026 •

Copy link
Copy Markdown
Member

Fixes #97399

Replaces #97470 (moved to a branch in this repository so deploy tests run).

What?

Fix a Turbopack production-build deadlock when a web worker spawns its own entry, and test that the resulting workers actually run in both dev and production.

Why?

The worker loader originally built every worker chunk group with root availability. A self-reference repeatedly regenerated a content-hashed chunk group, making the chunk path depend on its own content. The first iteration of this fix gave every worker the page's available modules and forwarded all page chunks to it, although a worker runs in a separate realm.

How?

  • Create worker loaders during chunking, when availability is known, with a stable module ID. A new worker spawned from a page starts with root availability and a self-contained entry; a nested worker inherits its parent worker's availability, breaking the self-reference cycle. Only nested workers re-import their parent's loaded chunks; page modules are not sent to first-level workers. Node worker threads keep their root-availability behavior.
  • The module containing new Worker(...) references the on-demand createWorker helper and passes its exported function to the late loader at runtime. The loader does not resolve a helper or require its worker target to implement ResolveOrigin (which fails for facade-wrapped modules).
  • Exclude Node worker entry assets from a parent server entry's dev HMR chunk-list versions, avoiding a separate self-reference loop in versioned_content() while keeping worker entry output unchanged.
  • Replace two build-only regression suites with one e2e suite: both a co-located worker and a worker in a separate module send a message from the page through worker 1 → a self-spawned worker 2 → worker 1 → the page. Both workers use a module shared with the client and a worker-only module.

Tests

The new e2e passes locally in Turbopack, webpack, and rspack dev/build+start, and Turbopack cache-components dev/build+start. Existing browser/Node worker suites, Rust workspace checks, snapshots, and execution tests pass. Deployed-mode coverage remains enabled.

@github-actions

github-actions Bot commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Stats from current PR

✅ No significant changes detected

📊 All Metrics
📖 Metrics Glossary

Dev Server Metrics:

  • Listen = TCP port starts accepting connections
  • First Request = HTTP server returns successful response
  • Cold = Fresh build (no cache)
  • Warm = With cached build artifacts

Build Metrics:

  • Fresh = Clean build (no .next directory)
  • Cached = With existing .next directory

Change Thresholds:

  • Time: Changes < 50ms AND < 10%, OR < 2% are insignificant
  • Size: Changes < 1KB AND < 1% are insignificant
  • All other changes are flagged to catch regressions

⚡ Dev Server

Metric Canary PR Change Trend
Cold (Listen) 814ms 813ms ✓ ▂▂▂▂█
Cold (Ready in log) 822ms 818ms ✓ ▁▁▁▁█
Cold (First Request) 1.345s 1.329s ✓ ▁▁▁▁█
Warm (Listen) 815ms 814ms ✓ ▂▁▂▂█
Warm (Ready in log) 819ms 807ms ✓ ▁▁▁▁█
Warm (First Request) 1.336s 1.303s ✓ ▁▁▁▁█
📦 Dev Server (Webpack) (Legacy)

📦 Dev Server (Webpack)

Metric Canary PR Change Trend
Cold (Listen) 814ms 813ms ✓ ▁▁▁▁█
Cold (Ready in log) 794ms 801ms ✓ ▁▁▂▁█
Cold (First Request) 3.397s 3.328s ✓ ▂▁▂▁█
Warm (Listen) 813ms 812ms ✓ ▁▁▁▁▇
Warm (Ready in log) 802ms 793ms ✓ ▁▁▂▁█
Warm (First Request) 3.369s 3.349s ✓ ▁▁▂▁█

⚡ Production Builds

Metric Canary PR Change Trend
Fresh Build 5.960s 6.007s ✓ ▁▁▁▂█
Cached Build 3.158s 3.203s ✓ ▁▁▁▁█
📦 Production Builds (Webpack) (Legacy)

📦 Production Builds (Webpack)

Metric Canary PR Change Trend
Fresh Build 23.768s 23.631s ✓ ▁▁▂▁█
Cached Build 23.913s 24.093s ✓ ▁▁▂▁█
node_modules Size 557 MB 557 MB ✓ █████
📦 Bundle Sizes

Bundle Sizes

⚡ Turbopack

Client

Main Bundles
Canary PR Change
017_4moyv5m34.js gzip 117 B 117 B ✓
05jocc85-b73_.js gzip 341 B N/A -
0cz1d0mv5g_q7.js gzip 39.4 kB 39.4 kB ✓
0e5lgz-xcf6ln.js gzip 273 B 273 B ✓
0enhddgqs2dl-.js gzip 265 B 265 B ✓
0h34mejwa_7pd.js gzip 354 B 354 B ✓
0w853zxni3ro0.js gzip 1.36 kB N/A -
13lq2aw3_0bgw.js gzip 3.52 kB N/A -
19ll0d77yn0fq.js gzip 71.5 kB N/A -
1drifx6-oqf3q.js gzip 5.7 kB N/A -
1elt1qium-r2m.css gzip 115 B 115 B ✓
1m5vdnlf0deiv.js gzip 411 B 411 B ✓
1nigpy_mr0xvj.js gzip 339 B N/A -
1p0l8mn2wz5f2.js gzip 1.97 kB N/A -
1ph_nbsm967ot.js gzip 152 B N/A -
1sqoyy39jpgkq.js gzip 121 B 121 B ✓
1tf1phijqlx9j.js gzip 220 B 220 B ✓
2-54v5vj7orwp.js gzip 46.8 kB N/A -
2b05_424u1zq-.js gzip 120 B N/A -
2ca40xittx95r.js gzip 8.75 kB N/A -
2kci0vnt-49o-.js gzip 113 B 113 B ✓
2rtriq9lc6zj-.js gzip 116 B N/A -
2sxgl_rcrrn_p.js gzip 399 B N/A -
2vt62i17bhbdu.js gzip 2.35 kB N/A -
33ykhou7tl-4h.js gzip 7.53 kB N/A -
342di-crrg9kq.js gzip 119 B N/A -
36hfc2_979aw2.js gzip 119 B N/A -
38wfp6jjrrdfm.js gzip 131 B 131 B ✓
3dl1ilk37-30g.js gzip 117 B N/A -
3h0us7yy7bd_o.js gzip 13.1 kB N/A -
3kvxagoobts7h.js gzip 117 B N/A -
3p99hd9vwe6ae.js gzip 13 kB N/A -
3r0475panhk6m.js gzip 123 B N/A -
3s0i-weykgzrt.js gzip 125 B N/A -
3vjosdbddykpw.js gzip 5.88 kB N/A -
3wijk2ywqgsv9.js gzip 2.52 kB N/A -
3xuguv9lsoqw7.js gzip 65.5 kB N/A -
41u5s3oe2-erp.js gzip 2.29 kB N/A -
turbopack-0d..9ejy.js gzip 3.74 kB 3.74 kB ✓
02zc7sqlpqxn3.js gzip N/A 119 B -
06pzxqp5qiozm.js gzip N/A 5.7 kB -
0dw-iyxdctoxo.js gzip N/A 65.5 kB -
0e28eh3gbwmp0.js gzip N/A 3.51 kB -
0e5ldm8_fmypq.js gzip N/A 71.5 kB -
0jwho9fkrb_t3.js gzip N/A 2.29 kB -
0longconn5mlm.js gzip N/A 123 B -
0p4l3ozbez2cz.js gzip N/A 2.35 kB -
0sbh_15ejop4o.js gzip N/A 13 kB -
0skdm-9rpid8g.js gzip N/A 117 B -
0wv-h56sz83wn.js gzip N/A 120 B -
137xup2t69ovc.js gzip N/A 1.97 kB -
1ezuqvubf86h1.js gzip N/A 5.88 kB -
1fk10s3m1oecx.js gzip N/A 400 B -
1hut5u-lz2yif.js gzip N/A 117 B -
20qhkyyj2_tla.js gzip N/A 116 B -
2imyufe18bsb7.js gzip N/A 1.36 kB -
2p4n3-8-xixh_.js gzip N/A 341 B -
2vqdfieehz5do.js gzip N/A 8.75 kB -
2xn85ykzw2v1e.js gzip N/A 13.1 kB -
34knadd6wu9qg.js gzip N/A 119 B -
35eub0y5wk0-c.js gzip N/A 7.53 kB -
37b0_t16yn97f.js gzip N/A 153 B -
39jq2b6t6z6up.js gzip N/A 339 B -
3a9a9tvkag_-j.js gzip N/A 2.52 kB -
3d61tgick8g6b.js gzip N/A 46.8 kB -
3t121tj-h0kbm.js gzip N/A 125 B -
Total 299 kB 299 kB ⚠️ +3 B

Server

Middleware
Canary PR Change
middleware-b..fest.js gzip 1.02 kB 1.03 kB ✓
Total 1.02 kB 1.03 kB ⚠️ +2 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 846 B 847 B ✓
Total 846 B 847 B ⚠️ +1 B
Build Cache
Canary PR Change
00000001.sst gzip 13.4 MB 13.3 MB 🟢 84.7 kB (-1%)
00000002.sst gzip 13.8 MB 14.7 MB 🔴 +932 kB (+7%)
00000003.sst gzip 12.7 MB 13.8 MB 🔴 +1.1 MB (+9%)
00000004.sst gzip 15 MB 13.1 MB 🟢 1.98 MB (-13%)
00000005.sst gzip 10 MB 10.1 MB 🔴 +10.7 kB (+0%)
00000006.sst gzip 2.75 MB 2.75 MB ✓
00000007.sst gzip 59 B 59 B ✓
00000008.meta gzip 89 B 297 kB 🔴 +297 kB (+333607%)
00000009.meta gzip 297 kB 89 B 🟢 297 kB (-100%)
00000010.meta gzip 297 kB 297 kB ✓
00000011.meta gzip 297 kB 297 kB ✓
00000012.sst gzip 52.2 kB 52.5 kB ✓
00000013.sst gzip 1.78 MB 1.8 MB 🔴 +10.1 kB (+1%)
00000014.sst gzip 59 B 59 B ✓
00000015.meta gzip 116 B 116 B ✓
00000016.meta gzip 327 kB 327 kB ✓
00000017.meta gzip 404 kB 404 kB ✓
00000018.sst gzip 53.2 kB 53.3 kB ✓
00000019.sst gzip 1.35 MB 1.36 MB 🔴 +10.3 kB (+1%)
00000020.sst gzip 59 B 59 B ✓
00000021.meta gzip 116 B 116 B ✓
00000022.meta gzip 327 kB 327 kB ✓
00000023.meta gzip 372 kB 372 kB ✓
00000024.sst gzip 53.1 kB 53.3 kB ✓
00000025.sst gzip 1.35 MB 1.36 MB 🔴 +10.4 kB (+1%)
00000026.sst gzip 59 B 59 B ✓
00000027.meta gzip 116 B 116 B ✓
00000028.meta gzip 327 kB 327 kB ✓
00000029.meta gzip 372 kB 372 kB ✓
00000030.sst gzip 52.4 kB 52.5 kB ✓
00000031.sst gzip 1.35 MB 1.36 MB 🔴 +10.4 kB (+1%)
00000032.sst gzip 59 B 59 B ✓
00000033.meta gzip 116 B 116 B ✓
00000034.meta gzip 327 kB 327 kB ✓
00000035.meta gzip 372 kB 372 kB ✓
00000036.sst gzip 53.1 kB 1.36 MB 🔴 +1.31 MB (+2471%)
00000037.sst gzip 1.35 MB 53.2 kB 🟢 1.3 MB (-96%)
00000038.sst gzip 59 B 59 B ✓
00000039.meta gzip 116 B 116 B ✓
00000040.meta gzip 327 kB 327 kB ✓
00000041.meta gzip 372 kB 372 kB ✓
CURRENT gzip 94 B 94 B ✓
LOG gzip 682 B 687 B ✓
Total 79.6 MB 79.6 MB ⚠️ +35.9 kB

📦 Webpack

Client

Main Bundles
Canary PR Change
3322-HASH.js gzip 65.7 kB N/A -
4191.HASH.js gzip 169 B N/A -
9784-HASH.js gzip 5.6 kB N/A -
b1ad9f4c-HASH.js gzip 63.5 kB N/A -
framework-HASH.js gzip 59.7 kB 59.7 kB ✓
main-app-HASH.js gzip 253 B 250 B 🟢 3 B (-1%)
main-HASH.js gzip 39.8 kB 39.7 kB ✓
webpack-HASH.js gzip 1.68 kB 1.68 kB ✓
3577.HASH.js gzip N/A 168 B -
578-HASH.js gzip N/A 65.8 kB -
8590-HASH.js gzip N/A 5.57 kB -
a8984546-HASH.js gzip N/A 63.5 kB -
Total 236 kB 236 kB ⚠️ +103 B
Polyfills
Canary PR Change
polyfills-HASH.js gzip 39.4 kB 39.4 kB ✓
Total 39.4 kB 39.4 kB ✓
Pages
Canary PR Change
_app-HASH.js gzip 194 B 193 B ✓
_error-HASH.js gzip 181 B 182 B ✓
css-HASH.js gzip 334 B 331 B ✓
dynamic-HASH.js gzip 1.81 kB 1.81 kB ✓
edge-ssr-HASH.js gzip 255 B 253 B ✓
head-HASH.js gzip 349 B 351 B ✓
hooks-HASH.js gzip 382 B 384 B ✓
image-HASH.js gzip 5.05 kB 5.05 kB ✓
index-HASH.js gzip 260 B 259 B ✓
link-HASH.js gzip 2.48 kB 2.48 kB ✓
routerDirect..HASH.js gzip 317 B 318 B ✓
script-HASH.js gzip 384 B 386 B ✓
withRouter-HASH.js gzip 316 B 315 B ✓
1afbb74e6ecf..834.css gzip 106 B 106 B ✓
Total 12.4 kB 12.4 kB ✅ -6 B

Server

Edge SSR
Canary PR Change
edge-ssr.js gzip 128 kB 128 kB ✓
page.js gzip 293 kB 293 kB ✓
Total 421 kB 421 kB ✅ -439 B
Middleware
Canary PR Change
middleware-b..fest.js gzip 599 B 603 B ✓
middleware-r..fest.js gzip 156 B 156 B ✓
middleware.js gzip 45.6 kB 45.6 kB ✓
edge-runtime..pack.js gzip 842 B 842 B ✓
Total 47.2 kB 47.2 kB ⚠️ +3 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 699 B 699 B ✓
Total 699 B 699 B ✓
Build Cache
Canary PR Change
0.pack gzip 4.8 MB 4.79 MB ✓
index.pack gzip 123 kB 123 kB ✓
index.pack.old gzip 124 kB 122 kB 🟢 1.67 kB (-1%)
Total 5.05 MB 5.04 MB ✅ -6.22 kB

🔄 Shared (bundler-independent)

Runtimes
Canary PR Change
app-page-exp...dev.js gzip 375 kB 375 kB ✓
app-page-exp..prod.js gzip 206 kB 206 kB ✓
app-page-tur...dev.js gzip 374 kB 374 kB ✓
app-page-tur..prod.js gzip 206 kB 206 kB ✓
app-page-tur...dev.js gzip 371 kB 371 kB ✓
app-page-tur..prod.js gzip 204 kB 204 kB ✓
app-page.run...dev.js gzip 371 kB 371 kB ✓
app-page.run..prod.js gzip 204 kB 204 kB ✓
app-route-ex...dev.js gzip 82.9 kB 82.9 kB ✓
app-route-ex..prod.js gzip 55.8 kB 55.8 kB ✓
app-route-tu...dev.js gzip 82.9 kB 82.9 kB ✓
app-route-tu..prod.js gzip 55.9 kB 55.9 kB ✓
app-route-tu...dev.js gzip 82.5 kB 82.5 kB ✓
app-route-tu..prod.js gzip 55.6 kB 55.6 kB ✓
app-route.ru...dev.js gzip 82.4 kB 82.4 kB ✓
app-route.ru..prod.js gzip 55.6 kB 55.6 kB ✓
dev-validati...dev.js gzip 134 kB 134 kB ✓
dev-validati...dev.js gzip 134 kB 134 kB ✓
dev-validati...dev.js gzip 132 kB 132 kB ✓
dev-validati...dev.js gzip 132 kB 132 kB ✓
dist_client_...dev.js gzip 324 B 324 B ✓
dist_client_...dev.js gzip 326 B 326 B ✓
dist_client_...dev.js gzip 318 B 318 B ✓
dist_client_...dev.js gzip 317 B 317 B ✓
pages-api-tu...dev.js gzip 46.3 kB 46.3 kB ✓
pages-api-tu..prod.js gzip 34.3 kB 34.3 kB ✓
pages-api.ru...dev.js gzip 46.2 kB 46.2 kB ✓
pages-api.ru..prod.js gzip 34.3 kB 34.3 kB ✓
pages-turbo....dev.js gzip 54.7 kB 54.7 kB ✓
pages-turbo...prod.js gzip 39.7 kB 39.7 kB ✓
pages.runtim...dev.js gzip 54.7 kB 54.7 kB ✓
pages.runtim..prod.js gzip 39.6 kB 39.6 kB ✓
server.runti..prod.js gzip 66 kB 66 kB ✓
use-cache-pr...dev.js gzip 73.4 kB 73.4 kB ✓
use-cache-pr...dev.js gzip 73.4 kB 73.4 kB ✓
use-cache-pr...dev.js gzip 71.7 kB 71.7 kB ✓
use-cache-pr...dev.js gzip 71.6 kB 71.6 kB ✓
Total 4.1 MB 4.1 MB ✅ -3 B
📎 Tarball URL
https://vercel-packages.vercel.app/next/commits/38306954da2ce5af224b9160f7a7e47ace16daca/next

Commit: 3830695

@github-actions

github-actions Bot commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Tests Passed

Commit: b732ade

sokra added a commit that referenced this pull request Sep 7, 2026
…eload URLs first

Two review comments from #97956:

- `ChunkingType::Worker` was mapped onto `ChunkGroupKey::Isolated`, which
  conflated worker groups with isolated ones. Give workers their own
  `ChunkGroupKey::Worker` / `ChunkGroup::Worker`, handled in `entries()`,
  `entries_count()`, both debug-string impls and the key -> group
  conversion. `WorkerLoaderModule::chunk_group` now builds
  `ChunkGroup::Worker` for web/shared workers and Node worker threads, so
  worker groups describe themselves in chunk-group debugging instead of
  showing up as "Isolated".

- Move the preloaded chunk URLs to the front of the worker bootstrap
  params, matching the order they are loaded in:

      [PRELOAD_CHUNK_URLS, TURBOPACK_NEXT_CHUNK_URLS, ASSET_SUFFIX,
       WORKER_CHUNK_BASE_PATH, ...forwarded globals]

  The Rust-generated bootstrap reads shift accordingly (`params[0]` /
  `params[1]`, `param(2)` / `param(3)`, forwarded globals from `param(4)`).

Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
@sokra
sokra requested a review from mischnic September 8, 2026 10:07
@mischnic

mischnic commented Sep 8, 2026

Copy link
Copy Markdown
Member

There are a lot of failing tests.

@vercel-fleet-prod

Copy link
Copy Markdown
Contributor

Thanks — those were real, and the cause is now understood.

The failures you saw were from eb0b6baa. That commit tried to break the Node worker-thread recursion by skipping the worker chunk group when the target module was already available, which cannot work: for new Worker(__filename) the worker target is the module containing the call, and a chunk group's availability always includes its own entry — so it looked "already available" on the very first, non-recursive call. The entry chunk was never emitted, hence the 500s / module-not-found.

20bd7bd2 fixes it differently. Node worker loaders keep building rooted, self-contained entry chunks, and make_chunk_group now passes AvailabilityInfo::root() for NodeWorkerThread before calling worker_loader_chunk_item, so the task's arguments (and therefore its memoization key) are independent of nesting depth. The rediscovery that happens while chunking the worker's own entry group then lands on the same task instead of creating a new one per level — i.e. canary's termination-by-memoization. Doing it at the call site is the load-bearing part: arguments are hashed before a #[turbo_tasks::function] body runs, so normalizing inside WorkerLoaderModule::new was too late.

Web/shared workers are untouched and keep the enclosing availability, which is what unrolls the content-hash cycle this PR is actually about.

This one is verified against the reproducer rather than by inference: turbopack-tests::execution's basic/worker-threads fixture has a should handle self-referencing worker (__filename) case matching the CI failure, and it only needs jest-circus to run locally. It fails with the fix stashed and passes with it applied; the full execution suite is green (279 passed).

@sokra
sokra force-pushed the fix/worker-availability-cycle branch from 20bd7bd to 9587bef Compare September 9, 2026 02:13
sokra added a commit that referenced this pull request Sep 9, 2026
…eload URLs first

Two review comments from #97956:

- `ChunkingType::Worker` was mapped onto `ChunkGroupKey::Isolated`, which
  conflated worker groups with isolated ones. Give workers their own
  `ChunkGroupKey::Worker` / `ChunkGroup::Worker`, handled in `entries()`,
  `entries_count()`, both debug-string impls and the key -> group
  conversion. `WorkerLoaderModule::chunk_group` now builds
  `ChunkGroup::Worker` for web/shared workers and Node worker threads, so
  worker groups describe themselves in chunk-group debugging instead of
  showing up as "Isolated".

- Move the preloaded chunk URLs to the front of the worker bootstrap
  params, matching the order they are loaded in:

      [PRELOAD_CHUNK_URLS, TURBOPACK_NEXT_CHUNK_URLS, ASSET_SUFFIX,
       WORKER_CHUNK_BASE_PATH, ...forwarded globals]

  The Rust-generated bootstrap reads shift accordingly (`params[0]` /
  `params[1]`, `param(2)` / `param(3)`, forwarded globals from `param(4)`).

Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
@sokra
sokra force-pushed the fix/worker-availability-cycle branch from 9587bef to f5d2f34 Compare September 9, 2026 15:53
sokra added a commit that referenced this pull request Sep 9, 2026
…eload URLs first

Two review comments from #97956:

- `ChunkingType::Worker` was mapped onto `ChunkGroupKey::Isolated`, which
  conflated worker groups with isolated ones. Give workers their own
  `ChunkGroupKey::Worker` / `ChunkGroup::Worker`, handled in `entries()`,
  `entries_count()`, both debug-string impls and the key -> group
  conversion. `WorkerLoaderModule::chunk_group` now builds
  `ChunkGroup::Worker` for web/shared workers and Node worker threads, so
  worker groups describe themselves in chunk-group debugging instead of
  showing up as "Isolated".

- Move the preloaded chunk URLs to the front of the worker bootstrap
  params, matching the order they are loaded in:

      [PRELOAD_CHUNK_URLS, TURBOPACK_NEXT_CHUNK_URLS, ASSET_SUFFIX,
       WORKER_CHUNK_BASE_PATH, ...forwarded globals]

  The Rust-generated bootstrap reads shift accordingly (`params[0]` /
  `params[1]`, `param(2)` / `param(3)`, forwarded globals from `param(4)`).

Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
sokra added a commit that referenced this pull request Sep 11, 2026
…eload URLs first

Two review comments from #97956:

- `ChunkingType::Worker` was mapped onto `ChunkGroupKey::Isolated`, which
  conflated worker groups with isolated ones. Give workers their own
  `ChunkGroupKey::Worker` / `ChunkGroup::Worker`, handled in `entries()`,
  `entries_count()`, both debug-string impls and the key -> group
  conversion. `WorkerLoaderModule::chunk_group` now builds
  `ChunkGroup::Worker` for web/shared workers and Node worker threads, so
  worker groups describe themselves in chunk-group debugging instead of
  showing up as "Isolated".

- Move the preloaded chunk URLs to the front of the worker bootstrap
  params, matching the order they are loaded in:

      [PRELOAD_CHUNK_URLS, TURBOPACK_NEXT_CHUNK_URLS, ASSET_SUFFIX,
       WORKER_CHUNK_BASE_PATH, ...forwarded globals]

  The Rust-generated bootstrap reads shift accordingly (`params[0]` /
  `params[1]`, `param(2)` / `param(3)`, forwarded globals from `param(4)`).

Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
@sokra
sokra force-pushed the fix/worker-availability-cycle branch 2 times, most recently from f66aff2 to 819fe75 Compare September 14, 2026 11:01
sokra added a commit that referenced this pull request Sep 14, 2026
…eload URLs first

Two review comments from #97956:

- `ChunkingType::Worker` was mapped onto `ChunkGroupKey::Isolated`, which
  conflated worker groups with isolated ones. Give workers their own
  `ChunkGroupKey::Worker` / `ChunkGroup::Worker`, handled in `entries()`,
  `entries_count()`, both debug-string impls and the key -> group
  conversion. `WorkerLoaderModule::chunk_group` now builds
  `ChunkGroup::Worker` for web/shared workers and Node worker threads, so
  worker groups describe themselves in chunk-group debugging instead of
  showing up as "Isolated".

- Move the preloaded chunk URLs to the front of the worker bootstrap
  params, matching the order they are loaded in:

      [PRELOAD_CHUNK_URLS, TURBOPACK_NEXT_CHUNK_URLS, ASSET_SUFFIX,
       WORKER_CHUNK_BASE_PATH, ...forwarded globals]

  The Rust-generated bootstrap reads shift accordingly (`params[0]` /
  `params[1]`, `param(2)` / `param(3)`, forwarded globals from `param(4)`).

Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
@sokra
sokra force-pushed the fix/worker-availability-cycle branch from 819fe75 to 269164c Compare September 16, 2026 12:25
sokra added a commit that referenced this pull request Sep 16, 2026
…eload URLs first

Two review comments from #97956:

- `ChunkingType::Worker` was mapped onto `ChunkGroupKey::Isolated`, which
  conflated worker groups with isolated ones. Give workers their own
  `ChunkGroupKey::Worker` / `ChunkGroup::Worker`, handled in `entries()`,
  `entries_count()`, both debug-string impls and the key -> group
  conversion. `WorkerLoaderModule::chunk_group` now builds
  `ChunkGroup::Worker` for web/shared workers and Node worker threads, so
  worker groups describe themselves in chunk-group debugging instead of
  showing up as "Isolated".

- Move the preloaded chunk URLs to the front of the worker bootstrap
  params, matching the order they are loaded in:

      [PRELOAD_CHUNK_URLS, TURBOPACK_NEXT_CHUNK_URLS, ASSET_SUFFIX,
       WORKER_CHUNK_BASE_PATH, ...forwarded globals]

  The Rust-generated bootstrap reads shift accordingly (`params[0]` /
  `params[1]`, `param(2)` / `param(3)`, forwarded globals from `param(4)`).

Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
sokra added a commit that referenced this pull request Sep 16, 2026
…eload URLs first

Two review comments from #97956:

- `ChunkingType::Worker` was mapped onto `ChunkGroupKey::Isolated`, which
  conflated worker groups with isolated ones. Give workers their own
  `ChunkGroupKey::Worker` / `ChunkGroup::Worker`, handled in `entries()`,
  `entries_count()`, both debug-string impls and the key -> group
  conversion. `WorkerLoaderModule::chunk_group` now builds
  `ChunkGroup::Worker` for web/shared workers and Node worker threads, so
  worker groups describe themselves in chunk-group debugging instead of
  showing up as "Isolated".

- Move the preloaded chunk URLs to the front of the worker bootstrap
  params, matching the order they are loaded in:

      [PRELOAD_CHUNK_URLS, TURBOPACK_NEXT_CHUNK_URLS, ASSET_SUFFIX,
       WORKER_CHUNK_BASE_PATH, ...forwarded globals]

  The Rust-generated bootstrap reads shift accordingly (`params[0]` /
  `params[1]`, `param(2)` / `param(3)`, forwarded globals from `param(4)`).

Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
@sokra
sokra force-pushed the fix/worker-availability-cycle branch from 269164c to c636a0d Compare September 16, 2026 18:26
sokra added a commit that referenced this pull request Sep 16, 2026
…eload URLs first

Two review comments from #97956:

- `ChunkingType::Worker` was mapped onto `ChunkGroupKey::Isolated`, which
  conflated worker groups with isolated ones. Give workers their own
  `ChunkGroupKey::Worker` / `ChunkGroup::Worker`, handled in `entries()`,
  `entries_count()`, both debug-string impls and the key -> group
  conversion. `WorkerLoaderModule::chunk_group` now builds
  `ChunkGroup::Worker` for web/shared workers and Node worker threads, so
  worker groups describe themselves in chunk-group debugging instead of
  showing up as "Isolated".

- Move the preloaded chunk URLs to the front of the worker bootstrap
  params, matching the order they are loaded in:

      [PRELOAD_CHUNK_URLS, TURBOPACK_NEXT_CHUNK_URLS, ASSET_SUFFIX,
       WORKER_CHUNK_BASE_PATH, ...forwarded globals]

  The Rust-generated bootstrap reads shift accordingly (`params[0]` /
  `params[1]`, `param(2)` / `param(3)`, forwarded globals from `param(4)`).

Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
@sokra
sokra force-pushed the fix/worker-availability-cycle branch 2 times, most recently from 83b170b to c70c815 Compare September 22, 2026 11:28
sokra added a commit that referenced this pull request Sep 22, 2026
…eload URLs first

Two review comments from #97956:

- `ChunkingType::Worker` was mapped onto `ChunkGroupKey::Isolated`, which
  conflated worker groups with isolated ones. Give workers their own
  `ChunkGroupKey::Worker` / `ChunkGroup::Worker`, handled in `entries()`,
  `entries_count()`, both debug-string impls and the key -> group
  conversion. `WorkerLoaderModule::chunk_group` now builds
  `ChunkGroup::Worker` for web/shared workers and Node worker threads, so
  worker groups describe themselves in chunk-group debugging instead of
  showing up as "Isolated".

- Move the preloaded chunk URLs to the front of the worker bootstrap
  params, matching the order they are loaded in:

      [PRELOAD_CHUNK_URLS, TURBOPACK_NEXT_CHUNK_URLS, ASSET_SUFFIX,
       WORKER_CHUNK_BASE_PATH, ...forwarded globals]

  The Rust-generated bootstrap reads shift accordingly (`params[0]` /
  `params[1]`, `param(2)` / `param(3)`, forwarded globals from `param(4)`).

Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
sokra added a commit that referenced this pull request Sep 23, 2026
…eload URLs first

Two review comments from #97956:

- `ChunkingType::Worker` was mapped onto `ChunkGroupKey::Isolated`, which
  conflated worker groups with isolated ones. Give workers their own
  `ChunkGroupKey::Worker` / `ChunkGroup::Worker`, handled in `entries()`,
  `entries_count()`, both debug-string impls and the key -> group
  conversion. `WorkerLoaderModule::chunk_group` now builds
  `ChunkGroup::Worker` for web/shared workers and Node worker threads, so
  worker groups describe themselves in chunk-group debugging instead of
  showing up as "Isolated".

- Move the preloaded chunk URLs to the front of the worker bootstrap
  params, matching the order they are loaded in:

      [PRELOAD_CHUNK_URLS, TURBOPACK_NEXT_CHUNK_URLS, ASSET_SUFFIX,
       WORKER_CHUNK_BASE_PATH, ...forwarded globals]

  The Rust-generated bootstrap reads shift accordingly (`params[0]` /
  `params[1]`, `param(2)` / `param(3)`, forwarded globals from `param(4)`).

Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
@sokra
sokra force-pushed the fix/worker-availability-cycle branch 2 times, most recently from b09667c to 54f8b52 Compare September 24, 2026 09:33
@sokra
sokra force-pushed the fix/worker-availability-cycle branch 2 times, most recently from 7b0b674 to 656b569 Compare October 21, 2026 13:03
sokra added a commit that referenced this pull request Oct 21, 2026
…eload URLs first

Two review comments from #97956:

- `ChunkingType::Worker` was mapped onto `ChunkGroupKey::Isolated`, which
  conflated worker groups with isolated ones. Give workers their own
  `ChunkGroupKey::Worker` / `ChunkGroup::Worker`, handled in `entries()`,
  `entries_count()`, both debug-string impls and the key -> group
  conversion. `WorkerLoaderModule::chunk_group` now builds
  `ChunkGroup::Worker` for web/shared workers and Node worker threads, so
  worker groups describe themselves in chunk-group debugging instead of
  showing up as "Isolated".

- Move the preloaded chunk URLs to the front of the worker bootstrap
  params, matching the order they are loaded in:

      [PRELOAD_CHUNK_URLS, TURBOPACK_NEXT_CHUNK_URLS, ASSET_SUFFIX,
       WORKER_CHUNK_BASE_PATH, ...forwarded globals]

  The Rust-generated bootstrap reads shift accordingly (`params[0]` /
  `params[1]`, `param(2)` / `param(3)`, forwarded globals from `param(4)`).

Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
vercel-fleet-prod Bot and others added 26 commits October 24, 2026 21:03
…::Worker

Part 1 of the worker-availability-cycle fix (see requirements.md):

- Move  enum from turbopack-ecmascript to turbopack-core
  so it can be referenced by .
- Add  variant.
- Handle  in all exhaustive  matches across
  core: Display, without_inherit_async, introspect, chunk-group
  inheritance, and module-graph edge tracing.

No behavior change yet — Worker is treated identically to Isolated.
The variant is needed for the late WorkerLoaderModule creation that
will thread availability info to break the self-referencing worker
deadlock (issue #97399).

Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
…on't deadlock

Production builds parked forever at "Creating an optimized production
build ..." at 0% CPU whenever a web worker spawned itself.

`WorkerLoaderModule::chunk_group` hard-coded `AvailabilityInfo::root()`,
so no module was ever "already available" inside a worker chunk group.
A worker that spawns itself therefore kept creating a fresh nested chunk
group, and because the loader's generated code embeds the chunk group's
(content-hashed) chunk paths, that closed an await ring turbo-tasks
cannot detect:

    chunk path -> content hash -> chunk content -> worker loader code
    -> chunks_data -> chunk_group -> chunk path -> ...

Fix it the way `import()` cycles are already unrolled: build the worker
chunk group with the *enclosing* chunk group's availability info, so a
self-reference sees its own entry as available and stops recursing.

To do that the loader has to be created once the availability info is
known, i.e. during chunking rather than while building the module graph:

- Move `WorkerType` to turbopack-core and add `ChunkingType::Worker`.
- `WorkerAssetReference` now resolves to a new `WorkerEntryModule`
  marker (carrying the inner module, worker type and asset context) and
  reports `ChunkingType::Worker`. Tracing-only references keep resolving
  to the raw module and keep their old parallel chunking type.
- `chunk_group_content_operation` collects worker edges into
  `worker_modules`. This runs before the `ModuleOrBatch::None` and
  chunkability checks because the marker is deliberately not chunkable,
  and reads the module off the edge for the same reason.
- `make_chunk_group` creates the `WorkerLoaderModule` from each marker
  via the new `ChunkingContext::worker_loader_chunk_item`, passing
  `new_availability_info`.
- The loader gained an `availability_info` field (excluded from `ident()`,
  like `AsyncLoaderModule`) and no longer carries the isolated edge to
  the worker entry — that lives on the marker now.

Unlike an async loader, the loader must *not* collapse to an empty asset
list when the target is available: `parentImport(id)` works because it
runs in the same runtime, whereas a worker gets a fresh realm and still
needs its evaluate chunk to instantiate the entry. The factories for
already-available modules instead reach the worker at runtime:

- Each browser runtime records the JS chunks whose module factories it
  installed, exposed as `__turbopack_get_loaded_chunk_paths__`.
- `createWorker` sends those paths in a new params slot, separate from
  the worker's own chunks. Module factories are functions and cannot be
  structured-cloned across realms, so the worker re-imports the chunks —
  cheap, since the browser has them cached.
- The worker bootstrap loads those chunks *first*, before the evaluate
  chunk runs the entry module, and still loads the shared runtime last.
  `TURBOPACK_NEXT_CHUNK_URLS` stays exactly the reverse of the
  registration order it is popped in.

Content hashing and chunk identity are untouched.

Adds production regression tests for a worker that spawns itself, both
inline and through a separate module file.

Fixes #97399

Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
…s ident

The previous commit made `WorkerAssetReference` resolve to a non-chunkable
`WorkerEntryModule` marker that is excluded from the enclosing chunk group,
but left the `new Worker(...)` code generation resolving through
`PatternMapping` with `ResolveType::ChunkItem`. That path requires the
resolved module to be a chunkable module with a module id in the current
chunk group, so it rejected the marker:

    non-ecmascript placeable asset
    ... (ecmascript, worker loader) is not placeable in ESM chunks

which broke every worker: the `require()` for the loader was never emitted,
so no worker ever started.

Dynamic imports don't hit this because their codegen never asks
`resolve_reference()` for the id — it looks the loader up by ident via
`ChunkingContext::async_loader_chunk_item_ident`. Add the worker equivalent:

- Add `ResolveType::WorkerLoader` and handle it in
  `to_single_pattern_mapping` *before* the chunkable downcast, deriving a
  `ModuleLoader` id from the marker's ident.
- Use it from `WorkerAssetReferenceCodeGen`, so the generated code still
  emits `__turbopack_require__(loader_id)`.
- Add `WorkerEntryModule::asset_ident_for(inner, worker_type)` and route
  *both* the marker's and the late `WorkerLoaderModule`'s `ident()` through
  it.

That last part is load-bearing: `ModuleIdStrategy` keys ids by the resolved
`Vc<AssetIdent>`, not by ident content, so an equal-but-separately-computed
ident would miss the lookup and fail with "ModuleId not found for ident".
The marker is the module that appears in the module graph (and is therefore
registered in the global module id map), while the loader is what actually
becomes the chunk item, so the two have to share one memoized ident. This
mirrors `AsyncLoaderModule::asset_ident_for`.

Also document why the worker bootstrap must load the worker's own chunks
last: a worker loader has the same module id in every chunk group but a
different chunk list per group, so loading its own chunks last is what makes
a nested worker pick up the correctly-pruned list.

Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Workers still failed at runtime with:

    Module .../worker/browser/createWorker.ts ... was instantiated because
    it was required from ... (ecmascript, worker loader), but the module
    factory is not available.

`WorkerLoaderModule` is created during `make_chunk_group`, i.e. after the
module graph has been built, so its `Module::references()` are never
traversed. The `createWorker` runtime helper it referenced was therefore
never discovered by graph construction: it got no module id and was never
chunked, even though the loader's generated code `require()`s it.

Move that dependency onto the `WorkerEntryModule` marker, which *is* part of
the graph:

- Extract `create_worker_module(asset_context, worker_type)` as a shared
  memoized function. The marker references it and the loader delegates to
  it, so both resolve to the identical module and therefore the identical
  chunk item id.
- `WorkerEntryModule::references()` now declares both the isolated edge to
  the worker's own entry module and a parallel reference to the helper.
- `WorkerLoaderModule::references()` is now empty, with a comment
  explaining that a module created during chunking cannot contribute graph
  edges (declaring them there was silently dead).

Moving the reference alone was not sufficient: `ChunkingType::Worker`
returned `GraphTraversalAction::Exclude`, which skips the node *and its
children*, so the helper still would not have been chunked. Worker edges now
fall through to the same handling as `Parallel`, placing the marker's
referenced modules in the chunk group that also receives the loader.

That fall-through also fixes a related subtlety: the marker is reached over a
non-parallel edge, so it starts its own pre-batch, and because the marker
itself is not chunkable that batch reduces to the *helper* rather than the
marker. The marker therefore has to be read from `edge.module` (not the
node), and the node has to continue into normal chunk-item collection. The
marker's reference to the worker entry stays `ChunkingType::Isolated` and is
still excluded, so the worker's own modules do not leak into the parent
group.

Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Two CI failures, both mechanical:

- `runtime-base.ts` had a stray double blank line, failing `prettier-check`.
- Six snapshot tests embed runtime source this branch modifies, so their
  expected output was stale.

The four non-worker snapshots (debug-ids/browser, imports/ignore-comments,
runtime/default_dev_runtime, swc_transforms/preset_env) embed
`browser/runtime/base/runtime-base.ts`, and pick up only the new
`loadedJsChunkPaths` set, `registerLoadedJsChunk`, `getLoadedChunkPaths` and
the `browserContextPrototype.G` assignment. No module ids or unrelated
runtime code change.

The two worker snapshots additionally cover `createWorker.ts` and the new
graph/chunking layout, and are worth reading as verification of the previous
two commits: the regenerated output shows the page chunk registering
`createWorker.ts` under the same id the worker loader requires, the worker
loader in a chunk that the page's evaluate chunk lists in `otherChunks`, and
the loader passing the worker entrypoint plus the worker's own chunk list.
That is exactly the wiring the "module factory is not available" failure was
reporting as missing.

Regenerated with:

    UPDATE=1 cargo test -p turbopack-tests --test snapshot

Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
…eload URLs first

Two review comments from #97956:

- `ChunkingType::Worker` was mapped onto `ChunkGroupKey::Isolated`, which
  conflated worker groups with isolated ones. Give workers their own
  `ChunkGroupKey::Worker` / `ChunkGroup::Worker`, handled in `entries()`,
  `entries_count()`, both debug-string impls and the key -> group
  conversion. `WorkerLoaderModule::chunk_group` now builds
  `ChunkGroup::Worker` for web/shared workers and Node worker threads, so
  worker groups describe themselves in chunk-group debugging instead of
  showing up as "Isolated".

- Move the preloaded chunk URLs to the front of the worker bootstrap
  params, matching the order they are loaded in:

      [PRELOAD_CHUNK_URLS, TURBOPACK_NEXT_CHUNK_URLS, ASSET_SUFFIX,
       WORKER_CHUNK_BASE_PATH, ...forwarded globals]

  The Rust-generated bootstrap reads shift accordingly (`params[0]` /
  `params[1]`, `param(2)` / `param(3)`, forwarded globals from `param(4)`).

Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
The marker existed only to carry three things across the gap between module
graph construction and chunking. Each of them now uses a mechanism that
already exists, so the marker is gone and `WorkerAssetReference` resolves
straight to the worker's entry module.

1. The loader's module id. Added
   `ChunkingContext::worker_loader_chunk_item_ident`, mirroring
   `async_loader_chunk_item_ident`. It and `WorkerLoaderModule::ident()` both
   route through the same memoized `worker_loader_asset_ident_for`, so the id
   the `new Worker(...)` codegen looks up and the id the chunk item is
   registered under still agree. `ResolveType::WorkerLoader` now carries the
   worker type and goes through the normal chunkable-module branch, since the
   resolved target is a real chunkable module again.

2. Graph reachability for the `createWorker` runtime helper. The loader is
   created during chunking, so its own `references()` are never traversed and
   it cannot put the helper into the graph. `add_create_worker_reference` now
   declares it as a `SingleChunkableModuleReference` next to each
   `WorkerAssetReference` during analysis — the same pattern
   `WebAssemblyModuleAsset` uses for its own runtime helper. The helper stays
   its own on-demand module (it is only chunked into apps that use workers)
   rather than moving into the common runtime.

3. The `AssetContext` the helper has to be processed with. `make_chunk_group`
   recovers it from the worker's entry module via `ResolveOrigin`. Both
   `url_resolve` (web workers) and the Node worker-thread branch resolve the
   worker through `origin.asset_context().process_resolve_result(...)`, so
   that is the same context the helper reference used — which is what makes
   the memoized `create_worker_module` return the identical `Vc`, and
   therefore the identical chunk item id, on both sides.

Because worker edges no longer point at a non-chunkable node, the chunking
traversal reverts to a plain `Exclude` for them, dropping the
fall-through-to-`Parallel` workaround the marker required. `WorkerModuleReference`
became unused and is removed.

Generated output is unchanged: the worker snapshots needed no regeneration.

Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Production builds failed with:

    ModuleId not found for ident:
      [project]/app/browser/worker.js [app-client] (ecmascript, worker loader)

`get_global_module_id_strategy` builds the deterministic id map from the
modules in the module graph, plus a special case that registers the
*synthetic* idents of loaders that only come into existence during chunking
— previously just `AsyncLoaderModule`.

The `WorkerEntryModule` marker used to be a real graph module, so the worker
loader's id was registered as a side effect of the marker being traversed.
Removing the marker made that ident synthetic, exactly like the async
loader's, but without the matching registration — so the `new Worker(...)`
codegen looked up an id that was never put in the map.

Handle `ChunkingType::Worker` alongside `ChunkingType::Async` there, using
the same memoized `worker_loader_asset_ident_for` that both the codegen
lookup and `WorkerLoaderModule::ident()` go through. `async_idents` is
renamed to `loader_idents` since it now covers both.

Note on why this escaped local testing: the worker snapshot fixtures run
with `runtimeType: Development`, which uses `ModuleIdStrategy { module_id_map:
None, fallback: Ident }` and never consults the id map. The deterministic
path is only reached from next-api's production project setup and
`turbopack-cli build`, neither of which the turbopack-tests harness
exercises — so development snapshots were byte-identical while production
builds were broken.

Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
`node-worker-threads` hung: 5 of 7 tests timed out at exactly 120s, starting
with the self-spawning `new Worker(__filename)` case. The later timeouts are
a cascade — it is a dev-mode test, so the blocked compilation stalls every
later route request.

Canary creates `WorkerLoaderModule` while building the module graph, without
availability info in its identity, so a worker that references itself
memoizes back to the same loader task and `chunk_group` runs once. This
branch creates loaders during chunking *with* availability info in their
identity, so each nesting level is a distinct task. Web workers terminate
because the nested availability prunes the nested chunk group empty, but the
Node path still went through `root_entry_chunk_group`, which hardcodes
`AvailabilityInfo::root()` — so availability never accumulated and each level
rediscovered the self-reference.

Inheriting availability into the Node entry chunk group would fix the
recursion but break the workers that currently pass: `entry_chunk_group`
forwards availability to `make_chunk_group`, which prunes already-available
modules, and a Node worker thread loads only its own entry chunk (plus what
that requires relative to `__dirname`). It has no equivalent of the browser
`createWorker` preload list, so a pruned entry chunk is simply missing module
factories.

So keep Node entry groups rooted and self-contained, and break the recursion
explicitly instead:

- Add `inner_is_available`, the same batch-aware availability probe
  `AsyncLoaderModule::chunk_group` uses.
- When a nested Node worker loader finds its own entry already available,
  return no output assets instead of building a second entry chunk group.
- Add memoized `node_worker_entry_path`, deriving the entry filename from the
  inner module's ident, and generate the loader's `__dirname + "/" + …` path
  from it rather than from `assets.last()`. The nested loader then names the
  file the outer level already emitted, without needing a chunk group of its
  own.

Termination: the route group excludes the worker edge, so loader(w, A) has
`w ∉ A` and builds the real entry group; that group's availability B contains
`w`, so the self-reference's loader(w, B) short-circuits. Bottoms out at
depth 2.

Browser worker output is unchanged — re-running the snapshot suite with
UPDATE=1 modifies no files.

Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
The previous attempt made the self-referencing `new Worker(__filename)`
route return 500 instead of hanging: it skipped building the worker's entry
chunk group whenever the worker target was already available, so the entry
file was never emitted and the worker could not be required.

That check cannot work. For `new Worker(__filename)` the worker target *is*
the module containing the call, and a chunk group's availability always
includes its own entry — so the target looks "already available" on the very
first, non-recursive call, indistinguishable from the recursive one.

Fix the recursion where it actually comes from instead. Node worker loaders
keep building rooted, self-contained entry chunks (they run in a fresh thread
with no browser-style preload list, so their entry chunk must not be pruned),
and `make_chunk_group` now passes `AvailabilityInfo::root()` for
`NodeWorkerThread` *before* calling `worker_loader_chunk_item`. That keeps the
task's arguments — and therefore its memoization key — independent of nesting
depth, so the rediscovery that happens while chunking the worker's own entry
group lands on the same task instead of creating a new one per level. This is
canary's termination-by-memoization, restored.

Doing this at the call site matters: arguments are hashed to find the cached
cell before a `#[turbo_tasks::function]` body runs, so normalizing inside
`WorkerLoaderModule::new` (as the previous commit did) is too late to change
the key. `new` keeps the normalization as defense-in-depth, with a comment
saying so.

Web and shared web workers are unchanged: they still get the enclosing chunk
group's availability, which is what unrolls their content-hash cycle.

Verified against the reproducer this time. `turbopack-tests::execution`'s
`basic/worker-threads` fixture has a `should handle self-referencing worker
(__filename)` case — the same failure CI reported. It needs only
`jest-circus`, so it can be run locally: the test fails with the fix stashed
and passes with it applied, and the full execution suite is green (279).

Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Pure content drift from rebasing across ~105 canary commits (e.g. an
unrelated DOM CSS-reload fix changed embedded runtime source, which
shifts embedded source-map content in these fixtures). No logic change.

Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Pure content drift from rebasing across 20 canary commits. No logic
change.

Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Pure content drift from rebasing across 16 canary commits. No logic
change.

Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Pure content drift from rebasing across 64 canary commits: content-addressed
output names and embedded source maps changed. No logic change.

Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Pure content drift from rebasing across 62 canary commits (shared HMR runtime refactor changes embedded runtime source). No logic change.

Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Content drift from rebasing onto canary: the DOM runtime now uses the findChunk* lookup helpers (#100047). No logic change.

Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
@sokra
sokra force-pushed the fix/worker-availability-cycle branch from 656b569 to b732ade Compare October 24, 2026 21:04

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

created-by: Turbopack team PRs by the Turbopack team. tests Turbopack Related to Turbopack with Next.js.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Turbopack: next build deadlocks at 0% CPU — content-hashed chunk paths + mutually-referencing async chunks form an await cycle

2 participants