@rangojs/router 0.32.0 → 0.33.1
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/README.md +3 -2
- package/dist/bin/browser-7KIJ5HSV.js +15 -0
- package/dist/bin/chunk-3B7CKBRO.js +463 -0
- package/dist/bin/chunk-524VACCG.js +71 -0
- package/dist/bin/chunk-LD7UBHT4.js +196 -0
- package/dist/bin/chunk-TO6CRWLN.js +1429 -0
- package/dist/bin/chunk-TTXC2U3G.js +2477 -0
- package/dist/bin/client-urls-projection-DQLWPDAT.js +28 -0
- package/dist/bin/forward-user-plugins-URBZKJP4.js +62 -0
- package/dist/bin/rango.js +42 -10173
- package/dist/bin/runtime-EPUDEHO4.js +596 -0
- package/dist/bin/runtime-discovery-3HH6BP3K.js +352 -0
- package/dist/bin/state-ACD2MGKJ.js +16 -0
- package/dist/bin/temp-rsc-server-JALTIWLT.js +3190 -0
- package/dist/types/bin/rango.d.ts +1 -0
- package/dist/types/browser/action-fence.d.ts +20 -0
- package/dist/types/browser/cookie-name.d.ts +51 -10
- package/dist/types/browser/invalidate-client-cache.d.ts +8 -6
- package/dist/types/browser/navigation-bridge.d.ts +36 -1
- package/dist/types/browser/navigation-client.d.ts +19 -0
- package/dist/types/browser/navigation-store-handle.d.ts +37 -4
- package/dist/types/browser/navigation-store.d.ts +2 -2
- package/dist/types/browser/navigation-transaction.d.ts +0 -1
- package/dist/types/browser/partial-update.d.ts +13 -0
- package/dist/types/browser/prefetch/cache.d.ts +96 -8
- package/dist/types/browser/prefetch/fetch.d.ts +26 -2
- package/dist/types/browser/prefetch/queue.d.ts +3 -2
- package/dist/types/browser/rango-state.d.ts +34 -5
- package/dist/types/browser/react/Link.d.ts +15 -6
- package/dist/types/browser/types.d.ts +25 -0
- package/dist/types/build/generate-route-types.d.ts +1 -1
- package/dist/types/build/route-types/include-resolution.d.ts +9 -1
- package/dist/types/build/route-types/router-processing.d.ts +3 -1
- package/dist/types/cache/cache-key-utils.d.ts +12 -8
- package/dist/types/cache/cache-scope.d.ts +4 -2
- package/dist/types/cache/cf/cf-cache-types.d.ts +9 -5
- package/dist/types/cache/document-cache-policy.d.ts +9 -1
- package/dist/types/cache/memory-segment-store.d.ts +3 -2
- package/dist/types/cache/segment-codec.d.ts +2 -2
- package/dist/types/cache/shell-snapshot.d.ts +1 -1
- package/dist/types/cache/types.d.ts +12 -7
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +2 -1
- package/dist/types/client-urls/types.d.ts +7 -5
- package/dist/types/prefetch-markers.d.ts +9 -0
- package/dist/types/prefetch-source-scope.d.ts +66 -0
- package/dist/types/route-definition/helpers-types.d.ts +8 -5
- package/dist/types/router/intercept-resolution.d.ts +25 -11
- package/dist/types/router/match-context.d.ts +1 -0
- package/dist/types/router/match-handlers.d.ts +1 -1
- package/dist/types/router/prefetch-cache-ttl.d.ts +11 -3
- package/dist/types/router/router-context.d.ts +1 -1
- package/dist/types/router/router-options.d.ts +6 -3
- package/dist/types/router/segment-resolution/loader-cache.d.ts +8 -0
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +23 -7
- package/dist/types/router/segment-resolution/revalidation.d.ts +19 -0
- package/dist/types/router/source-reads.d.ts +53 -0
- package/dist/types/router/types.d.ts +1 -1
- package/dist/types/rsc/helpers.d.ts +5 -2
- package/dist/types/rsc/prefetch-render-error.d.ts +7 -2
- package/dist/types/rsc/request-stages.d.ts +2 -0
- package/dist/types/rsc/shell-serve.d.ts +6 -1
- package/dist/types/server/context.d.ts +23 -6
- package/dist/types/server/cookie-store.d.ts +15 -1
- package/dist/types/server/document-location-state.d.ts +5 -2
- package/dist/types/server/handle-store.d.ts +2 -0
- package/dist/types/server/request-context.d.ts +43 -7
- package/dist/types/testing/e2e/server.d.ts +16 -1
- package/dist/types/testing/render-route.d.ts +66 -2
- package/dist/types/testing/serve-shell-request.d.ts +6 -3
- package/dist/types/types/handler-context.d.ts +7 -5
- package/dist/types/types/loader-types.d.ts +7 -4
- package/dist/types/types/segments.d.ts +15 -4
- package/dist/types/urls/path-helper-types.d.ts +7 -5
- package/dist/types/vite/temp-rsc-server.d.ts +2 -2
- package/dist/vite/index.js +449 -189
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +81 -0
- package/skills/app-structure/e-commerce.md +10 -9
- package/skills/app-structure/large-app.md +18 -2
- package/skills/caching/SKILL.md +10 -5
- package/skills/client-urls/SKILL.md +29 -11
- package/skills/comparison/references/framework-comparison.md +13 -8
- package/skills/document-cache/SKILL.md +8 -0
- package/skills/hooks/navigation.md +8 -8
- package/skills/hooks/outlets.md +4 -0
- package/skills/hooks/state.md +24 -8
- package/skills/intercept/SKILL.md +33 -12
- package/skills/layout/SKILL.md +60 -0
- package/skills/links/SKILL.md +101 -4
- package/skills/loader/SKILL.md +49 -22
- package/skills/migrate-nextjs/SKILL.md +8 -2
- package/skills/migrate-react-router/data-and-actions.md +5 -0
- package/skills/ppr/SKILL.md +26 -5
- package/skills/prerender/SKILL.md +4 -1
- package/skills/route/SKILL.md +6 -6
- package/skills/router-setup/SKILL.md +2 -1
- package/skills/server-actions/SKILL.md +41 -8
- package/skills/testing/client-components.md +29 -13
- package/skills/testing/serve-shell-request.md +7 -7
- package/skills/testing/server-actions.md +8 -0
- package/skills/ux-architecture/SKILL.md +70 -19
- package/src/bin/rango.ts +39 -13
- package/src/browser/action-fence.ts +72 -0
- package/src/browser/cookie-name.ts +82 -14
- package/src/browser/invalidate-client-cache.ts +9 -7
- package/src/browser/navigation-bridge.ts +164 -32
- package/src/browser/navigation-client.ts +95 -20
- package/src/browser/navigation-store-handle.ts +67 -5
- package/src/browser/navigation-store.ts +86 -17
- package/src/browser/navigation-transaction.ts +0 -6
- package/src/browser/partial-update.ts +31 -12
- package/src/browser/prefetch/cache.ts +188 -11
- package/src/browser/prefetch/fetch.ts +211 -51
- package/src/browser/prefetch/queue.ts +3 -2
- package/src/browser/rango-state.ts +50 -8
- package/src/browser/react/Link.tsx +20 -9
- package/src/browser/react/use-navigation.ts +1 -1
- package/src/browser/rsc-router.tsx +8 -1
- package/src/browser/server-action-bridge.ts +16 -11
- package/src/browser/types.ts +25 -0
- package/src/build/generate-route-types.ts +1 -0
- package/src/build/route-types/include-resolution.ts +40 -3
- package/src/build/route-types/router-processing.ts +170 -25
- package/src/build/route-types/scan-filter.ts +1 -0
- package/src/cache/cache-key-utils.ts +12 -8
- package/src/cache/cache-runtime.ts +5 -1
- package/src/cache/cache-scope.ts +28 -7
- package/src/cache/cf/cf-cache-types.ts +9 -5
- package/src/cache/document-cache-policy.ts +27 -0
- package/src/cache/document-cache.ts +5 -0
- package/src/cache/memory-segment-store.ts +3 -2
- package/src/cache/segment-codec.ts +2 -2
- package/src/cache/shell-snapshot.ts +1 -1
- package/src/cache/types.ts +12 -7
- package/src/cache/vercel/vercel-cache-store.ts +2 -1
- package/src/client-urls/types.ts +7 -5
- package/src/prefetch-markers.ts +10 -0
- package/src/prefetch-source-scope.ts +137 -0
- package/src/route-definition/helpers-types.ts +8 -5
- package/src/router/intercept-resolution.ts +94 -50
- package/src/router/loader-resolution.ts +8 -6
- package/src/router/match-api.ts +33 -29
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +2 -1
- package/src/router/match-middleware/cache-lookup.ts +21 -13
- package/src/router/match-result.ts +39 -87
- package/src/router/navigation-snapshot.ts +18 -3
- package/src/router/prefetch-cache-ttl.ts +15 -3
- package/src/router/revalidation.ts +53 -23
- package/src/router/router-context.ts +2 -1
- package/src/router/router-options.ts +6 -3
- package/src/router/segment-resolution/loader-cache.ts +21 -3
- package/src/router/segment-resolution/prefetch-deferral.ts +38 -31
- package/src/router/segment-resolution/revalidation.ts +40 -12
- package/src/router/source-reads.ts +122 -0
- package/src/router/types.ts +2 -1
- package/src/router.ts +11 -12
- package/src/rsc/helpers.ts +8 -3
- package/src/rsc/prefetch-render-error.ts +7 -2
- package/src/rsc/progressive-enhancement.ts +3 -2
- package/src/rsc/request-stages.ts +59 -0
- package/src/rsc/rsc-rendering.ts +49 -33
- package/src/rsc/server-action.ts +2 -1
- package/src/rsc/shell-capture.ts +4 -5
- package/src/rsc/shell-serve.ts +12 -1
- package/src/segment-system.tsx +1 -1
- package/src/server/context.ts +37 -6
- package/src/server/cookie-store.ts +22 -1
- package/src/server/document-location-state.ts +23 -2
- package/src/server/handle-store.ts +2 -0
- package/src/server/request-context.ts +113 -28
- package/src/testing/dispatch.ts +3 -0
- package/src/testing/e2e/fixture.ts +4 -12
- package/src/testing/e2e/server.ts +91 -4
- package/src/testing/render-route.tsx +269 -4
- package/src/testing/serve-shell-request.ts +6 -3
- package/src/types/handler-context.ts +7 -5
- package/src/types/loader-types.ts +7 -4
- package/src/types/segments.ts +15 -4
- package/src/urls/path-helper-types.ts +7 -5
- package/src/vite/temp-rsc-server.ts +2 -2
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -102,6 +102,7 @@ import { contextGet } from "../context-var.js";
|
|
|
102
102
|
import { rewriteShellAudience, rewriteTrail } from "../server/rewrite-trail.js";
|
|
103
103
|
import { visitorShellKey } from "./shell-capture-constants.js";
|
|
104
104
|
import { PREFETCH_HEADER } from "../prefetch-markers.js";
|
|
105
|
+
import { isPrefetchKind } from "../router/segment-resolution/prefetch-deferral.js";
|
|
105
106
|
import { WIRE } from "../wire-params.js";
|
|
106
107
|
import {
|
|
107
108
|
isWarmReplace,
|
|
@@ -140,6 +141,7 @@ import {
|
|
|
140
141
|
SEGMENT_FRAGMENT_CAPABILITY_HEADER,
|
|
141
142
|
SEGMENT_FRAGMENT_RECOVERY_HEADER,
|
|
142
143
|
} from "../segment-fragments.js";
|
|
144
|
+
import { prefetchScopeOf } from "../router/source-reads.js";
|
|
143
145
|
|
|
144
146
|
type PprReplayStatus =
|
|
145
147
|
| { outcome: "HIT"; freshness: "fresh" | "stale" }
|
|
@@ -361,10 +363,10 @@ type PreparedRender =
|
|
|
361
363
|
kind: "payload";
|
|
362
364
|
payload: RscPayload;
|
|
363
365
|
/**
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
+
* `x-rsc-prefetch-scope`, when the response depends on the source
|
|
367
|
+
* page (prefetchScopeOf).
|
|
366
368
|
*/
|
|
367
|
-
|
|
369
|
+
prefetchScope: string | undefined;
|
|
368
370
|
pprReplayStatus?: PprReplayStatus;
|
|
369
371
|
/** The key a navigation-only heal capture stores under, when one is needed. */
|
|
370
372
|
healKey?: string;
|
|
@@ -857,7 +859,7 @@ function* preparePayloadPlan<TEnv>(
|
|
|
857
859
|
return {
|
|
858
860
|
kind: "payload",
|
|
859
861
|
payload: buildFullPayload(match, ctx, url, reqCtx, handleStore),
|
|
860
|
-
|
|
862
|
+
prefetchScope: undefined,
|
|
861
863
|
pprReplayStatus,
|
|
862
864
|
healKey,
|
|
863
865
|
};
|
|
@@ -869,6 +871,7 @@ function* preparePayloadPlan<TEnv>(
|
|
|
869
871
|
// showed at once, and ran held work again.
|
|
870
872
|
const deferring = result.segments.some((s) => s.deferred);
|
|
871
873
|
if (deferring) reqCtx._payloadDeferred = true;
|
|
874
|
+
if (result.fromStorage) reqCtx._servedFromStorage = true;
|
|
872
875
|
return {
|
|
873
876
|
kind: "payload",
|
|
874
877
|
payload: {
|
|
@@ -898,10 +901,11 @@ function* preparePayloadPlan<TEnv>(
|
|
|
898
901
|
stateCookieName: ctx.router.resolvedStateCookieName,
|
|
899
902
|
},
|
|
900
903
|
},
|
|
901
|
-
|
|
902
|
-
result.interceptTargeted === true ||
|
|
903
|
-
result.sourceHeld
|
|
904
|
-
|
|
904
|
+
prefetchScope: prefetchScopeOf(
|
|
905
|
+
result.interceptTargeted === true || deferring,
|
|
906
|
+
result.sourceHeld,
|
|
907
|
+
result.diff.length === 0,
|
|
908
|
+
),
|
|
905
909
|
pprReplayStatus,
|
|
906
910
|
healKey,
|
|
907
911
|
};
|
|
@@ -925,7 +929,7 @@ function* preparePayloadPlan<TEnv>(
|
|
|
925
929
|
return {
|
|
926
930
|
kind: "payload",
|
|
927
931
|
payload: buildFullPayload(match, ctx, url, reqCtx, handleStore),
|
|
928
|
-
|
|
932
|
+
prefetchScope: undefined,
|
|
929
933
|
};
|
|
930
934
|
}
|
|
931
935
|
|
|
@@ -1111,7 +1115,7 @@ function renderPreparedRscResponse<TEnv>(
|
|
|
1111
1115
|
): Promise<Response> {
|
|
1112
1116
|
const { ctx, request, env, url, isPartial, nonce, reqCtx, renderSpan } =
|
|
1113
1117
|
input;
|
|
1114
|
-
const { payload, pprReplayStatus,
|
|
1118
|
+
const { payload, pprReplayStatus, prefetchScope } = prepared;
|
|
1115
1119
|
const metricsStore = reqCtx._metricsStore;
|
|
1116
1120
|
|
|
1117
1121
|
const rscHeaders: Record<string, string> = {
|
|
@@ -1128,24 +1132,24 @@ function renderPreparedRscResponse<TEnv>(
|
|
|
1128
1132
|
serializePprReplayStatus(pprReplayStatus);
|
|
1129
1133
|
}
|
|
1130
1134
|
// Tell the client's prefetch cache to scope this response to its source
|
|
1131
|
-
//
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
if (sourceScoped) {
|
|
1139
|
-
rscHeaders["x-rsc-prefetch-scope"] = "source";
|
|
1135
|
+
// (instead of the default source-agnostic wildcard). See prefetchScopeOf.
|
|
1136
|
+
if (prefetchScope !== undefined) {
|
|
1137
|
+
rscHeaders["x-rsc-prefetch-scope"] = prefetchScope;
|
|
1138
|
+
// A shared cache keys a partial by URL and the ids the client holds, not
|
|
1139
|
+
// by its source page (document-cache.ts): stored, a scoped answer would
|
|
1140
|
+
// serve its diff to another page that holds the same ids.
|
|
1141
|
+
reqCtx._payloadSourceScoped = true;
|
|
1140
1142
|
}
|
|
1141
|
-
//
|
|
1142
|
-
//
|
|
1143
|
-
//
|
|
1144
|
-
//
|
|
1145
|
-
//
|
|
1146
|
-
//
|
|
1147
|
-
//
|
|
1143
|
+
// Not reused by a shared cache or the document cache (which also refuses a
|
|
1144
|
+
// payload marked `_payloadDeferred` or `_payloadSourceScoped`), whatever
|
|
1145
|
+
// Cache-Control the route set: a partial scoped to its source page (above);
|
|
1146
|
+
// a response that carries deferred units, which answers one page's prefetch,
|
|
1147
|
+
// and a fill, the second half of one adoption (prefetch: false,
|
|
1148
|
+
// docs/design/prefetch-false.md, R7); and a stale partial render, which a
|
|
1149
|
+
// shared cache keyed by URL must not store or serve. Any other route keeps
|
|
1150
|
+
// its headers.
|
|
1148
1151
|
if (
|
|
1152
|
+
reqCtx._payloadSourceScoped ||
|
|
1149
1153
|
reqCtx._payloadDeferred ||
|
|
1150
1154
|
(isPartial &&
|
|
1151
1155
|
(reqCtx._requestKind === "fill" ||
|
|
@@ -1195,7 +1199,7 @@ function renderPreparedRscResponse<TEnv>(
|
|
|
1195
1199
|
},
|
|
1196
1200
|
);
|
|
1197
1201
|
// A prefetch whose render reported an error is not kept (#1170).
|
|
1198
|
-
if (reqCtx._requestKind
|
|
1202
|
+
if (!isPrefetchKind(reqCtx._requestKind) || !isPartial) return rendered;
|
|
1199
1203
|
return rendered.then((response) =>
|
|
1200
1204
|
markPrefetchOnRenderError(response, reqCtx._renderErrors ?? []),
|
|
1201
1205
|
);
|
|
@@ -1735,8 +1739,14 @@ function serveShellHit(
|
|
|
1735
1739
|
// request's own context and before the response leaves (the tail runs on a
|
|
1736
1740
|
// derived context, after an await), so the response goes
|
|
1737
1741
|
// `private, no-store` and stays out of the document cache
|
|
1738
|
-
// (server/document-location-state.ts).
|
|
1739
|
-
|
|
1742
|
+
// (server/document-location-state.ts). The tail carries exactly this
|
|
1743
|
+
// state: a write during the tail (a loader's, through getRequestContext())
|
|
1744
|
+
// comes after these headers chose the response's caching, so it is not
|
|
1745
|
+
// sent, as the skill documents (skills/hooks/state.md).
|
|
1746
|
+
// Never undefined: attachLocationStateIfPresent would read the tail's
|
|
1747
|
+
// context instead.
|
|
1748
|
+
const tailLocationState = reqCtx._locationState ?? [];
|
|
1749
|
+
if (tailLocationState.length > 0) {
|
|
1740
1750
|
reqCtx._documentLocationState = true;
|
|
1741
1751
|
}
|
|
1742
1752
|
const metricsStore = reqCtx._metricsStore;
|
|
@@ -1766,7 +1776,7 @@ function serveShellHit(
|
|
|
1766
1776
|
reportCacheError(error, "cache-write", "[ShellServe] retire");
|
|
1767
1777
|
}
|
|
1768
1778
|
};
|
|
1769
|
-
/** The first loader pin of the entry that failed to decode
|
|
1779
|
+
/** The first loader pin of the entry that failed to decode. */
|
|
1770
1780
|
let undecodablePin: string | undefined;
|
|
1771
1781
|
|
|
1772
1782
|
const renderTail = async (
|
|
@@ -1797,7 +1807,7 @@ function serveShellHit(
|
|
|
1797
1807
|
payload.metadata.pathname = entry.pathname ?? url.pathname;
|
|
1798
1808
|
// The visitor's location state, as a MISS document's payload carries
|
|
1799
1809
|
// it. The request was marked per visitor above.
|
|
1800
|
-
attachLocationStateIfPresent(payload);
|
|
1810
|
+
attachLocationStateIfPresent(payload, false, tailLocationState);
|
|
1801
1811
|
}
|
|
1802
1812
|
// Full Flight render per request: hydration needs the whole payload (there
|
|
1803
1813
|
// is no Flight-side resume — a React limitation, not ours).
|
|
@@ -2061,7 +2071,13 @@ function serveShellHit(
|
|
|
2061
2071
|
"the doc record could not supply the handler layer",
|
|
2062
2072
|
);
|
|
2063
2073
|
}
|
|
2064
|
-
|
|
2074
|
+
// A document redirect's state cookie: this response's headers
|
|
2075
|
+
// cleared it, and the reload must read it again.
|
|
2076
|
+
controller.enqueue(
|
|
2077
|
+
new TextEncoder().encode(
|
|
2078
|
+
shellReloadScript(reqCtx._carriedLocationStateCookie),
|
|
2079
|
+
),
|
|
2080
|
+
);
|
|
2065
2081
|
if (tailTiming) {
|
|
2066
2082
|
tailTiming.outcome = "error";
|
|
2067
2083
|
tailTiming.completeMs = Math.round(performance.now() - tailT0);
|
|
@@ -2087,7 +2103,7 @@ function serveShellHit(
|
|
|
2087
2103
|
}
|
|
2088
2104
|
// A pin that failed to decode left its loader to this request's run:
|
|
2089
2105
|
// this response hydrated once over a prelude rendered from the
|
|
2090
|
-
// capture's value
|
|
2106
|
+
// capture's value. Retire the entry once the response is
|
|
2091
2107
|
// closed, and back the key off so a pin that keeps failing costs one
|
|
2092
2108
|
// mismatch per window. No reload: that would cost a second page load.
|
|
2093
2109
|
if (undecodablePin !== undefined) {
|
package/src/rsc/server-action.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
setRequestContextParams,
|
|
23
23
|
} from "../server/request-context.js";
|
|
24
24
|
import { printServerError } from "../server/print-server-error.js";
|
|
25
|
+
import { runActionBody } from "../server/cookie-store.js";
|
|
25
26
|
import { observePhase, PHASES } from "../router/instrument.js";
|
|
26
27
|
import { resolveActionRefId } from "../router/is-action.js";
|
|
27
28
|
import { getInterceptSourceHeader } from "../router/navigation-snapshot.js";
|
|
@@ -203,7 +204,7 @@ export async function executeServerAction<TEnv>(
|
|
|
203
204
|
|
|
204
205
|
try {
|
|
205
206
|
loadedAction = await ctx.loadServerAction(actionId);
|
|
206
|
-
let data = await loadedAction!.apply(null, args);
|
|
207
|
+
let data = await runActionBody(() => loadedAction!.apply(null, args));
|
|
207
208
|
|
|
208
209
|
// Intercept redirect Responses: serializing one as the action returnValue
|
|
209
210
|
// would fail, and revalidation would run needlessly.
|
package/src/rsc/shell-capture.ts
CHANGED
|
@@ -552,7 +552,7 @@ const SNAPSHOT_BYTE_ENCODER = new TextEncoder();
|
|
|
552
552
|
|
|
553
553
|
/**
|
|
554
554
|
* Warn once per key that the capture's loader pins exceeded the route's
|
|
555
|
-
* `maxSnapshotBytes` cap, so no document shell was stored
|
|
555
|
+
* `maxSnapshotBytes` cap, so no document shell was stored. Once per
|
|
556
556
|
* key: the route recaptures every backoff window.
|
|
557
557
|
*/
|
|
558
558
|
function warnSnapshotOverCapOnce(
|
|
@@ -2603,8 +2603,7 @@ async function captureAndStoreShell(
|
|
|
2603
2603
|
throw new Error(`[rango] no shell pin for "${segmentKey}"`);
|
|
2604
2604
|
}
|
|
2605
2605
|
} catch (error) {
|
|
2606
|
-
// Unpinned, the loader would run on every HIT over this prelude
|
|
2607
|
-
// (dc-t89): refuse like an encode error.
|
|
2606
|
+
// Unpinned, the loader would run on every HIT over this prelude: refuse like an encode error.
|
|
2608
2607
|
reqCtx._renderErrors?.push(error);
|
|
2609
2608
|
}
|
|
2610
2609
|
}
|
|
@@ -2650,7 +2649,7 @@ async function captureAndStoreShell(
|
|
|
2650
2649
|
}
|
|
2651
2650
|
|
|
2652
2651
|
// Snapshot size guard: `maxSnapshotBytes` bounds the loader pins, and a
|
|
2653
|
-
// document HIT needs every pin its prelude baked
|
|
2652
|
+
// document HIT needs every pin its prelude baked. Over the cap
|
|
2654
2653
|
// the entry keeps the navigation half only (the doc record, or nothing:
|
|
2655
2654
|
// a tombstone that also shadows a build shell) and the attempt refuses.
|
|
2656
2655
|
// The whole entry is bounded separately below, by the store's limit.
|
|
@@ -2897,7 +2896,7 @@ export {
|
|
|
2897
2896
|
|
|
2898
2897
|
// Exported for unit tests that pin the refused-capture backoff policy directly
|
|
2899
2898
|
// (dev cap vs production exponential growth, stored-clears, cold-start re-probe),
|
|
2900
|
-
// and markCaptureBackoff for serveShellHit (a pin that failed to decode
|
|
2899
|
+
// and markCaptureBackoff for serveShellHit (a pin that failed to decode).
|
|
2901
2900
|
// These are the same module-level functions the schedule path uses; a test that
|
|
2902
2901
|
// drove them through a real capture round-trip could not assert the exact window
|
|
2903
2902
|
// arithmetic without a full cold render.
|
package/src/rsc/shell-serve.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
import { routerKeyPrefix } from "../cache/cache-key-utils.js";
|
|
39
39
|
import { reportCacheError } from "../cache/cache-error.js";
|
|
40
40
|
import { appendMetric } from "../router/metrics.js";
|
|
41
|
+
import { escapeJsonForScript } from "../escape-script.js";
|
|
41
42
|
import type { MetricsStore } from "../server/context.js";
|
|
42
43
|
import { WIRE } from "../wire-params.js";
|
|
43
44
|
|
|
@@ -162,12 +163,22 @@ export function withoutShellMissMarker(request: Request): Request | undefined {
|
|
|
162
163
|
* the page. window.stop() runs only when it reloads: it keeps the half-sent
|
|
163
164
|
* Flight stream from being closed by DOMContentLoaded, which would throw
|
|
164
165
|
* React's "Connection closed" (#412) before the reload lands.
|
|
166
|
+
*
|
|
167
|
+
* `restoreCookie` is the request's `_carriedLocationStateCookie`: the HIT's
|
|
168
|
+
* headers already cleared the location state cookie a document redirect left
|
|
169
|
+
* (server/document-location-state.ts), so the script sets it again before it
|
|
170
|
+
* reloads, and the reload carries the state.
|
|
165
171
|
*/
|
|
166
|
-
export function shellReloadScript(): string {
|
|
172
|
+
export function shellReloadScript(restoreCookie?: string): string {
|
|
173
|
+
const restore =
|
|
174
|
+
restoreCookie === undefined
|
|
175
|
+
? ""
|
|
176
|
+
: `document.cookie=${escapeJsonForScript(JSON.stringify(restoreCookie))};`;
|
|
167
177
|
return inlineShellScript(
|
|
168
178
|
"(function(){var u=new URL(location.href);" +
|
|
169
179
|
`if(u.searchParams.has(${JSON.stringify(WIRE.shellMiss)}))return;` +
|
|
170
180
|
`u.searchParams.set(${JSON.stringify(WIRE.shellMiss)},"miss");` +
|
|
181
|
+
restore +
|
|
171
182
|
"try{window.stop()}catch(e){}location.replace(u.href)})()",
|
|
172
183
|
);
|
|
173
184
|
}
|
package/src/segment-system.tsx
CHANGED
|
@@ -318,7 +318,7 @@ function wrapDefaultOutletContent(
|
|
|
318
318
|
* is settled like a flagged one, so the commit shows no fallback. A loader
|
|
319
319
|
* error is left to its read site there, its errorBoundary() fallback riding
|
|
320
320
|
* the throw, as on a navigation: the nearest boundary to the read catches it
|
|
321
|
-
* (#1129
|
|
321
|
+
* (#1129). notFound()/redirect() keep their build-time node.
|
|
322
322
|
*/
|
|
323
323
|
async function buildLoaderStreams(
|
|
324
324
|
loaders: ResolvedSegment[],
|
package/src/server/context.ts
CHANGED
|
@@ -126,9 +126,10 @@ export type EntryPropRender = {
|
|
|
126
126
|
loading?: ReactNode | false;
|
|
127
127
|
/**
|
|
128
128
|
* loading(fallback, { prefetch: false }) with a renderable fallback: a
|
|
129
|
-
* prefetch defers what the fallback covers while the entry's
|
|
130
|
-
* new to the client
|
|
131
|
-
* on both DSL
|
|
129
|
+
* speculative prefetch defers what the fallback covers while the entry's
|
|
130
|
+
* segment is new to the client
|
|
131
|
+
* (router/segment-resolution/prefetch-deferral.ts). Set on both DSL
|
|
132
|
+
* evaluations, whatever `ssr` did to `loading`.
|
|
132
133
|
*/
|
|
133
134
|
loadingPrefetch?: false;
|
|
134
135
|
/**
|
|
@@ -171,8 +172,8 @@ export type LoaderEntry = {
|
|
|
171
172
|
*/
|
|
172
173
|
bake?: true;
|
|
173
174
|
/**
|
|
174
|
-
* loader(Def, { prefetch: false }): a prefetch defers this
|
|
175
|
-
* while its segment is new to the client
|
|
175
|
+
* loader(Def, { prefetch: false }): a speculative prefetch defers this
|
|
176
|
+
* registration while its segment is new to the client
|
|
176
177
|
* (router/segment-resolution/prefetch-deferral.ts).
|
|
177
178
|
*/
|
|
178
179
|
prefetch?: false;
|
|
@@ -211,7 +212,10 @@ export type InterceptSegmentsState = {
|
|
|
211
212
|
* @internal This type is an implementation detail and may change without notice.
|
|
212
213
|
*/
|
|
213
214
|
export type InterceptSelectorContext<TEnv = any> = {
|
|
214
|
-
/**
|
|
215
|
+
/**
|
|
216
|
+
* Where the navigation comes from: the intercept source while one is open.
|
|
217
|
+
* Its `url` never carries a #fragment.
|
|
218
|
+
*/
|
|
215
219
|
from: ServerRouteLocation;
|
|
216
220
|
/** The navigation target. */
|
|
217
221
|
to: ServerRouteLocation;
|
|
@@ -1719,6 +1723,33 @@ export function isInsideLoaderBody(loaderId: string): boolean {
|
|
|
1719
1723
|
return false;
|
|
1720
1724
|
}
|
|
1721
1725
|
|
|
1726
|
+
// The segment of the streamed handler or loader body running here; the
|
|
1727
|
+
// walker moves the shared `_currentSegmentId` on without awaiting either.
|
|
1728
|
+
const segmentPinALS: AsyncLocalStorage<string | undefined> = ((
|
|
1729
|
+
globalThis as any
|
|
1730
|
+
)[Symbol.for("rangojs-router:segment-pin")] ??= new AsyncLocalStorage<
|
|
1731
|
+
string | undefined
|
|
1732
|
+
>());
|
|
1733
|
+
|
|
1734
|
+
/**
|
|
1735
|
+
* Run `fn` as segment `segmentId`'s code: a handle push, a loader kickoff or
|
|
1736
|
+
* a "use cache" call it makes, before or after an await, files under
|
|
1737
|
+
* `segmentId` (segmentPinOf).
|
|
1738
|
+
*/
|
|
1739
|
+
export function runAsSegment<R>(segmentId: string | undefined, fn: () => R): R {
|
|
1740
|
+
return segmentPinALS.run(segmentId, fn);
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
/**
|
|
1744
|
+
* The segment code here runs for: the innermost runAsSegment scope, else
|
|
1745
|
+
* `ctx`'s pin (a handler the walker awaits).
|
|
1746
|
+
*/
|
|
1747
|
+
export function segmentPinOf(ctx: {
|
|
1748
|
+
_currentSegmentId?: string;
|
|
1749
|
+
}): string | undefined {
|
|
1750
|
+
return segmentPinALS.getStore() ?? ctx._currentSegmentId;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1722
1753
|
// Scope for handle PUSH CALLBACKS (push(() => ...), including async ones).
|
|
1723
1754
|
// A push callback's value is stored as-is; if it is a promise it is NOT tracked
|
|
1724
1755
|
// by handleStore.settled and does not block segment resolution, so a
|
|
@@ -330,7 +330,8 @@ export function invalidateClientCache(): void {
|
|
|
330
330
|
*
|
|
331
331
|
* Sets an internal response header the bridge reads. Idempotent within a
|
|
332
332
|
* request. Inert (a dev warning) outside a request context — there is no
|
|
333
|
-
* automatic invalidation to suppress.
|
|
333
|
+
* automatic invalidation to suppress. It also keeps the state's write part
|
|
334
|
+
* (runActionBody).
|
|
334
335
|
*/
|
|
335
336
|
export function keepClientCache(): void {
|
|
336
337
|
const ctx = _getRequestContext();
|
|
@@ -347,6 +348,26 @@ export function keepClientCache(): void {
|
|
|
347
348
|
ctx._setKeepCacheDirective();
|
|
348
349
|
}
|
|
349
350
|
|
|
351
|
+
/**
|
|
352
|
+
* Run a server action's body (the JS call or a no-JS form post), then bump the
|
|
353
|
+
* rango state's write part for the client that called it: the response's
|
|
354
|
+
* state `Set-Cookie` keeps the client's timestamp and carries a new write part
|
|
355
|
+
* (browser/cookie-name.ts). Every later request of every tab and reload sends
|
|
356
|
+
* the new `X-Rango-State`, so a prefetch or navigation the browser HTTP cache
|
|
357
|
+
* holds from before the write (a response's own `Cache-Control: max-age`)
|
|
358
|
+
* misses, while the client's in-memory caches only go stale, as the action
|
|
359
|
+
* bridge marks them. Bumped however the body ends (returned, threw, or
|
|
360
|
+
* redirected), since the write may have committed either way, and not after
|
|
361
|
+
* keepClientCache() or invalidateClientCache() (whose rotation covers it).
|
|
362
|
+
*/
|
|
363
|
+
export async function runActionBody<T>(body: () => T | Promise<T>): Promise<T> {
|
|
364
|
+
try {
|
|
365
|
+
return await body();
|
|
366
|
+
} finally {
|
|
367
|
+
_getRequestContext()?._rotateStateCookie("write");
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
350
371
|
/**
|
|
351
372
|
* Create a CookieStore backed by a RequestContext. Reads go through the
|
|
352
373
|
* context's unguarded `_readCookie()` / `_readCookies()`: cookies() ran the
|
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
* reads it on the next GET document request that navigates to the target
|
|
15
15
|
* pathname (`Sec-Fetch-Dest` a browsing context, where sent), seeds
|
|
16
16
|
* `_locationState` with its entries (so the render attaches them to the
|
|
17
|
-
* bootstrap payload, rsc/rsc-rendering.ts) and clears it.
|
|
17
|
+
* bootstrap payload, rsc/rsc-rendering.ts) and clears it. A shell HIT that
|
|
18
|
+
* degrades into a reload after its headers cleared the cookie sets it
|
|
19
|
+
* again from script first (`_carriedLocationStateCookie`), so the reload
|
|
20
|
+
* carries the state.
|
|
18
21
|
*
|
|
19
22
|
* A document that carries location state is per visitor: it is marked
|
|
20
23
|
* `_documentLocationState`, never stored by the document cache or as a PPR
|
|
@@ -136,7 +139,10 @@ function isNavigationRequest(request: Request): boolean {
|
|
|
136
139
|
* slots are taken from it (the value comes from the browser).
|
|
137
140
|
*/
|
|
138
141
|
export function consumeDocumentLocationState(
|
|
139
|
-
reqCtx: Pick<
|
|
142
|
+
reqCtx: Pick<
|
|
143
|
+
RequestContext,
|
|
144
|
+
"_locationState" | "_carriedLocationStateCookie" | "setCookie"
|
|
145
|
+
>,
|
|
140
146
|
request: Request,
|
|
141
147
|
url: URL,
|
|
142
148
|
stateCookieName: string,
|
|
@@ -159,6 +165,21 @@ export function consumeDocumentLocationState(
|
|
|
159
165
|
}
|
|
160
166
|
if (entries.length === 0) return;
|
|
161
167
|
reqCtx._locationState = entries;
|
|
168
|
+
reqCtx._carriedLocationStateCookie = restoreCookieLine(name, raw, secure);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The `document.cookie` line that sets the consumed cookie again, for a
|
|
173
|
+
* shell HIT that reloads after its headers cleared it (rsc/rsc-rendering.ts
|
|
174
|
+
* serveShellHit). Script cannot write an `HttpOnly` cookie, so this one is
|
|
175
|
+
* readable by the page's script for its short life; it carries nothing the
|
|
176
|
+
* page's payload does not already hold.
|
|
177
|
+
*/
|
|
178
|
+
function restoreCookieLine(name: string, raw: string, secure: boolean): string {
|
|
179
|
+
return (
|
|
180
|
+
`${name}=${raw}; Path=/; Max-Age=${DOCUMENT_LOCATION_STATE_MAX_AGE}; ` +
|
|
181
|
+
`SameSite=Lax${secure ? "; Secure" : ""}`
|
|
182
|
+
);
|
|
162
183
|
}
|
|
163
184
|
|
|
164
185
|
/** JSON keeps it as it is: what the cookie can carry without change. */
|
|
@@ -302,6 +302,8 @@ export interface HandleStore {
|
|
|
302
302
|
* so a loader that throws or skips its push shows none rather than the
|
|
303
303
|
* record's. A hole's replays go with them. Any other loader's replays stay
|
|
304
304
|
* (a dependency another entry's HIT replayed), and so do restored copies.
|
|
305
|
+
* A prefetch that defers the loader calls it too (revalidation.ts
|
|
306
|
+
* resolveOwnerLoaders): no run is made, so none of its placeholders stand.
|
|
305
307
|
*/
|
|
306
308
|
settleLoaderRun(loaderId: string): void;
|
|
307
309
|
|