@rangojs/router 0.27.0 → 0.28.0
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/bin/rango.js +1 -1
- package/dist/types/__internal.d.ts +7 -1
- package/dist/types/browser/event-controller.d.ts +2 -1
- package/dist/types/browser/history-state.d.ts +3 -9
- package/dist/types/browser/navigation-store.d.ts +8 -6
- package/dist/types/browser/react/filter-segment-order.d.ts +2 -2
- package/dist/types/browser/segment-reconciler.d.ts +6 -4
- package/dist/types/browser/types.d.ts +2 -1
- package/dist/types/browser/wire-protocol.d.ts +25 -0
- package/dist/types/cache/cache-key-utils.d.ts +11 -5
- package/dist/types/cache/cache-scope.d.ts +34 -22
- package/dist/types/cache/cf/cf-cache-constants.d.ts +2 -2
- package/dist/types/cache/cf/cf-cache-store.d.ts +47 -65
- package/dist/types/cache/cf/cf-kv-utils.d.ts +1 -1
- package/dist/types/cache/handle-snapshot.d.ts +9 -4
- package/dist/types/cache/segment-codec.d.ts +14 -0
- package/dist/types/cache/shell-snapshot.d.ts +1 -1
- package/dist/types/cache/tag-invalidation.d.ts +10 -0
- package/dist/types/cache/taint.d.ts +5 -3
- package/dist/types/cache/types.d.ts +7 -3
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +8 -6
- package/dist/types/prefetch-markers.d.ts +5 -14
- package/dist/types/prerender/store.d.ts +31 -0
- package/dist/types/prerender/warm-request.d.ts +3 -3
- package/dist/types/prerender.d.ts +2 -2
- package/dist/types/route-name.d.ts +5 -0
- package/dist/types/router/error-handling.d.ts +4 -4
- package/dist/types/router/instrument.d.ts +4 -3
- package/dist/types/router/intercept-resolution.d.ts +25 -9
- package/dist/types/router/match-context.d.ts +4 -11
- package/dist/types/router/match-handlers.d.ts +2 -6
- package/dist/types/router/match-middleware/background-revalidation.d.ts +10 -20
- package/dist/types/router/match-middleware/cache-lookup.d.ts +34 -86
- package/dist/types/router/match-middleware/segment-resolution.d.ts +8 -6
- package/dist/types/router/prerender-match.d.ts +21 -6
- package/dist/types/router/preview-match.d.ts +2 -7
- package/dist/types/router/request-classification.d.ts +5 -0
- package/dist/types/router/router-context.d.ts +7 -46
- package/dist/types/router/router-interfaces.d.ts +8 -14
- package/dist/types/router/segment-resolution/loader-cache.d.ts +3 -3
- package/dist/types/router/segment-resolution/loader-mask.d.ts +4 -4
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +20 -18
- package/dist/types/router/segment-resolution/resolution-pass.d.ts +93 -0
- package/dist/types/router/segment-resolution/revalidation.d.ts +82 -31
- package/dist/types/router/segment-resolution.d.ts +1 -2
- package/dist/types/router/segment-wrappers.d.ts +11 -48
- package/dist/types/router/telemetry.d.ts +2 -0
- package/dist/types/router/types.d.ts +10 -0
- package/dist/types/rsc/full-payload.d.ts +2 -2
- package/dist/types/rsc/handler-context.d.ts +6 -6
- package/dist/types/rsc/json-route-result.d.ts +5 -9
- package/dist/types/rsc/request-stages.d.ts +203 -0
- package/dist/types/rsc/response-cache-serve.d.ts +7 -12
- package/dist/types/rsc/response-route-handler.d.ts +2 -2
- package/dist/types/rsc/routine-plan.d.ts +1 -1
- package/dist/types/rsc/server-action.d.ts +2 -2
- package/dist/types/rsc/shell-capture-constants.d.ts +1 -1
- package/dist/types/rsc/shell-capture.d.ts +132 -15
- package/dist/types/rsc/shell-serve.d.ts +37 -12
- package/dist/types/server/context.d.ts +9 -8
- package/dist/types/server/handle-store.d.ts +16 -0
- package/dist/types/server/render-pass.d.ts +72 -0
- package/dist/types/server/request-context.d.ts +24 -104
- package/dist/types/server/rewrite-trail.d.ts +2 -2
- package/dist/types/testing/dispatch.d.ts +35 -18
- package/dist/types/types/cache-types.d.ts +6 -3
- package/dist/types/types/error-types.d.ts +9 -1
- package/dist/types/types/handler-context.d.ts +9 -1
- package/dist/types/types/segments.d.ts +1 -1
- package/dist/types/vite/discovery/build-discovery-phase.d.ts +40 -0
- package/dist/types/vite/discovery/build-env.d.ts +22 -0
- package/dist/types/vite/discovery/bundle-scan-phase.d.ts +12 -0
- package/dist/types/vite/discovery/dev-prerender-cache.d.ts +3 -2
- package/dist/types/vite/discovery/dev-server-phase.d.ts +26 -0
- package/dist/types/vite/discovery/discovery-errors.d.ts +1 -1
- package/dist/types/vite/discovery/state.d.ts +12 -0
- package/dist/types/vite/plugins/version-plugin.d.ts +1 -1
- package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
- package/dist/types/vite/router-discovery.d.ts +8 -47
- package/dist/types/vite/temp-rsc-server.d.ts +39 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +9 -0
- package/dist/types/wire-params.d.ts +34 -0
- package/dist/vite/index.js +3809 -3710
- package/package.json +1 -1
- package/skills/cache-guide/SKILL.md +8 -5
- package/skills/caching/SKILL.md +37 -18
- package/skills/document-cache/SKILL.md +13 -1
- package/skills/handler-use/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +17 -7
- package/skills/middleware/SKILL.md +18 -0
- package/skills/ppr/SKILL.md +18 -3
- package/skills/prerender/SKILL.md +39 -15
- package/skills/rango/SKILL.md +5 -1
- package/skills/server-actions/SKILL.md +8 -1
- package/skills/testing/response-routes.md +6 -4
- package/skills/use-cache/SKILL.md +46 -8
- package/src/__internal.ts +7 -1
- package/src/browser/connection-warmup.ts +3 -1
- package/src/browser/event-controller.ts +5 -3
- package/src/browser/history-state.ts +6 -12
- package/src/browser/navigation-bridge.ts +14 -13
- package/src/browser/navigation-client.ts +18 -25
- package/src/browser/navigation-store.ts +25 -27
- package/src/browser/navigation-transaction.ts +4 -1
- package/src/browser/prefetch/cache.ts +2 -1
- package/src/browser/prefetch/fetch.ts +8 -16
- package/src/browser/prefetch/queue.ts +4 -4
- package/src/browser/react/NavigationProvider.tsx +2 -1
- package/src/browser/react/filter-segment-order.ts +2 -2
- package/src/browser/rsc-router.tsx +1 -0
- package/src/browser/segment-reconciler.ts +66 -53
- package/src/browser/server-action-bridge.ts +50 -57
- package/src/browser/types.ts +2 -1
- package/src/browser/wire-protocol.ts +41 -0
- package/src/cache/cache-key-utils.ts +11 -5
- package/src/cache/cache-runtime.ts +110 -50
- package/src/cache/cache-scope.ts +110 -64
- package/src/cache/cf/cf-cache-constants.ts +2 -2
- package/src/cache/cf/cf-cache-store.ts +486 -874
- package/src/cache/cf/cf-kv-utils.ts +1 -1
- package/src/cache/document-cache.ts +34 -13
- package/src/cache/handle-snapshot.ts +19 -7
- package/src/cache/segment-codec.ts +55 -0
- package/src/cache/shell-snapshot.ts +2 -2
- package/src/cache/tag-invalidation.ts +21 -2
- package/src/cache/taint.ts +5 -3
- package/src/cache/types.ts +7 -3
- package/src/cache/vercel/vercel-cache-store.ts +243 -310
- package/src/handles/pushed-by.ts +1 -1
- package/src/prefetch-markers.ts +5 -16
- package/src/prerender/store.ts +76 -0
- package/src/prerender/warm-request.ts +8 -4
- package/src/prerender.ts +2 -2
- package/src/route-name.ts +8 -0
- package/src/router/error-handling.ts +4 -4
- package/src/router/instrument.ts +4 -3
- package/src/router/intercept-resolution.ts +77 -75
- package/src/router/loader-resolution.ts +5 -4
- package/src/router/match-api.ts +4 -5
- package/src/router/match-context.ts +20 -12
- package/src/router/match-handlers.ts +8 -20
- package/src/router/match-middleware/background-revalidation.ts +41 -53
- package/src/router/match-middleware/cache-lookup.ts +675 -726
- package/src/router/match-middleware/cache-store.ts +4 -8
- package/src/router/match-middleware/intercept-resolution.ts +3 -23
- package/src/router/match-middleware/segment-resolution.ts +34 -61
- package/src/router/middleware.ts +69 -11
- package/src/router/navigation-snapshot.ts +4 -4
- package/src/router/prerender-match.ts +100 -44
- package/src/router/preview-match.ts +6 -41
- package/src/router/request-classification.ts +13 -7
- package/src/router/route-snapshot.ts +2 -5
- package/src/router/router-context.ts +10 -95
- package/src/router/router-interfaces.ts +8 -14
- package/src/router/segment-resolution/loader-cache.ts +14 -9
- package/src/router/segment-resolution/loader-mask.ts +5 -5
- package/src/router/segment-resolution/prefetch-deferral.ts +30 -27
- package/src/router/segment-resolution/resolution-pass.ts +137 -0
- package/src/router/segment-resolution/revalidation.ts +897 -776
- package/src/router/segment-resolution.ts +4 -13
- package/src/router/segment-wrappers.ts +51 -269
- package/src/router/telemetry.ts +5 -0
- package/src/router/types.ts +10 -0
- package/src/router.ts +12 -13
- package/src/rsc/full-payload.ts +6 -5
- package/src/rsc/handler-context.ts +8 -14
- package/src/rsc/handler.ts +126 -507
- package/src/rsc/json-route-result.ts +5 -9
- package/src/rsc/loader-fetch.ts +6 -5
- package/src/rsc/progressive-enhancement.ts +5 -0
- package/src/rsc/request-stages.ts +757 -0
- package/src/rsc/response-cache-serve.ts +10 -14
- package/src/rsc/response-route-handler.ts +13 -5
- package/src/rsc/rewrite-passes.ts +2 -1
- package/src/rsc/routine-plan.ts +1 -1
- package/src/rsc/rsc-rendering.ts +164 -349
- package/src/rsc/server-action.ts +87 -113
- package/src/rsc/shell-capture-constants.ts +1 -1
- package/src/rsc/shell-capture.ts +212 -169
- package/src/rsc/shell-serve.ts +121 -18
- package/src/rsc/ssr-setup.ts +5 -4
- package/src/segment-system.tsx +3 -3
- package/src/server/context.ts +22 -25
- package/src/server/handle-store.ts +55 -0
- package/src/server/render-pass.ts +291 -0
- package/src/server/request-context.ts +26 -115
- package/src/server/rewrite-trail.ts +6 -4
- package/src/ssr/index.tsx +1 -1
- package/src/testing/dispatch.ts +223 -710
- package/src/testing/e2e/page-helpers.ts +2 -2
- package/src/testing/serve-shell-request.ts +23 -36
- package/src/types/cache-types.ts +6 -3
- package/src/types/error-types.ts +9 -1
- package/src/types/handler-context.ts +12 -1
- package/src/types/segments.ts +1 -1
- package/src/vite/discovery/build-discovery-phase.ts +204 -0
- package/src/vite/discovery/build-env.ts +148 -0
- package/src/vite/discovery/bundle-scan-phase.ts +128 -0
- package/src/vite/discovery/dev-prerender-cache.ts +8 -3
- package/src/vite/discovery/dev-server-phase.ts +1776 -0
- package/src/vite/discovery/discover-routers.ts +1 -1
- package/src/vite/discovery/discovery-errors.ts +1 -1
- package/src/vite/discovery/prerender-collection.ts +24 -39
- package/src/vite/discovery/state.ts +15 -0
- package/src/vite/plugins/version-plugin.ts +1 -1
- package/src/vite/plugins/virtual-entries.ts +1 -1
- package/src/vite/router-discovery.ts +29 -2428
- package/src/vite/temp-rsc-server.ts +223 -0
- package/src/vite/utils/prerender-utils.ts +47 -0
- package/src/wire-params.ts +35 -0
- package/dist/types/router/segment-resolution/fresh.d.ts +0 -72
- package/src/router/segment-resolution/fresh.ts +0 -945
|
@@ -52,6 +52,7 @@ import { stampLocationState } from "./react/location-state-shared.js";
|
|
|
52
52
|
import { classifyActionOutcome } from "./action-coordinator.js";
|
|
53
53
|
import { revalidationErrorOf } from "../decode-loader-results.js";
|
|
54
54
|
import { getAppVersion } from "./app-version.js";
|
|
55
|
+
import { writePartialParams } from "./wire-protocol.js";
|
|
55
56
|
import {
|
|
56
57
|
decideCommitGatedOff,
|
|
57
58
|
findActionFormData,
|
|
@@ -487,28 +488,16 @@ export function createServerActionBridge(
|
|
|
487
488
|
// Create temporary references for serialization
|
|
488
489
|
const temporaryReferences = deps.createTemporaryReferenceSet();
|
|
489
490
|
|
|
490
|
-
// Build action request URL with current segments
|
|
491
|
-
|
|
492
|
-
url.
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
url.searchParams.set("_rsc_v", version);
|
|
501
|
-
}
|
|
502
|
-
// Add router ID for app switch detection
|
|
503
|
-
const rid = store.getRouterId?.();
|
|
504
|
-
if (rid) {
|
|
505
|
-
url.searchParams.set("_rsc_rid", rid);
|
|
506
|
-
}
|
|
507
|
-
// The page's rewrite target: the server diffs the re-render against it.
|
|
508
|
-
const rewriteTarget = store.getHistoryEntryMemory()?.rewriteTarget;
|
|
509
|
-
if (rewriteTarget) {
|
|
510
|
-
url.searchParams.set("_rsc_rt", rewriteTarget);
|
|
511
|
-
}
|
|
491
|
+
// Build action request URL with current segments; the server diffs the
|
|
492
|
+
// re-render against the page's rewrite target.
|
|
493
|
+
const url = writePartialParams(new URL(window.location.href), {
|
|
494
|
+
action: id,
|
|
495
|
+
segmentIds: segmentState.currentSegmentIds,
|
|
496
|
+
emptySegments: "send",
|
|
497
|
+
version: getAppVersion(),
|
|
498
|
+
routerId: store.getRouterId?.(),
|
|
499
|
+
rewriteTarget: store.getHistoryEntryMemory()?.rewriteTarget,
|
|
500
|
+
});
|
|
512
501
|
|
|
513
502
|
// Encode arguments
|
|
514
503
|
const encodedBody = await deps.encodeReply(args, { temporaryReferences });
|
|
@@ -740,36 +729,50 @@ export function createServerActionBridge(
|
|
|
740
729
|
}
|
|
741
730
|
|
|
742
731
|
// Handle error responses with error boundary UI
|
|
743
|
-
if (isError && isPartial && segments
|
|
732
|
+
if (isError && isPartial && segments) {
|
|
744
733
|
log("processing error boundary response");
|
|
745
734
|
|
|
746
|
-
//
|
|
747
|
-
//
|
|
735
|
+
// transition({ when }) sees action.error on the boundary commit and on
|
|
736
|
+
// a dropped one's follow-up.
|
|
737
|
+
whenAction = {
|
|
738
|
+
...whenAction,
|
|
739
|
+
error: returnValue && !returnValue.ok ? returnValue.data : undefined,
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
// No consolidation: the boundary replaces the page, or the page left.
|
|
743
|
+
handle.clearConsolidation();
|
|
744
|
+
|
|
745
|
+
const currentKey = store.getHistoryKey();
|
|
746
|
+
const pageLeft = () =>
|
|
747
|
+
navigatedSince() || store.getHistoryKey() !== currentKey;
|
|
748
|
+
// The action's page left the screen (during the flight or the error
|
|
749
|
+
// render): drop the boundary; the call rejects and the exit owes the
|
|
750
|
+
// follow-up.
|
|
751
|
+
const dropBoundary = () => {
|
|
752
|
+
log("user navigated during error render, skipping");
|
|
753
|
+
if (returnValue && !returnValue.ok) throw returnValue.data;
|
|
754
|
+
handle.complete(undefined);
|
|
755
|
+
return undefined;
|
|
756
|
+
};
|
|
757
|
+
if (pageLeft()) return dropBoundary();
|
|
758
|
+
|
|
759
|
+
// fail() settles once the response's stream ends; the abort below
|
|
760
|
+
// spares this action, so its entry keeps the error for useAction().
|
|
748
761
|
if (returnValue && !returnValue.ok) {
|
|
749
762
|
handle.fail(returnValue.data);
|
|
750
763
|
}
|
|
751
764
|
|
|
752
|
-
// Abort
|
|
753
|
-
//
|
|
754
|
-
eventController.abortAllActions();
|
|
755
|
-
|
|
756
|
-
// Clear concurrent action tracking - no consolidation needed when showing error
|
|
757
|
-
handle.clearConsolidation();
|
|
765
|
+
// Abort the other actions in flight: the error takes precedence, so
|
|
766
|
+
// none completes over the boundary.
|
|
767
|
+
eventController.abortAllActions(handle.id);
|
|
758
768
|
|
|
759
769
|
// Get current page's cached segments
|
|
760
|
-
const currentKey = store.getHistoryKey();
|
|
761
770
|
const cached = store.getCachedSegments(currentKey);
|
|
762
771
|
const cachedSegments = cached?.segments || [];
|
|
763
772
|
|
|
764
773
|
// Reconcile error segments with cached tree
|
|
765
774
|
const errorResult = reconcileErrorSegments(cachedSegments, segments);
|
|
766
775
|
|
|
767
|
-
// transition({ when }) decides the error-boundary commit too, with
|
|
768
|
-
// `action.error` set.
|
|
769
|
-
whenAction = {
|
|
770
|
-
...whenAction,
|
|
771
|
-
error: returnValue && !returnValue.ok ? returnValue.data : undefined,
|
|
772
|
-
};
|
|
773
776
|
const errorGatedOff = decideActionGatedOff(
|
|
774
777
|
errorResult.segments,
|
|
775
778
|
metadata,
|
|
@@ -786,28 +789,18 @@ export function createServerActionBridge(
|
|
|
786
789
|
: undefined,
|
|
787
790
|
});
|
|
788
791
|
|
|
789
|
-
|
|
790
|
-
// flight or while the error tree rendered: the boundary is not shown,
|
|
791
|
-
// and the action's exit owes the page on screen the follow-up.
|
|
792
|
-
if (navigatedSince() || store.getHistoryKey() !== currentKey) {
|
|
793
|
-
log("user navigated during error render, skipping");
|
|
794
|
-
if (returnValue && !returnValue.ok) {
|
|
795
|
-
throw returnValue.data;
|
|
796
|
-
}
|
|
797
|
-
handle.complete(undefined);
|
|
798
|
-
return undefined;
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
// Update segment tracking to exclude error segment IDs
|
|
802
|
-
const errorSegmentIds = new Set(diff);
|
|
803
|
-
const segmentIdsAfterError = segmentState.currentSegmentIds.filter(
|
|
804
|
-
(id) => !errorSegmentIds.has(id),
|
|
805
|
-
);
|
|
792
|
+
if (pageLeft()) return dropBoundary();
|
|
806
793
|
|
|
807
794
|
// Cache (and bump the nav instance) BEFORE the UI update so a deferred
|
|
808
795
|
// handle pushed by the error-boundary render still applies — see the
|
|
809
|
-
// "normal" case below for why caching after onUpdate dropped it.
|
|
810
|
-
|
|
796
|
+
// "normal" case below for why caching after onUpdate dropped it. The
|
|
797
|
+
// page stops claiming the error segments only: the loaders the
|
|
798
|
+
// response re-ran above the boundary stay held.
|
|
799
|
+
store.setSegmentIds(
|
|
800
|
+
segmentState.currentSegmentIds.filter(
|
|
801
|
+
(id) => !segments.some((s) => s.id === id && s.type === "error"),
|
|
802
|
+
),
|
|
803
|
+
);
|
|
811
804
|
const currentHandleData = liveHandleDataForCache(
|
|
812
805
|
eventController,
|
|
813
806
|
store,
|
package/src/browser/types.ts
CHANGED
|
@@ -649,7 +649,8 @@ export interface NavigationStore {
|
|
|
649
649
|
handlesPending?: boolean;
|
|
650
650
|
}
|
|
651
651
|
| undefined;
|
|
652
|
-
|
|
652
|
+
/** Whether an intercept entry is cached for the plain `historyKey`, from any source page. */
|
|
653
|
+
hasInterceptCache(historyKey: string): boolean;
|
|
653
654
|
/**
|
|
654
655
|
* Update only the handleData (and optionally the stale / handlesPending flags)
|
|
655
656
|
* of an existing cache entry. When a flag is omitted the entry's current value
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { WIRE } from "../wire-params.js";
|
|
2
|
+
|
|
3
|
+
/** The parameters of a partial request: a navigation, a prefetch or a JS server action. */
|
|
4
|
+
export interface PartialRequestInit {
|
|
5
|
+
/** A JS server action's id: written as `_rsc_action` in place of `_rsc_partial`. */
|
|
6
|
+
action?: string;
|
|
7
|
+
/** The mounted segment ids the server diffs against. */
|
|
8
|
+
segmentIds: readonly string[];
|
|
9
|
+
/**
|
|
10
|
+
* An empty `segmentIds`: "send" writes `_rsc_segments=` (navigation,
|
|
11
|
+
* action), "omit" leaves it out (prefetch). The prefetch cache keys carry
|
|
12
|
+
* the difference.
|
|
13
|
+
*/
|
|
14
|
+
emptySegments: "send" | "omit";
|
|
15
|
+
stale?: boolean;
|
|
16
|
+
version?: string;
|
|
17
|
+
routerId?: string;
|
|
18
|
+
rewriteTarget?: string;
|
|
19
|
+
fill?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Writes the partial-request parameters onto `url` in the one order every
|
|
24
|
+
* sender uses. Navigation and prefetch cache keys are built from the result
|
|
25
|
+
* (prefetch/cache.ts), so a navigation adopts a prefetch only while both
|
|
26
|
+
* write identically: browser/__tests__/wire-urls.test.ts pins the strings.
|
|
27
|
+
*/
|
|
28
|
+
export function writePartialParams(url: URL, init: PartialRequestInit): URL {
|
|
29
|
+
const params = url.searchParams;
|
|
30
|
+
if (init.action !== undefined) params.set(WIRE.action, init.action);
|
|
31
|
+
else params.set(WIRE.partial, "true");
|
|
32
|
+
if (init.segmentIds.length > 0 || init.emptySegments === "send") {
|
|
33
|
+
params.set(WIRE.segments, init.segmentIds.join(","));
|
|
34
|
+
}
|
|
35
|
+
if (init.stale) params.set(WIRE.stale, "true");
|
|
36
|
+
if (init.version) params.set(WIRE.version, init.version);
|
|
37
|
+
if (init.routerId) params.set(WIRE.routerId, init.routerId);
|
|
38
|
+
if (init.rewriteTarget) params.set(WIRE.rewriteTarget, init.rewriteTarget);
|
|
39
|
+
if (init.fill) params.set(WIRE.fill, "1");
|
|
40
|
+
return url;
|
|
41
|
+
}
|
|
@@ -187,8 +187,10 @@ export const KEY_PART_PREFIX = "key:";
|
|
|
187
187
|
* (CacheScope.resolveKeyFrom, CacheScope.resolvePartition): the `key()`
|
|
188
188
|
* results of the scope's cache() chain, outermost first, then its default
|
|
189
189
|
* parts: the keyGenerator results of enclosing scopes on another store
|
|
190
|
-
* (#974) and, for a record key
|
|
191
|
-
* default key (or its store's keyGenerator result,
|
|
190
|
+
* (#974) and, for a route record key, the scope's own part: without a
|
|
191
|
+
* `key()` of its own, its default key (or its store's keyGenerator result,
|
|
192
|
+
* #970); with one, the default key's request kind (`doc`, `partial` or
|
|
193
|
+
* `intercept:<intercept>`, routeRecordKind in cache-scope.ts).
|
|
192
194
|
*
|
|
193
195
|
* Key scheme invariant (#975). A `key()` result is often request input,
|
|
194
196
|
* and a record key has no route or scope discriminator (the store reads
|
|
@@ -214,9 +216,13 @@ export const KEY_PART_PREFIX = "key:";
|
|
|
214
216
|
* A namespaced `key()` result starts with `key:` and holds no `|`. A
|
|
215
217
|
* composed key holds a `|` and either starts with `key:` or holds no `:`.
|
|
216
218
|
* Among composed keys, splitting on `|` recovers each part, a part's kind
|
|
217
|
-
* (a `key:` prefix, or no `:` at all) and its value.
|
|
218
|
-
*
|
|
219
|
-
*
|
|
219
|
+
* (a `key:` prefix, or no `:` at all) and its value. A request kind part is
|
|
220
|
+
* a default key's prefix without the URL after it, so it never equals a
|
|
221
|
+
* default key part; a keyGenerator part can equal it (`doc`) only when the
|
|
222
|
+
* keyGenerator returns a constant, which already gives every URL one record.
|
|
223
|
+
* Pinned by the collision probe in
|
|
224
|
+
* src/cache/__tests__/cache-scope-chain.test.ts. A keyGenerator result is
|
|
225
|
+
* the store's own namespace, as before.
|
|
220
226
|
*
|
|
221
227
|
* Shared with the testing helper `shellCacheKey`, so a test builds the
|
|
222
228
|
* production partition.
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
} from "../deps/rsc-client.js";
|
|
23
23
|
import {
|
|
24
24
|
getRequestContext,
|
|
25
|
-
|
|
25
|
+
type RequestContext,
|
|
26
26
|
} from "../server/request-context.js";
|
|
27
27
|
import { isUnderTestRunner } from "../runtime-env.js";
|
|
28
28
|
import {
|
|
@@ -34,7 +34,12 @@ import {
|
|
|
34
34
|
} from "./taint.js";
|
|
35
35
|
|
|
36
36
|
export { isCachedFunction };
|
|
37
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
serializeResult,
|
|
39
|
+
deserializeResult,
|
|
40
|
+
isUnsupportedValueError,
|
|
41
|
+
teeResult,
|
|
42
|
+
} from "./segment-codec.js";
|
|
38
43
|
import {
|
|
39
44
|
appendHandles,
|
|
40
45
|
encodeHandles,
|
|
@@ -70,6 +75,7 @@ import {
|
|
|
70
75
|
executionStart,
|
|
71
76
|
invalidatedSince,
|
|
72
77
|
predatesInvalidation,
|
|
78
|
+
runAsBackgroundRefresh,
|
|
73
79
|
type ExecutionStart,
|
|
74
80
|
} from "./tag-invalidation.js";
|
|
75
81
|
import type { CacheItemResult } from "./types.js";
|
|
@@ -384,6 +390,23 @@ function refreshView(arg: unknown): unknown {
|
|
|
384
390
|
});
|
|
385
391
|
}
|
|
386
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Run `fn` inside a "use cache" execution's scopes: its exec scope
|
|
395
|
+
* (cookies()/headers() reads and ctx writes refuse) and its tag scope
|
|
396
|
+
* (cacheTag() and nested calls tag the entry, #980), `recorded` when given.
|
|
397
|
+
* The body runs here, and so does the value's encode: it renders the server
|
|
398
|
+
* components in the value, which are part of the entry. Scar: the encode ran
|
|
399
|
+
* in the tag scope only, so a returned component's cookies() read went
|
|
400
|
+
* through and the entry served the first visitor's value to the next.
|
|
401
|
+
*/
|
|
402
|
+
function inExecution<T>(
|
|
403
|
+
scope: CacheExecScope,
|
|
404
|
+
fn: () => T,
|
|
405
|
+
recorded?: Set<string>,
|
|
406
|
+
): { result: T; tags: Set<string> } {
|
|
407
|
+
return runWithCacheTagScope(() => runWithCacheExecScope(fn, scope), recorded);
|
|
408
|
+
}
|
|
409
|
+
|
|
387
410
|
// ============================================================================
|
|
388
411
|
// Core: registerCachedFunction
|
|
389
412
|
// ============================================================================
|
|
@@ -459,14 +482,6 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
459
482
|
const entryTags = (recorded: Set<string>): string[] => [
|
|
460
483
|
...new Set(normalizeTags([...(profile.tags ?? []), ...recorded])),
|
|
461
484
|
];
|
|
462
|
-
// The value's Flight encode runs back inside its execution's tag scope:
|
|
463
|
-
// it renders the server components in the value, and a "use cache" call
|
|
464
|
-
// or cacheTag() among them belongs to this entry (#980).
|
|
465
|
-
const inTagScope = <T>(
|
|
466
|
-
recorded: Set<string>,
|
|
467
|
-
encode: () => Promise<T>,
|
|
468
|
-
): Promise<T> => runWithCacheTagScope(encode, recorded).result;
|
|
469
|
-
|
|
470
485
|
// Separate request-scoped args (ctx, a Request, the request's env) from
|
|
471
486
|
// key-generating args. For those that carry route context (ctx params,
|
|
472
487
|
// pathname, searchParams; a Request's URL), extract serializable values
|
|
@@ -717,25 +732,24 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
717
732
|
// reads the ambient getRequestContext() (e.g.
|
|
718
733
|
// getRequestContext().env.ApiKey) resolves during the background
|
|
719
734
|
// revalidation instead of throwing "called outside of a request
|
|
720
|
-
// context".
|
|
721
|
-
// synchronous kickoff; its async continuations inherit it.
|
|
735
|
+
// context". runAsBackgroundRefresh sets the store for fn's
|
|
736
|
+
// synchronous kickoff; its async continuations inherit it. No
|
|
737
|
+
// visitor made the invalidations it runs: they set no
|
|
738
|
+
// fresh-reads cookie.
|
|
722
739
|
//
|
|
723
740
|
// The span opens inside the request ALS and wraps the WHOLE task —
|
|
724
741
|
// fn kickoff through the store write — so its platform spans nest
|
|
725
742
|
// under rango.background and a slow or failing setItem is part of
|
|
726
743
|
// the traced revalidation, not an untraced tail.
|
|
727
|
-
await
|
|
744
|
+
await runAsBackgroundRefresh(requestCtx, () =>
|
|
728
745
|
observePhase(
|
|
729
746
|
PHASES.background("use-cache-revalidation"),
|
|
730
747
|
async () => {
|
|
731
748
|
const refreshArgs = args.map(refreshView);
|
|
732
749
|
// The refresh reads its data from here on (#973, #977).
|
|
733
750
|
const start = executionStart();
|
|
734
|
-
const scoped =
|
|
735
|
-
|
|
736
|
-
() => fn.apply(this, refreshArgs),
|
|
737
|
-
bgScope,
|
|
738
|
-
),
|
|
751
|
+
const scoped = inExecution(bgScope, () =>
|
|
752
|
+
fn.apply(this, refreshArgs),
|
|
739
753
|
);
|
|
740
754
|
const freshResult = await scoped.result;
|
|
741
755
|
bgCapture?.stop();
|
|
@@ -744,17 +758,22 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
744
758
|
const onFlightError = (error: unknown): void => {
|
|
745
759
|
flightErrors.push(error);
|
|
746
760
|
};
|
|
747
|
-
const serialized = await
|
|
748
|
-
|
|
749
|
-
|
|
761
|
+
const serialized = await inExecution(
|
|
762
|
+
bgScope,
|
|
763
|
+
() => serializeResult(freshResult, onFlightError),
|
|
764
|
+
scoped.tags,
|
|
765
|
+
).result;
|
|
750
766
|
if (serialized !== null) {
|
|
751
767
|
const encodedHandles = bgCapture
|
|
752
|
-
? await
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
768
|
+
? await inExecution(
|
|
769
|
+
bgScope,
|
|
770
|
+
() =>
|
|
771
|
+
encodeHandles(
|
|
772
|
+
bgCapture.capture.data,
|
|
773
|
+
onFlightError,
|
|
774
|
+
),
|
|
775
|
+
scoped.tags,
|
|
776
|
+
).result
|
|
758
777
|
: undefined;
|
|
759
778
|
// An error row would replace the stale entry; the catch
|
|
760
779
|
// below reports it and the stale entry keeps serving.
|
|
@@ -921,21 +940,24 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
921
940
|
// deliberately NOT stamped: a property on that SHARED object made every
|
|
922
941
|
// parallel read on the request throw for the cached body's whole
|
|
923
942
|
// execution window (a 2s cached fetch poisoned a sibling loader's
|
|
924
|
-
// cookies() — same hazard class as issue #684, plan 010).
|
|
943
|
+
// cookies() — same hazard class as issue #684, plan 010). Not as an
|
|
944
|
+
// argument either (getRequestContext(), any pass: a derived pass inherits
|
|
945
|
+
// `_requestRoot`): a derived pass's writers that it does not route to a
|
|
946
|
+
// sink check the request's own context (server/render-pass.ts), so the
|
|
947
|
+
// stamp failed a shell capture's or a background re-render's writes while
|
|
948
|
+
// a foreground body held it.
|
|
925
949
|
const taintedArgs: unknown[] = [];
|
|
926
950
|
for (const arg of args) {
|
|
927
|
-
if (isTainted(arg)) {
|
|
951
|
+
if (isTainted(arg) && !(arg as RequestContext)._requestRoot) {
|
|
928
952
|
stampCacheExec(arg as object);
|
|
929
953
|
taintedArgs.push(arg);
|
|
930
954
|
}
|
|
931
955
|
}
|
|
932
956
|
|
|
933
957
|
let result: any;
|
|
934
|
-
let scoped: ReturnType<typeof
|
|
958
|
+
let scoped: ReturnType<typeof inExecution>;
|
|
935
959
|
try {
|
|
936
|
-
scoped =
|
|
937
|
-
runWithCacheExecScope(() => fn.apply(this, args), execScope),
|
|
938
|
-
);
|
|
960
|
+
scoped = inExecution(execScope, () => fn.apply(this, args));
|
|
939
961
|
result = await scoped.result;
|
|
940
962
|
} catch (execError) {
|
|
941
963
|
// The function threw: drop the in-flight entry and reject any waiters so
|
|
@@ -957,25 +979,39 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
957
979
|
const recorded = scoped!.tags;
|
|
958
980
|
recordRequestTags(entryTags(recorded), requestCtx);
|
|
959
981
|
|
|
960
|
-
//
|
|
961
|
-
//
|
|
962
|
-
//
|
|
963
|
-
//
|
|
982
|
+
// Encode the value ONCE, inside the execution's scopes, and hand the
|
|
983
|
+
// caller a decoded copy of it, as a hit does. The caller rendering
|
|
984
|
+
// `result` itself would share its lazy state (a React.lazy, a memoizing
|
|
985
|
+
// thenable) with the encode: whichever render reached it first decided
|
|
986
|
+
// its value for both, so a read the caller's render made outside the
|
|
987
|
+
// scope was stored for the next visitor. Then resolve the in-flight
|
|
988
|
+
// envelope so concurrent followers serve a synthetic hit, and persist.
|
|
989
|
+
// When waitUntil is available the leader waits for the copy's root row
|
|
990
|
+
// only, not for the rest of the encode or the store write.
|
|
991
|
+
const flightErrors: unknown[] = [];
|
|
992
|
+
const onFlightError = (error: unknown): void => {
|
|
993
|
+
flightErrors.push(error);
|
|
994
|
+
};
|
|
995
|
+
const encoded = inExecution(
|
|
996
|
+
execScope,
|
|
997
|
+
() => teeResult(result, onFlightError),
|
|
998
|
+
recorded,
|
|
999
|
+
).result;
|
|
964
1000
|
const finalizeAndWrite = async (): Promise<void> => {
|
|
965
1001
|
let serialized: string | null;
|
|
966
1002
|
let encodedHandles: string | undefined;
|
|
967
|
-
const flightErrors: unknown[] = [];
|
|
968
|
-
const onFlightError = (error: unknown): void => {
|
|
969
|
-
flightErrors.push(error);
|
|
970
|
-
};
|
|
971
1003
|
try {
|
|
972
|
-
serialized = await
|
|
973
|
-
|
|
974
|
-
|
|
1004
|
+
serialized = encoded ? await encoded.serialized : null;
|
|
1005
|
+
// The returned tree renders only in the encode, so its cacheTag()
|
|
1006
|
+
// calls tag the entry alone: record the entry's tags onto the
|
|
1007
|
+
// request, as a hit does.
|
|
1008
|
+
recordRequestTags(entryTags(recorded), requestCtx);
|
|
975
1009
|
encodedHandles = capture
|
|
976
|
-
? await
|
|
977
|
-
|
|
978
|
-
|
|
1010
|
+
? await inExecution(
|
|
1011
|
+
execScope,
|
|
1012
|
+
() => encodeHandles(capture.capture.data, onFlightError),
|
|
1013
|
+
recorded,
|
|
1014
|
+
).result
|
|
979
1015
|
: undefined;
|
|
980
1016
|
// Flight encodes an async component that throws, or a rejected
|
|
981
1017
|
// promise in the result or a handle value, as an error row and
|
|
@@ -986,10 +1022,17 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
986
1022
|
assertLoaderReadsClean(execScope);
|
|
987
1023
|
} catch (buildError) {
|
|
988
1024
|
// Serialize/handle-encode failed: no envelope for followers (they run
|
|
989
|
-
// fresh) and nothing to write.
|
|
1025
|
+
// fresh) and nothing to write. Logged too: a read the guard refused
|
|
1026
|
+
// in the value is otherwise silent with no onError. An error the call
|
|
1027
|
+
// also throws reaches onError once (the request dedups by identity).
|
|
990
1028
|
clearSelf();
|
|
991
1029
|
rejectEnvelope(buildError);
|
|
992
|
-
|
|
1030
|
+
reportCacheError(
|
|
1031
|
+
buildError,
|
|
1032
|
+
"cache-write",
|
|
1033
|
+
`[use cache] "${id}" write`,
|
|
1034
|
+
requestCtx,
|
|
1035
|
+
);
|
|
993
1036
|
return;
|
|
994
1037
|
}
|
|
995
1038
|
if (serialized === null) {
|
|
@@ -1039,7 +1082,24 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
1039
1082
|
|
|
1040
1083
|
await runBackground(requestCtx, finalizeAndWrite, true);
|
|
1041
1084
|
|
|
1042
|
-
|
|
1085
|
+
// The encode could not start: nothing is stored, the value renders live.
|
|
1086
|
+
if (!encoded) return result;
|
|
1087
|
+
try {
|
|
1088
|
+
return await encoded.decoded;
|
|
1089
|
+
} catch (decodeError) {
|
|
1090
|
+
// The root row failed. A value Flight cannot encode (a function, a
|
|
1091
|
+
// class instance) is not cached and renders live, as it always has;
|
|
1092
|
+
// anything else, a refused read above all, fails the call as the same
|
|
1093
|
+
// read in the body does, with the encode's own error (production
|
|
1094
|
+
// Flight redacts the decoded one).
|
|
1095
|
+
if (
|
|
1096
|
+
flightErrors.length > 0 &&
|
|
1097
|
+
flightErrors.every(isUnsupportedValueError)
|
|
1098
|
+
) {
|
|
1099
|
+
return result;
|
|
1100
|
+
}
|
|
1101
|
+
throw flightErrors[0] ?? decodeError;
|
|
1102
|
+
}
|
|
1043
1103
|
};
|
|
1044
1104
|
|
|
1045
1105
|
// Brand the wrapper so it can be detected at runtime (e.g., to prevent
|