@rsc-kit/core 0.20.2 → 0.20.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/host.d.ts CHANGED
@@ -18,6 +18,8 @@ import type { RouteManifest } from "./manifest.js";
18
18
  export interface RscEngine {
19
19
  /** The route table this bundle was built from. */
20
20
  manifest?(): RouteManifest;
21
+ /** A short id of this build's client, for the version the host answers with when given none. */
22
+ buildId?(): Promise<string>;
21
23
  installHostFn(fn: (name: string, ...args: unknown[]) => unknown): void;
22
24
  handleRscStream(component: string, props?: Record<string, unknown>, layouts?: {
23
25
  component: string;
@@ -143,10 +145,11 @@ export interface RscHostOptions {
143
145
  /** Serve a built browser asset. Return null for anything not found. */
144
146
  assets?: (pathname: string, request: Request) => Promise<Response | null> | Response | null;
145
147
  /**
146
- * Identifies this build to the client, which compares it on every
147
- * navigation and falls back to a full load when it changes. Without one a
148
- * client keeps talking to a deployment that no longer exists — worst behind
149
- * a CDN, where the shell it holds may already be from an older build.
148
+ * Identifies this build to the client, which says it back on every
149
+ * navigation and is sent to load the document when it differs. For an
150
+ * engine with no `buildId` of its own; the generated one has, and every
151
+ * document it renders says that id, so a version named here would
152
+ * disagree with what the client was told and refuse every navigation.
150
153
  */
151
154
  version?: string;
152
155
  /**
package/dist/host.js CHANGED
@@ -286,7 +286,13 @@ function matchPage(routes, url) {
286
286
  return matchRoute(routes, url.pathname, hostOf.get(url) ?? null);
287
287
  }
288
288
  export function createRscHandler(options) {
289
- const { engine, assets, version } = options;
289
+ const { engine, assets } = options;
290
+ // The build's own id, which is also what every document says it is - the
291
+ // two must agree, or a client's honest claim is a 409 on every request.
292
+ // A version named by the app applies only to an engine with no id of its
293
+ // own. Resolved on the first request: the engine reads it from a build
294
+ // product it only has at runtime.
295
+ let version = engine.buildId ? undefined : options.version;
290
296
  const maxActionBody = options.maxActionBody ?? DEFAULT_MAX_ACTION_BODY;
291
297
  const compress = options.compress ?? true;
292
298
  // Annotated rather than inferred: the narrowing below is lost inside the
@@ -421,6 +427,8 @@ export function createRscHandler(options) {
421
427
  return [version ?? "", url.pathname + url.search, ...varies].join("\n");
422
428
  }
423
429
  return async function handle(request) {
430
+ if (version === undefined && engine.buildId)
431
+ version = await engine.buildId();
424
432
  return await withRequest(request, () => withCache(() =>
425
433
  // Open for the whole request and sealed the moment an answer exists,
426
434
  // so middleware — which runs before any rendering — can put headers on
@@ -583,6 +591,20 @@ export function createRscHandler(options) {
583
591
  const formPost = await formPostOf(request, url);
584
592
  if (!formPost && request.method !== "GET" && request.method !== "HEAD")
585
593
  return null;
594
+ // A client saying which build it runs, and it is not this one: its
595
+ // manifest cannot load what this build's payload names - a client
596
+ // component added since is "client reference not found" and the route's
597
+ // error boundary, on a page that worked a click ago. Under a service
598
+ // worker that serves the last build's document first, that is every
599
+ // returning visitor's first navigation after a deploy, not an open tab.
600
+ // A 409 sends the client to load the document instead, from this build.
601
+ const claimed = request.headers.get(HEADER.version);
602
+ if (claimed !== null && claimed !== "" && version && claimed !== version && request.headers.get(HEADER.rsc) !== null) {
603
+ return new Response(null, {
604
+ status: 409,
605
+ headers: withVersion({ "X-RSC-Location": url.pathname + url.search, "Cache-Control": "no-store" }),
606
+ });
607
+ }
586
608
  // One named region of this page, asked for without mutating anything to
587
609
  // earn it. What an action invalidated does not come through here — that
588
610
  // travels back inside the action's own answer, which is the whole point of