Skip to content

[pull] canary from vercel:canary#1123

Merged
pull[bot] merged 9 commits into
code:canaryfrom
vercel:canary
Jun 12, 2026
Merged

[pull] canary from vercel:canary#1123
pull[bot] merged 9 commits into
code:canaryfrom
vercel:canary

Conversation

@pull

@pull pull Bot commented Jun 12, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

lukesandberg and others added 9 commits June 11, 2026 13:18
### What?
Adopt
`[scattered_collect`](https://docs.rs/scattered-collect/latest/scattered_collect/)
for collecting static rcstr values.

### Why?

Built by our good friend @mmastrac to solve some of our problems.
Replaces inventory with a simpler linker driver approach.

Basically inventory works using constructor functions to build a linked
list, then iterating follows the list. `scattered_collect` on the
otherhand uses a linker section for the data, so it all gets moved to a
dedicated part of the binary and bingo, a slice!. This means we don't
need `init` time logic.

This is preparation for migrating other uses of `inventory` just to
prove it out. Which was a good idea because we found and fixed a bug!
See
https://github.com/mmastrac/linktime/releases/tag/link-section-0.18.2

## Wasm

scatter_collect supports wasm, but requires us to register a 'section
reader' function with the runtime, see
https://docs.rs/link-section/latest/link_section/#wasm. The instructions
are staightforward but a little tricky since we rely on `wasm-bindgen`
to constrct the `Wasm.Module` instance and it is encapsulated. So to
'inject' that function we would need to patch some of the bootstrapping.
Given that this registry is only an optimization for deserialization
which never happens in a wasm build i just flagged it all off.
The client sends the dev-only `x-nextjs-request-id` and
`x-nextjs-html-request-id` headers so the server can route debug
information back to the originating request. They are internal plumbing,
like the flight headers, but unlike the flight headers they were never
stripped from the headers exposed to userland `headers()`. This change
strips them in `getHeaders` alongside the flight headers. The server
still reads them from the raw request headers, not from the sealed
userland copy, so the debug channel is unaffected.
Private caches were never stored in a cache handler, so every reload in
`next dev` re-ran them from scratch and they registered as a cache miss
on each load. This change persists `'use cache: private'` entries in
development in a dedicated built-in in-memory handler so that warm
reloads are fast. The handler is gated on
`process.env.__NEXT_DEV_SERVER` and is kept out of the kind-keyed
handlers map so it can never be replaced by a user-configured `default`
handler: private cache entries can hold data specific to the incoming
request (for example, derived from its cookies or headers) and must
never reach a remote or otherwise persistent handler. Production keeps
private caches non-persisted as before.

The coarse cache-handler key for a dev private cache is scoped by the
request's cookies and headers so entries for requests with different
request data don't collide. It excludes Next-internal cookies that
aren't application data (the HMR refresh hash, already part of the cache
key, and the instant-navigation cookie, which toggles while a navigation
lock is held) and the transport and content-negotiation headers that
vary between otherwise-equivalent requests (a browser reload adds
`cache-control`, and `accept` and `sec-fetch-*` differ between an HTML
navigation and an RSC request). Keying by only the cookies and headers a
cache actually reads is left as a follow-up; read root params are
already tracked that way, the same as for public caches. The cache life
is forced to `revalidate: 0` with a 5-minute `expire`, so each read
serves the stale entry immediately and warms a fresh one in the
background through the existing stale-while-revalidate path.

Cross-request deduplication now applies to private caches in development
too, so concurrent requests with identical request data share a single
fill; it remains skipped in production where request-specific data must
not be shared across requests. To make this deterministic,
`saveToCacheHandler` resolves the metadata a cross-request joiner awaits
only after the entry has been written to the handler, so the joiner
finds it when re-reading its recomputed key. This closes a pre-existing
race in that path, present for public caches too but never surfaced by
their cross-request test, where the joiner's metadata could resolve
before the handler write had landed.

The defensive invariant that rejected reading a private entry from a
handler is removed: it only existed to narrow `cacheContext.kind` for a
code path that no longer needs it, and dev now legitimately reads
persisted private entries while production never registers a private
handler to read from.
…sier (#94711)

Extracted from #94595. Cleans up a bunch of stuff in the
`instant-validation`module that deals with assembling payloads for
instant validation, no behavioral changes.
# What
This uses `scatter_collect` from @mmastrac to register all values,
traits and value impls of traits. now all data can be consistently
accessed when constructing the VALUEs registry, eliminating a number of
racy access opportunities.


# Why
Scatter collect uses link sections to gather registries at link time,
this resolve some inherent brittleness in constructor functions since
the relative execution order is **not guaranteed** and it is also not
even possible to _detect_ execution that is too early (early access to
an `inventory` collection will miss some entries silently)

We have a single report that appears to be due to a race like this

```
web:dev: thread 'tokio-rt-worker' (41574444) panicked at turbopack/crates/turbo-tasks/src/macro_helpers.rs:274:13:
web:dev: no trait impl registered for value type turbopack_core::module_graph::ModuleGraphImportTracer
web:dev: note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```

I do not entirely understand how this is possible since constructors
should complete prior to the first napi entry point (other than
`napi::module_init` which is itself a constructor).

Our initialization logic is fundamentally brittle since it relies on

* an `inventory` of all value types
* an `inventory `of all trait types
* a set of ctor functions to register trait implementations (basically
(value, trait) pairs)

All of these need to be initialized/invoked before the `VALUES` registry
is accessed, but the above panic implies that the ctor function for
`ModuleGraphImportTracer` to register its implementation of
`ImportTracer` hasn't occurred yet.

By moving the registies to link time we completely eliminate all startup
races since the linker does all the 'gathering', now the only possible
concern is that we attempt to downcast a trait prior the the `VALUES`
registry completing which does not appear to be possible.


# Binary size impact

Measured on `next-swc.darwin-arm64.node` built with `pnpm --filter
@next/swc build-native-release` (release profile), this branch vs
`canary`. Stripped = `strip -x`; gzip = `gzip -9` of the stripped
binary.

| Metric | branch | canary | Δ | Δ% |
|---|---:|---:|---:|---:|
| **raw** | 122,879,888 B | 124,184,976 B | **−1,305,088 B** |
**−1.051%** |
| **stripped** | 83,505,264 B | 84,127,232 B | **−621,968 B** |
**−0.739%** |
| **gzip+stripped** | 28,800,547 B | 28,925,679 B | **−125,132 B** |
**−0.433%** |

The addon shrinks on every metric. Replacing the generated per-impl
registration functions (and their `ctor`/`inventory` machinery) with
link-time scattered collection removes real code/data — ~622 KB even
after stripping — and laying the registries out contiguously at link
time compresses better, so the gzip win holds up too.

<!-- NEXT_JS_LLM_PR -->
The 'request' codepath in params/searchParams was forked
`process.env.NODE_ENV === 'development'`, but that's a leftover from
when dev was the only place where we did staged rendering. It didn't
account for conditions like `--debug-prerender`, and now makes it
generally confusing to reason about. This PR simplifies the flow so that
renders that care about stages are clearly distinguished, and dev is
just a minor variation within that (generally adding some extra warnings
via proxies)
When we send a `next-router-prefetch: 3` request, we now track if
unblocking session data produced new content. Exposed as `u?:
Promise<boolean>` on the navigation response.
Distinguish an absent postponed state from an empty one so cold RDC
resume requests perform a full dynamic RSC render instead of being
rejected or treated as revalidation.
@pull pull Bot locked and limited conversation to collaborators Jun 12, 2026
@pull pull Bot added the ⤵️ pull label Jun 12, 2026
@pull pull Bot merged commit 828e1ca into code:canary Jun 12, 2026
1 of 3 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants