@rsc-kit/core 0.20.7 → 0.20.8
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/cache.d.ts +11 -0
- package/dist/cache.js +13 -0
- package/dist/cache.js.map +1 -1
- package/dist/clientEntries.js +4 -1
- package/dist/clientEntries.js.map +1 -1
- package/dist/files.d.ts +38 -0
- package/dist/files.js +109 -0
- package/dist/files.js.map +1 -1
- package/dist/host.d.ts +11 -2
- package/dist/host.js +37 -6
- package/dist/host.js.map +1 -1
- package/dist/js/ActivityRouter.d.ts +11 -0
- package/dist/js/ActivityRouter.js +19 -4
- package/dist/js/ActivityRouter.js.map +1 -1
- package/dist/js/Form.js +24 -2
- package/dist/js/Form.js.map +1 -1
- package/dist/js/PathnameProvider.d.ts +15 -2
- package/dist/js/PathnameProvider.js +17 -2
- package/dist/js/PathnameProvider.js.map +1 -1
- package/dist/js/SegmentBoundary.js +19 -3
- package/dist/js/SegmentBoundary.js.map +1 -1
- package/dist/js/activityMarkers.d.ts +7 -0
- package/dist/js/activityMarkers.js +18 -0
- package/dist/js/activityMarkers.js.map +1 -0
- package/dist/js/createViteRscApp.js +122 -76
- package/dist/js/createViteRscApp.js.map +1 -1
- package/dist/js/earlyClicks.d.ts +3 -1
- package/dist/js/earlyClicks.js +28 -5
- package/dist/js/earlyClicks.js.map +1 -1
- package/dist/js/errors.d.ts +2 -0
- package/dist/js/errors.js +11 -0
- package/dist/js/errors.js.map +1 -1
- package/dist/js/imagePreload.d.ts +37 -0
- package/dist/js/imagePreload.js +116 -0
- package/dist/js/imagePreload.js.map +1 -0
- package/dist/js/navigate.d.ts +33 -0
- package/dist/js/navigate.js +200 -12
- package/dist/js/navigate.js.map +1 -1
- package/dist/js/segmentStore.d.ts +52 -0
- package/dist/js/segmentStore.js +123 -2
- package/dist/js/segmentStore.js.map +1 -1
- package/dist/js/staleAssets.d.ts +10 -0
- package/dist/js/staleAssets.js +23 -2
- package/dist/js/staleAssets.js.map +1 -1
- package/dist/js/viewportPrefetch.js +27 -2
- package/dist/js/viewportPrefetch.js.map +1 -1
- package/dist/shellHead.d.ts +22 -0
- package/dist/shellHead.js +43 -0
- package/dist/shellHead.js.map +1 -0
- package/dist/vite.js +237 -55
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
package/dist/js/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/js/errors.ts"],"names":[],"mappings":"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","sourcesContent":["export class ServerValidationError extends Error {\n public readonly errors: Record<string, string[]>;\n\n constructor(message: string, errors: Record<string, string[]>) {\n super(message);\n this.name = \"ServerValidationError\";\n this.errors = errors;\n }\n}\n\n/** An action answered with a location instead of a result. */\n/**\n * The redirect the last action answered with, for the one caller that asks.\n *\n * An action that redirects resolves - the navigation is already under way\n * and the caller has nothing to do - which leaves a form that wants to know\n * whether to say \"saved\" with no way to tell a redirect from a void answer.\n * callServer notes the destination here; <Form> reads and clears it right\n * after its await. Nothing else needs to.\n */\nlet lastRedirect: string | null = null;\n\nexport function noteRedirected(location: string): void {\n lastRedirect = location;\n}\n\n/** The redirect the action just performed, if it did - read once. */\nexport function redirectedTo(): string | null {\n const location = lastRedirect;\n\n lastRedirect = null;\n\n return location;\n}\n\nexport class ServerRedirectError extends Error {\n public readonly location: string;\n\n constructor(location: string) {\n super(`Server action redirected to ${location}`);\n this.name = \"ServerRedirectError\";\n this.location = location;\n }\n}\n\nexport class ServerAuthenticationError extends Error {\n constructor(message: string = \"Unauthenticated.\") {\n super(message);\n this.name = \"ServerAuthenticationError\";\n }\n}\n\nexport class ServerAuthorizationError extends Error {\n constructor(message: string = \"This action is unauthorized.\") {\n super(message);\n this.name = \"ServerAuthorizationError\";\n }\n}\n\nexport class ServerDumpError extends Error {\n constructor() {\n super(\"Server returned a dump response.\");\n this.name = \"ServerDumpError\";\n }\n}\n\nexport class ServerSessionExpiredError extends Error {\n constructor(message: string = \"Your session has expired. Please refresh the page.\") {\n super(message);\n this.name = \"ServerSessionExpiredError\";\n }\n}\n\n/**\n * Turn a failed server-action response into the error it describes.\n *\n * A server action that does not succeed answers with JSON or a redirect\n * header rather than a Flight stream. Passing one of those to the Flight\n * decoder does not produce the server's message — it produces an internal\n * parser failure (\"enqueueModel is not a function\") or a truncated read\n * (\"Connection closed.\"), which is what reached onError before this existed.\n *\n * Returns without throwing when the response is a stream to be decoded.\n */\nexport async function throwForFailedAction(response: Response): Promise<void> {\n // Before the status: a redirect the action asked for is a 204 with the\n // destination in this header - an ok answer with no Flight in it - and\n // an expired session's is a 401. Read first, the header decides for both.\n // Read after `ok`, the 204 fell through to the Flight decoder with an\n // empty body, and the form waited forever.\n const location = response.headers.get(\"X-RSC-Redirect\");\n\n if (location !== null && location !== \"\") {\n throw new ServerRedirectError(location);\n }\n\n if (response.ok) return;\n\n if (response.status === 422) {\n const payload = (await response.json().catch(() => null)) as\n | { message?: string; errors?: Record<string, string[]> }\n | null;\n\n throw new ServerValidationError(\n payload?.message ?? \"Validation failed\",\n payload?.errors ?? {},\n );\n }\n\n if (response.status === 413) {\n throw new Error(\n \"Server action refused: the request body is larger than the server accepts. \" +\n \"A file upload is the usual cause; the limit is the host's maxActionBody.\",\n );\n }\n\n throw new Error(`Server action failed with ${response.status}`);\n}\n\n/**\n * Reject a payload response that is not one.\n *\n * The page a PPR route serves is a static shell: real HTML, status 200, with\n * its Suspense fallbacks showing. Everything below them arrives in a second\n * request. If that request fails there is nothing on screen to say so — the\n * skeletons simply stay, for ever — and handing the failure body to the Flight\n * decoder reports the decoder's confusion rather than the status.\n */\nexport function throwForFailedPayload(response: Response): void {\n if (response.ok) return;\n\n throw new Error(`RSC payload request failed with ${response.status}`);\n}\n\n/**\n * Announce a failure that nothing else will.\n *\n * Dispatched as well as logged: an app that wants to replace a stuck skeleton\n * with something honest has no other way to find out.\n */\nexport function reportClientFailure(scope: string, error: unknown): void {\n if (typeof window !== \"undefined\") {\n window.dispatchEvent(new CustomEvent(\"rsc-client-error\", { detail: { scope, error } }));\n }\n\n console.error(`[rsc-kit] ${scope}`, error);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/js/errors.ts"],"names":[],"mappings":"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","sourcesContent":["export class ServerValidationError extends Error {\n public readonly errors: Record<string, string[]>;\n\n constructor(message: string, errors: Record<string, string[]>) {\n super(message);\n this.name = \"ServerValidationError\";\n this.errors = errors;\n }\n}\n\n/** An action answered with a location instead of a result. */\n/**\n * The redirect the last action answered with, for the one caller that asks.\n *\n * An action that redirects resolves - the navigation is already under way\n * and the caller has nothing to do - which leaves a form that wants to know\n * whether to say \"saved\" with no way to tell a redirect from a void answer.\n * callServer notes the destination here; <Form> reads and clears it right\n * after its await. Nothing else needs to.\n */\nlet lastRedirect: string | null = null;\n\nexport function noteRedirected(location: string): void {\n lastRedirect = location;\n}\n\n/** The redirect the action just performed, if it did - read once. */\nexport function redirectedTo(): string | null {\n const location = lastRedirect;\n\n lastRedirect = null;\n\n return location;\n}\n\nexport class ServerRedirectError extends Error {\n public readonly location: string;\n\n constructor(location: string) {\n super(`Server action redirected to ${location}`);\n this.name = \"ServerRedirectError\";\n this.location = location;\n }\n}\n\nexport class ServerAuthenticationError extends Error {\n constructor(message: string = \"Unauthenticated.\") {\n super(message);\n this.name = \"ServerAuthenticationError\";\n }\n}\n\nexport class ServerAuthorizationError extends Error {\n constructor(message: string = \"This action is unauthorized.\") {\n super(message);\n this.name = \"ServerAuthorizationError\";\n }\n}\n\nexport class ServerDumpError extends Error {\n constructor() {\n super(\"Server returned a dump response.\");\n this.name = \"ServerDumpError\";\n }\n}\n\nexport class ServerSessionExpiredError extends Error {\n constructor(message: string = \"Your session has expired. Please refresh the page.\") {\n super(message);\n this.name = \"ServerSessionExpiredError\";\n }\n}\n\n/**\n * Turn a failed server-action response into the error it describes.\n *\n * A server action that does not succeed answers with JSON or a redirect\n * header rather than a Flight stream. Passing one of those to the Flight\n * decoder does not produce the server's message — it produces an internal\n * parser failure (\"enqueueModel is not a function\") or a truncated read\n * (\"Connection closed.\"), which is what reached onError before this existed.\n *\n * Returns without throwing when the response is a stream to be decoded.\n */\nexport async function throwForFailedAction(response: Response): Promise<void> {\n // Before the status: a redirect the action asked for is a 204 with the\n // destination in this header - an ok answer with no Flight in it - and\n // an expired session's is a 401. Read first, the header decides for both.\n // Read after `ok`, the 204 fell through to the Flight decoder with an\n // empty body, and the form waited forever.\n const location = response.headers.get(\"X-RSC-Redirect\");\n\n if (location !== null && location !== \"\") {\n throw new ServerRedirectError(location);\n }\n\n if (response.ok) return;\n\n if (response.status === 422) {\n const payload = (await response.json().catch(() => null)) as\n | { message?: string; errors?: Record<string, string[]> }\n | null;\n\n throw new ServerValidationError(\n payload?.message ?? \"Validation failed\",\n payload?.errors ?? {},\n );\n }\n\n if (response.status === 413) {\n throw new Error(\n \"Server action refused: the request body is larger than the server accepts. \" +\n \"A file upload is the usual cause; the limit is the host's maxActionBody.\",\n );\n }\n\n throw new Error(`Server action failed with ${response.status}`);\n}\n\n/**\n * Reject a payload response that is not one.\n *\n * The page a PPR route serves is a static shell: real HTML, status 200, with\n * its Suspense fallbacks showing. Everything below them arrives in a second\n * request. If that request fails there is nothing on screen to say so — the\n * skeletons simply stay, for ever — and handing the failure body to the Flight\n * decoder reports the decoder's confusion rather than the status.\n */\nexport function throwForFailedPayload(response: Response): void {\n if (response.ok) return;\n\n // A 404 that is a payload is the not-found page, rendered: a url a\n // pattern's shell answered with a 200 and a page that, run for real,\n // found no row - a subcategory under the wrong category. The page is\n // what the visitor should see; refusing it left the shell's content on\n // screen with nothing hydrated behind it, and every tap dead.\n if (isPayload(response)) return;\n\n throw new Error(`RSC payload request failed with ${response.status}`);\n}\n\n/** Whether a response is a Flight payload, whatever its status. */\nexport function isPayload(response: Response): boolean {\n return (response.headers.get(\"Content-Type\") ?? \"\").includes(\"text/x-component\");\n}\n\n/**\n * Announce a failure that nothing else will.\n *\n * Dispatched as well as logged: an app that wants to replace a stuck skeleton\n * with something honest has no other way to find out.\n */\nexport function reportClientFailure(scope: string, error: unknown): void {\n if (typeof window !== \"undefined\") {\n window.dispatchEvent(new CustomEvent(\"rsc-client-error\", { detail: { scope, error } }));\n }\n\n console.error(`[rsc-kit] ${scope}`, error);\n}\n"]}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pictures a prefetched page shows, fetched before the click.
|
|
3
|
+
*
|
|
4
|
+
* A payload names them: every <img> the page renders is a row in it, src
|
|
5
|
+
* and srcSet included. The page is rendered hidden on touch, and a hidden
|
|
6
|
+
* page's images do load then - but a touch leads its click by a few hundred
|
|
7
|
+
* milliseconds, and a product picture on a phone's connection takes
|
|
8
|
+
* longer. The original of a port fetched the next page's image list from a
|
|
9
|
+
* route as each link came into view; here the list is already in the
|
|
10
|
+
* payload the router holds, so the images are asked for as it lands, at low
|
|
11
|
+
* priority, and the click finds them decoded.
|
|
12
|
+
*
|
|
13
|
+
* Only the ones the page would load at once: an <img loading="lazy"> waits
|
|
14
|
+
* for the viewport on the page too. Bounded per payload, and each url once
|
|
15
|
+
* per document; nothing under Save-Data.
|
|
16
|
+
*/
|
|
17
|
+
export type Priority = 'low' | 'high';
|
|
18
|
+
export interface ImageProps {
|
|
19
|
+
src?: string;
|
|
20
|
+
srcSet?: string;
|
|
21
|
+
sizes?: string;
|
|
22
|
+
loading?: string;
|
|
23
|
+
alt?: string;
|
|
24
|
+
}
|
|
25
|
+
/** The props of every <img> element row in a flight payload, in order. */
|
|
26
|
+
export declare function imagesIn(payload: string): ImageProps[];
|
|
27
|
+
/**
|
|
28
|
+
* Ask the browser for the eager images a payload names.
|
|
29
|
+
*
|
|
30
|
+
* Low as the payload lands on sight, behind everything the page itself is
|
|
31
|
+
* loading. High on intent - the touch, the settled hover - when this is the
|
|
32
|
+
* page about to show: a listing's two dozen tiles each preload their
|
|
33
|
+
* product's picture, and the one tapped first was queued behind the rest,
|
|
34
|
+
* for three hundred milliseconds on a phone; asked again as urgent, the
|
|
35
|
+
* browser moves it to the front, ahead of the pictures nobody touched.
|
|
36
|
+
*/
|
|
37
|
+
export declare function preloadImages(payload: string, priority?: Priority): number;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pictures a prefetched page shows, fetched before the click.
|
|
3
|
+
*
|
|
4
|
+
* A payload names them: every <img> the page renders is a row in it, src
|
|
5
|
+
* and srcSet included. The page is rendered hidden on touch, and a hidden
|
|
6
|
+
* page's images do load then - but a touch leads its click by a few hundred
|
|
7
|
+
* milliseconds, and a product picture on a phone's connection takes
|
|
8
|
+
* longer. The original of a port fetched the next page's image list from a
|
|
9
|
+
* route as each link came into view; here the list is already in the
|
|
10
|
+
* payload the router holds, so the images are asked for as it lands, at low
|
|
11
|
+
* priority, and the click finds them decoded.
|
|
12
|
+
*
|
|
13
|
+
* Only the ones the page would load at once: an <img loading="lazy"> waits
|
|
14
|
+
* for the viewport on the page too. Bounded per payload, and each url once
|
|
15
|
+
* per document; nothing under Save-Data.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Per payload, the first few: the pictures a page shows first are the ones
|
|
19
|
+
* at the top of its payload - a product's own picture before its related
|
|
20
|
+
* ones. Twenty-four a page, times the dozen pages a listing prefetches on
|
|
21
|
+
* sight, was three hundred requests on a phone's radio the moment the home
|
|
22
|
+
* page settled, and the next tap's payload queued behind them: "clicking a
|
|
23
|
+
* category breaks navigation for a few seconds".
|
|
24
|
+
*/
|
|
25
|
+
const PER_PAGE = 6;
|
|
26
|
+
/** Across the document: a home page with five hundred links in view is not five hundred pages of pictures. */
|
|
27
|
+
const PER_DOCUMENT = 96;
|
|
28
|
+
/** Each picture asked for, and how urgently. */
|
|
29
|
+
const asked = new Map();
|
|
30
|
+
/** The props of every <img> element row in a flight payload, in order. */
|
|
31
|
+
export function imagesIn(payload) {
|
|
32
|
+
const found = [];
|
|
33
|
+
let at = 0;
|
|
34
|
+
while (found.length < PER_PAGE) {
|
|
35
|
+
const start = payload.indexOf('["$","img",', at);
|
|
36
|
+
if (start === -1)
|
|
37
|
+
break;
|
|
38
|
+
// Past the key: `["$","img","key",{` or `["$","img",null,{`.
|
|
39
|
+
const brace = payload.indexOf('{', start);
|
|
40
|
+
if (brace === -1)
|
|
41
|
+
break;
|
|
42
|
+
const end = closingBrace(payload, brace);
|
|
43
|
+
at = end === -1 ? brace + 1 : end + 1;
|
|
44
|
+
if (end === -1)
|
|
45
|
+
continue;
|
|
46
|
+
try {
|
|
47
|
+
found.push(JSON.parse(payload.slice(brace, end + 1)));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// A props object with a reference in it that is not JSON; not a picture worth guessing at.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return found;
|
|
54
|
+
}
|
|
55
|
+
/** The index of the brace closing the object opened at `open`, honouring strings. */
|
|
56
|
+
function closingBrace(text, open) {
|
|
57
|
+
let depth = 0;
|
|
58
|
+
let inString = false;
|
|
59
|
+
for (let i = open; i < text.length; i++) {
|
|
60
|
+
const c = text[i];
|
|
61
|
+
if (inString) {
|
|
62
|
+
if (c === '\\')
|
|
63
|
+
i++;
|
|
64
|
+
else if (c === '"')
|
|
65
|
+
inString = false;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (c === '"')
|
|
69
|
+
inString = true;
|
|
70
|
+
else if (c === '{')
|
|
71
|
+
depth++;
|
|
72
|
+
else if (c === '}' && --depth === 0)
|
|
73
|
+
return i;
|
|
74
|
+
}
|
|
75
|
+
return -1;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Ask the browser for the eager images a payload names.
|
|
79
|
+
*
|
|
80
|
+
* Low as the payload lands on sight, behind everything the page itself is
|
|
81
|
+
* loading. High on intent - the touch, the settled hover - when this is the
|
|
82
|
+
* page about to show: a listing's two dozen tiles each preload their
|
|
83
|
+
* product's picture, and the one tapped first was queued behind the rest,
|
|
84
|
+
* for three hundred milliseconds on a phone; asked again as urgent, the
|
|
85
|
+
* browser moves it to the front, ahead of the pictures nobody touched.
|
|
86
|
+
*/
|
|
87
|
+
export function preloadImages(payload, priority = 'low') {
|
|
88
|
+
if (typeof document === 'undefined')
|
|
89
|
+
return 0;
|
|
90
|
+
if (navigator.connection?.saveData)
|
|
91
|
+
return 0;
|
|
92
|
+
let started = 0;
|
|
93
|
+
for (const props of imagesIn(payload)) {
|
|
94
|
+
if (asked.size >= PER_DOCUMENT && priority === 'low')
|
|
95
|
+
break;
|
|
96
|
+
if (props.loading === 'lazy' || !props.src)
|
|
97
|
+
continue;
|
|
98
|
+
const key = props.srcSet ?? props.src;
|
|
99
|
+
const before = asked.get(key);
|
|
100
|
+
if (before === 'high' || before === priority)
|
|
101
|
+
continue;
|
|
102
|
+
asked.set(key, priority);
|
|
103
|
+
const img = new Image();
|
|
104
|
+
img.decoding = 'async';
|
|
105
|
+
img.fetchPriority = priority;
|
|
106
|
+
// sizes before srcset before src: the browser chooses on assignment.
|
|
107
|
+
if (props.sizes)
|
|
108
|
+
img.sizes = props.sizes;
|
|
109
|
+
if (props.srcSet)
|
|
110
|
+
img.srcset = props.srcSet;
|
|
111
|
+
img.src = props.src;
|
|
112
|
+
started++;
|
|
113
|
+
}
|
|
114
|
+
return started;
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=imagePreload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imagePreload.js","sourceRoot":"","sources":["../../src/js/imagePreload.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The pictures a prefetched page shows, fetched before the click.\n *\n * A payload names them: every <img> the page renders is a row in it, src\n * and srcSet included. The page is rendered hidden on touch, and a hidden\n * page's images do load then - but a touch leads its click by a few hundred\n * milliseconds, and a product picture on a phone's connection takes\n * longer. The original of a port fetched the next page's image list from a\n * route as each link came into view; here the list is already in the\n * payload the router holds, so the images are asked for as it lands, at low\n * priority, and the click finds them decoded.\n *\n * Only the ones the page would load at once: an <img loading=\"lazy\"> waits\n * for the viewport on the page too. Bounded per payload, and each url once\n * per document; nothing under Save-Data.\n */\n\n/**\n * Per payload, the first few: the pictures a page shows first are the ones\n * at the top of its payload - a product's own picture before its related\n * ones. Twenty-four a page, times the dozen pages a listing prefetches on\n * sight, was three hundred requests on a phone's radio the moment the home\n * page settled, and the next tap's payload queued behind them: \"clicking a\n * category breaks navigation for a few seconds\".\n */\nconst PER_PAGE = 6\n/** Across the document: a home page with five hundred links in view is not five hundred pages of pictures. */\nconst PER_DOCUMENT = 96\n/** Each picture asked for, and how urgently. */\nconst asked = new Map<string, Priority>()\n\nexport type Priority = 'low' | 'high'\n\nexport interface ImageProps {\n src?: string\n srcSet?: string\n sizes?: string\n loading?: string\n alt?: string\n}\n\n/** The props of every <img> element row in a flight payload, in order. */\nexport function imagesIn(payload: string): ImageProps[] {\n const found: ImageProps[] = []\n let at = 0\n\n while (found.length < PER_PAGE) {\n const start = payload.indexOf('[\"$\",\"img\",', at)\n\n if (start === -1) break\n\n // Past the key: `[\"$\",\"img\",\"key\",{` or `[\"$\",\"img\",null,{`.\n const brace = payload.indexOf('{', start)\n\n if (brace === -1) break\n\n const end = closingBrace(payload, brace)\n\n at = end === -1 ? brace + 1 : end + 1\n\n if (end === -1) continue\n\n try {\n found.push(JSON.parse(payload.slice(brace, end + 1)) as ImageProps)\n } catch {\n // A props object with a reference in it that is not JSON; not a picture worth guessing at.\n }\n }\n\n return found\n}\n\n/** The index of the brace closing the object opened at `open`, honouring strings. */\nfunction closingBrace(text: string, open: number): number {\n let depth = 0\n let inString = false\n\n for (let i = open; i < text.length; i++) {\n const c = text[i]\n\n if (inString) {\n if (c === '\\\\') i++\n else if (c === '\"') inString = false\n\n continue\n }\n\n if (c === '\"') inString = true\n else if (c === '{') depth++\n else if (c === '}' && --depth === 0) return i\n }\n\n return -1\n}\n\n/**\n * Ask the browser for the eager images a payload names.\n *\n * Low as the payload lands on sight, behind everything the page itself is\n * loading. High on intent - the touch, the settled hover - when this is the\n * page about to show: a listing's two dozen tiles each preload their\n * product's picture, and the one tapped first was queued behind the rest,\n * for three hundred milliseconds on a phone; asked again as urgent, the\n * browser moves it to the front, ahead of the pictures nobody touched.\n */\nexport function preloadImages(payload: string, priority: Priority = 'low'): number {\n if (typeof document === 'undefined') return 0\n if ((navigator as { connection?: { saveData?: boolean } }).connection?.saveData) return 0\n\n let started = 0\n\n for (const props of imagesIn(payload)) {\n if (asked.size >= PER_DOCUMENT && priority === 'low') break\n if (props.loading === 'lazy' || !props.src) continue\n\n const key = props.srcSet ?? props.src\n const before = asked.get(key)\n\n if (before === 'high' || before === priority) continue\n\n asked.set(key, priority)\n\n const img = new Image()\n\n img.decoding = 'async'\n ;(img as { fetchPriority?: string }).fetchPriority = priority\n // sizes before srcset before src: the browser chooses on assignment.\n if (props.sizes) img.sizes = props.sizes\n if (props.srcSet) img.srcset = props.srcSet\n img.src = props.src\n started++\n }\n\n return started\n}\n"]}
|
package/dist/js/navigate.d.ts
CHANGED
|
@@ -37,6 +37,20 @@ export declare function setVersion(v: string): void;
|
|
|
37
37
|
export declare function setHeldLayouts(chain: string[]): void;
|
|
38
38
|
export declare function getHeldLayouts(): string[];
|
|
39
39
|
export declare function setNavigateHandler(fn: (tree: ReactNode, key: string, segmentDepth: number) => void): void;
|
|
40
|
+
export declare function setReplaceRootHandler(fn: ((tree: ReactNode) => void) | null): void;
|
|
41
|
+
export declare function setHeldHandlers(held: ((key: string, maxAge?: number) => boolean) | null, drop: (() => void) | null): void;
|
|
42
|
+
/**
|
|
43
|
+
* Everything the router holds about pages other than the one on screen,
|
|
44
|
+
* dropped: prefetched payloads, and pages kept behind this one.
|
|
45
|
+
*
|
|
46
|
+
* After an action that revalidated. The list a link prefetched before the
|
|
47
|
+
* mutation still shows the table without the new row - visit() landed on
|
|
48
|
+
* it with no request made, and a reload showed the row. What was fetched
|
|
49
|
+
* before a write is not a cache of what is true after it; the pages held
|
|
50
|
+
* for the back button are from before it too.
|
|
51
|
+
*/
|
|
52
|
+
export declare function forgetOtherPages(): void;
|
|
53
|
+
export declare function setPrerenderHandler(fn: ((tree: ReactNode, key: string, segmentDepth: number) => void) | null): void;
|
|
40
54
|
/**
|
|
41
55
|
* How the router reveals a page that is still mounted behind the current one.
|
|
42
56
|
*
|
|
@@ -88,6 +102,25 @@ export declare function navigate(url: Route, opts?: {
|
|
|
88
102
|
* is the same apply path a navigation uses — without a request, a url change
|
|
89
103
|
* or a history entry.
|
|
90
104
|
*/
|
|
105
|
+
/**
|
|
106
|
+
* Whether the page an action was invoked from is the one on screen.
|
|
107
|
+
*
|
|
108
|
+
* The one underneath, when an interception is showing: a modal opened after
|
|
109
|
+
* the submit sits over the same page.
|
|
110
|
+
*/
|
|
111
|
+
export declare function stillShowing(url: string): boolean;
|
|
112
|
+
/**
|
|
113
|
+
* Put what an action re-rendered on screen.
|
|
114
|
+
*
|
|
115
|
+
* `from` is the url the action was invoked on - what the host rendered the
|
|
116
|
+
* trees for. A tap that left the page while the action was in flight has
|
|
117
|
+
* changed what is showing, and a document rendered for the page before
|
|
118
|
+
* cannot go under the url after: "Add to cart", then the brand link at
|
|
119
|
+
* once, showed the home page and then, when the answer landed, the product
|
|
120
|
+
* again under `/`. The write still happened, and the page on screen was
|
|
121
|
+
* fetched before it - so that page is asked for again, whole, instead.
|
|
122
|
+
*/
|
|
123
|
+
export declare function applyRevalidations(from: string, revalidated: Record<string, ReactNode>): void;
|
|
91
124
|
export declare function applyRevalidated(target: string, tree: ReactNode): void;
|
|
92
125
|
/**
|
|
93
126
|
* Ask the server for part of this page again.
|
package/dist/js/navigate.js
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* The Flight deserializer is injected by createViteRscApp to avoid
|
|
6
6
|
* duplicate bundling of react-server-dom-webpack.
|
|
7
7
|
*/
|
|
8
|
-
import { isStaleAssetError, loadDocumentOnce } from "./staleAssets";
|
|
8
|
+
import { announceDocumentLoad, isStaleAssetError, loadDocumentOnce, } from "./staleAssets";
|
|
9
9
|
import { isUpdated, markStale } from "./updateStore";
|
|
10
10
|
import { navigationAbandoned, navigationCommitted, navigationReached, navigationStarted } from "./perf";
|
|
11
|
+
import { preloadImages } from "./imagePreload";
|
|
11
12
|
import { isSafeRedirect } from "../safeUrl.js";
|
|
12
13
|
import { reportReachable } from "./onlineStore";
|
|
13
14
|
import { clearSlots, setSlot } from "./slotStore";
|
|
@@ -19,6 +20,22 @@ let version = "";
|
|
|
19
20
|
const navigating = new Set();
|
|
20
21
|
let onNavigate = null;
|
|
21
22
|
let onRestore = null;
|
|
23
|
+
/**
|
|
24
|
+
* Re-render the whole document in place, for revalidate("all"): the page the
|
|
25
|
+
* visitor is on, not a page they went to. Without one registered, the
|
|
26
|
+
* navigation handler at depth 0 is used, which shows the tree as a page.
|
|
27
|
+
*/
|
|
28
|
+
let onReplaceRoot = null;
|
|
29
|
+
/** Whether a navigation to the key would reveal a held page, asked without revealing it. */
|
|
30
|
+
let isHeldPage = null;
|
|
31
|
+
/** Drop the pages held behind the one on screen - after a mutation. */
|
|
32
|
+
let dropHeld = null;
|
|
33
|
+
/**
|
|
34
|
+
* Render a decoded page in the background, hidden, before the click - see
|
|
35
|
+
* warm(). Given the same tree the navigation will hand to onNavigate, so
|
|
36
|
+
* that the navigation is a reveal of work already done rather than a render.
|
|
37
|
+
*/
|
|
38
|
+
let onPrerender = null;
|
|
22
39
|
/**
|
|
23
40
|
* How stale a held page may be and still be revealed by a link.
|
|
24
41
|
*
|
|
@@ -42,6 +59,33 @@ let interceptManifest = [];
|
|
|
42
59
|
// The layout chain currently mounted, outermost first. Sent so the server can
|
|
43
60
|
// skip re-rendering the layouts still on screen.
|
|
44
61
|
let heldLayouts = [];
|
|
62
|
+
/**
|
|
63
|
+
* The chain each page was shown under, by retention key.
|
|
64
|
+
*
|
|
65
|
+
* A held page revealed - the back button, or a link to the page just left -
|
|
66
|
+
* puts its layouts back on screen, and the chain has to say so. It used to
|
|
67
|
+
* keep the chain of the page being left: home, a category, back to home,
|
|
68
|
+
* then another category claimed the first category's layout as mounted. The
|
|
69
|
+
* server sent the page alone, and the client put it in the boundary that
|
|
70
|
+
* layout owns - inside the hidden category. The url changed and the page did
|
|
71
|
+
* not, and every tap after it did the same, until a reload. The demo froze
|
|
72
|
+
* on a phone within a dozen taps.
|
|
73
|
+
*
|
|
74
|
+
* Bounded like the payload cache: a key that is no longer held is never
|
|
75
|
+
* asked for, so the oldest can go.
|
|
76
|
+
*/
|
|
77
|
+
const chainOf = new Map();
|
|
78
|
+
const MAX_CHAINS = 64;
|
|
79
|
+
function rememberChain(key) {
|
|
80
|
+
chainOf.delete(key);
|
|
81
|
+
chainOf.set(key, heldLayouts);
|
|
82
|
+
while (chainOf.size > MAX_CHAINS) {
|
|
83
|
+
const oldest = chainOf.keys().next().value;
|
|
84
|
+
if (oldest === undefined)
|
|
85
|
+
break;
|
|
86
|
+
chainOf.delete(oldest);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
45
89
|
/**
|
|
46
90
|
* The boundary depth an interception was rendered at, while one is showing.
|
|
47
91
|
*
|
|
@@ -61,6 +105,11 @@ let interceptedAtDepth = null;
|
|
|
61
105
|
* screen with everything the user typed into it.
|
|
62
106
|
*/
|
|
63
107
|
let interceptedOver = null;
|
|
108
|
+
/**
|
|
109
|
+
* The url an interception was opened from, whichever way it was rendered -
|
|
110
|
+
* the page still on screen under the modal. Only stillShowing asks.
|
|
111
|
+
*/
|
|
112
|
+
let interceptedFrom = null;
|
|
64
113
|
const DEFAULT_PREFETCH_TTL = 30_000;
|
|
65
114
|
/**
|
|
66
115
|
* How long a payload the host marked `public` is kept.
|
|
@@ -130,6 +179,7 @@ export function seedStaticChain(url) {
|
|
|
130
179
|
if (!segments)
|
|
131
180
|
return false;
|
|
132
181
|
heldLayouts = segments.chain;
|
|
182
|
+
rememberChain(retentionKey(url, null));
|
|
133
183
|
return true;
|
|
134
184
|
}
|
|
135
185
|
/**
|
|
@@ -218,6 +268,7 @@ async function tellWorkerServerBuild(build) {
|
|
|
218
268
|
*/
|
|
219
269
|
export function setHeldLayouts(chain) {
|
|
220
270
|
heldLayouts = chain;
|
|
271
|
+
rememberChain(retentionKey(window.location.href, null));
|
|
221
272
|
}
|
|
222
273
|
export function getHeldLayouts() {
|
|
223
274
|
return heldLayouts;
|
|
@@ -225,6 +276,34 @@ export function getHeldLayouts() {
|
|
|
225
276
|
export function setNavigateHandler(fn) {
|
|
226
277
|
onNavigate = fn;
|
|
227
278
|
}
|
|
279
|
+
export function setReplaceRootHandler(fn) {
|
|
280
|
+
onReplaceRoot = fn;
|
|
281
|
+
}
|
|
282
|
+
export function setHeldHandlers(held, drop) {
|
|
283
|
+
isHeldPage = held;
|
|
284
|
+
dropHeld = drop;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Everything the router holds about pages other than the one on screen,
|
|
288
|
+
* dropped: prefetched payloads, and pages kept behind this one.
|
|
289
|
+
*
|
|
290
|
+
* After an action that revalidated. The list a link prefetched before the
|
|
291
|
+
* mutation still shows the table without the new row - visit() landed on
|
|
292
|
+
* it with no request made, and a reload showed the row. What was fetched
|
|
293
|
+
* before a write is not a cache of what is true after it; the pages held
|
|
294
|
+
* for the back button are from before it too.
|
|
295
|
+
*/
|
|
296
|
+
export function forgetOtherPages() {
|
|
297
|
+
for (const [key, controller] of prefetchControllers) {
|
|
298
|
+
controller.abort();
|
|
299
|
+
prefetchControllers.delete(key);
|
|
300
|
+
}
|
|
301
|
+
cache.clear();
|
|
302
|
+
dropHeld?.();
|
|
303
|
+
}
|
|
304
|
+
export function setPrerenderHandler(fn) {
|
|
305
|
+
onPrerender = fn;
|
|
306
|
+
}
|
|
228
307
|
/**
|
|
229
308
|
* How the router reveals a page that is still mounted behind the current one.
|
|
230
309
|
*
|
|
@@ -491,6 +570,7 @@ export async function navigate(url, opts) {
|
|
|
491
570
|
// advice. Not for a restore: going back to a page still held asks the
|
|
492
571
|
// server for nothing.
|
|
493
572
|
if (!opts?.restore && isUpdated()) {
|
|
573
|
+
announceDocumentLoad(url, "newer-build");
|
|
494
574
|
window.location.href = url;
|
|
495
575
|
return;
|
|
496
576
|
}
|
|
@@ -499,12 +579,14 @@ export async function navigate(url, opts) {
|
|
|
499
579
|
}
|
|
500
580
|
// External URLs can't be fetched (CORS) — go directly to full page navigation
|
|
501
581
|
if (isExternalUrl(url)) {
|
|
582
|
+
announceDocumentLoad(url, "external");
|
|
502
583
|
window.location.href = url;
|
|
503
584
|
return;
|
|
504
585
|
}
|
|
505
586
|
// A route.ts answers with a Response, not a page: a download, a redirect
|
|
506
587
|
// that decides where someone belongs, a sign-out. The browser goes there.
|
|
507
588
|
if (isApiRoute(url)) {
|
|
589
|
+
announceDocumentLoad(url, "api-route");
|
|
508
590
|
window.location.href = url;
|
|
509
591
|
return;
|
|
510
592
|
}
|
|
@@ -550,6 +632,7 @@ export async function navigate(url, opts) {
|
|
|
550
632
|
retentionKey(url, null) === retentionKey(interceptedOver, null)) {
|
|
551
633
|
clearSlots();
|
|
552
634
|
interceptedOver = null;
|
|
635
|
+
interceptedFrom = null;
|
|
553
636
|
interceptedAtDepth = null;
|
|
554
637
|
if (opts?.replace) {
|
|
555
638
|
history.replaceState({ rscUrl: url }, "", url);
|
|
@@ -581,8 +664,14 @@ export async function navigate(url, opts) {
|
|
|
581
664
|
onRestore?.(activityKey, opts?.restore ? undefined : revealWithin)) {
|
|
582
665
|
// A restored tree carries its own slot contents, so the flag only has to
|
|
583
666
|
// reflect whether what is now showing is an intercepted view.
|
|
584
|
-
if (!interceptSlot)
|
|
667
|
+
if (!interceptSlot) {
|
|
585
668
|
interceptedAtDepth = null;
|
|
669
|
+
interceptedFrom = null;
|
|
670
|
+
}
|
|
671
|
+
// Its layouts are the ones on screen now - see chainOf.
|
|
672
|
+
const chain = chainOf.get(activityKey);
|
|
673
|
+
if (chain)
|
|
674
|
+
heldLayouts = chain;
|
|
586
675
|
// opts?.replace, not opts.replace: this branch used to be reachable only
|
|
587
676
|
// with opts.restore set, so opts was always there. A link reaches it now
|
|
588
677
|
// with nothing passed at all.
|
|
@@ -665,6 +754,7 @@ export async function navigate(url, opts) {
|
|
|
665
754
|
const contentType = response.headers.get("Content-Type") ?? "";
|
|
666
755
|
if (staticPayloadSuffix === null &&
|
|
667
756
|
!contentType.includes("text/x-component")) {
|
|
757
|
+
announceDocumentLoad(url, `not-a-payload:${response.status}:${contentType.split(";")[0]}`);
|
|
668
758
|
window.location.href = url;
|
|
669
759
|
return;
|
|
670
760
|
}
|
|
@@ -729,6 +819,7 @@ export async function navigate(url, opts) {
|
|
|
729
819
|
}
|
|
730
820
|
if (nextLayouts !== null)
|
|
731
821
|
heldLayouts = nextLayouts;
|
|
822
|
+
rememberChain(activityKey);
|
|
732
823
|
// The answer is one region, not a piece of the page: the host rendered
|
|
733
824
|
// only the interceptor because the page underneath is already mounted and
|
|
734
825
|
// still correct. Putting it in the slot leaves that page — and everything
|
|
@@ -736,6 +827,7 @@ export async function navigate(url, opts) {
|
|
|
736
827
|
if (slotPayload !== null) {
|
|
737
828
|
setSlot(slotPayload, tree);
|
|
738
829
|
interceptedOver = interceptedOver ?? previousUrl;
|
|
830
|
+
interceptedFrom = interceptedFrom ?? previousUrl;
|
|
739
831
|
interceptedAtDepth = null;
|
|
740
832
|
return;
|
|
741
833
|
}
|
|
@@ -744,6 +836,7 @@ export async function navigate(url, opts) {
|
|
|
744
836
|
clearSlots();
|
|
745
837
|
interceptedOver = null;
|
|
746
838
|
interceptedAtDepth = interceptSlot ? segmentDepth : null;
|
|
839
|
+
interceptedFrom = interceptSlot ? (interceptedFrom ?? previousUrl) : null;
|
|
747
840
|
navigationReached("applied");
|
|
748
841
|
onNavigate?.(tree, activityKey, segmentDepth);
|
|
749
842
|
if (!opts?.preserveScroll && !interceptSlot) {
|
|
@@ -762,6 +855,7 @@ export async function navigate(url, opts) {
|
|
|
762
855
|
// A chunk the deploy no longer serves: the browser would have loaded the
|
|
763
856
|
// document, and the new names with it. Do what it would have done.
|
|
764
857
|
if (isStaleAssetError(err)) {
|
|
858
|
+
announceDocumentLoad(url, `stale-asset:${String(err?.message ?? err).slice(0, 120)}`);
|
|
765
859
|
window.location.href = url;
|
|
766
860
|
return;
|
|
767
861
|
}
|
|
@@ -785,12 +879,51 @@ export async function navigate(url, opts) {
|
|
|
785
879
|
* is the same apply path a navigation uses — without a request, a url change
|
|
786
880
|
* or a history entry.
|
|
787
881
|
*/
|
|
882
|
+
/**
|
|
883
|
+
* Whether the page an action was invoked from is the one on screen.
|
|
884
|
+
*
|
|
885
|
+
* The one underneath, when an interception is showing: a modal opened after
|
|
886
|
+
* the submit sits over the same page.
|
|
887
|
+
*/
|
|
888
|
+
export function stillShowing(url) {
|
|
889
|
+
const key = retentionKey(url, null);
|
|
890
|
+
const now = retentionKey(window.location.pathname + window.location.search, null);
|
|
891
|
+
return key === now || (interceptedFrom !== null && key === retentionKey(interceptedFrom, null));
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* Put what an action re-rendered on screen.
|
|
895
|
+
*
|
|
896
|
+
* `from` is the url the action was invoked on - what the host rendered the
|
|
897
|
+
* trees for. A tap that left the page while the action was in flight has
|
|
898
|
+
* changed what is showing, and a document rendered for the page before
|
|
899
|
+
* cannot go under the url after: "Add to cart", then the brand link at
|
|
900
|
+
* once, showed the home page and then, when the answer landed, the product
|
|
901
|
+
* again under `/`. The write still happened, and the page on screen was
|
|
902
|
+
* fetched before it - so that page is asked for again, whole, instead.
|
|
903
|
+
*/
|
|
904
|
+
export function applyRevalidations(from, revalidated) {
|
|
905
|
+
// What was fetched or held before the write is from before it.
|
|
906
|
+
forgetOtherPages();
|
|
907
|
+
if (!stillShowing(from)) {
|
|
908
|
+
void refresh("all");
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
for (const [target, tree] of Object.entries(revalidated)) {
|
|
912
|
+
applyRevalidated(target, tree);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
788
915
|
export function applyRevalidated(target, tree) {
|
|
789
916
|
const url = window.location.pathname + window.location.search;
|
|
790
917
|
const key = retentionKey(url, null);
|
|
791
918
|
if (target === "all") {
|
|
792
|
-
//
|
|
793
|
-
|
|
919
|
+
// The whole document again, in place. Not a navigation to this url:
|
|
920
|
+
// the visible entry may be keyed by the url the document loaded with,
|
|
921
|
+
// and showing the tree under the current one made a second entry and
|
|
922
|
+
// remounted the app under it.
|
|
923
|
+
if (onReplaceRoot)
|
|
924
|
+
onReplaceRoot(tree);
|
|
925
|
+
else
|
|
926
|
+
onNavigate?.(tree, key, 0);
|
|
794
927
|
return;
|
|
795
928
|
}
|
|
796
929
|
if (target === "page") {
|
|
@@ -818,6 +951,9 @@ export function applyRevalidated(target, tree) {
|
|
|
818
951
|
*/
|
|
819
952
|
export async function refresh(target = "page") {
|
|
820
953
|
const url = window.location.pathname + window.location.search;
|
|
954
|
+
// Asked because the data moved on; what was fetched or held before is
|
|
955
|
+
// from before.
|
|
956
|
+
forgetOtherPages();
|
|
821
957
|
if (target !== "page" && target !== "all") {
|
|
822
958
|
const response = await fetch(payloadUrl(url), {
|
|
823
959
|
headers: {
|
|
@@ -941,6 +1077,11 @@ export function prefetch(url, cacheForMs, intent = false) {
|
|
|
941
1077
|
// in view, was a 14 KB payload for the page already on screen.
|
|
942
1078
|
if (retentionKey(url, null) === retentionKey(window.location.href, null))
|
|
943
1079
|
return;
|
|
1080
|
+
// Nor a page still held behind this one, which a navigation would reveal
|
|
1081
|
+
// rather than fetch: the page just left, whose link is on every page,
|
|
1082
|
+
// was one wasted payload per navigation.
|
|
1083
|
+
if (!matchIntercept(url) && isHeldPage?.(retentionKey(url, null), revealWithin))
|
|
1084
|
+
return;
|
|
944
1085
|
// Nor anything, once this page is known to be the previous build: the
|
|
945
1086
|
// next navigation is a document load, and a payload it would not use is
|
|
946
1087
|
// a 409 for nothing - one per tap, measured.
|
|
@@ -958,15 +1099,57 @@ export function prefetch(url, cacheForMs, intent = false) {
|
|
|
958
1099
|
prefetchUrl(cacheKey, url, ttl, interceptSlot, currentUrl, held);
|
|
959
1100
|
}
|
|
960
1101
|
else {
|
|
961
|
-
prefetchUrl(url, url, ttl, undefined, undefined, held);
|
|
1102
|
+
prefetchUrl(retentionKeyFor(url, null), url, ttl, undefined, undefined, held);
|
|
962
1103
|
}
|
|
963
1104
|
}
|
|
964
|
-
/**
|
|
965
|
-
|
|
1105
|
+
/**
|
|
1106
|
+
* Do the click's work now: decode the held payload, which loads the chunks
|
|
1107
|
+
* it names, and render the page hidden so the click is a reveal.
|
|
1108
|
+
*
|
|
1109
|
+
* The numbers this came from, on an iPhone with everything prefetched: a
|
|
1110
|
+
* first visit to the landing page was 107 ms from tap to paint, 4 of them
|
|
1111
|
+
* decoding and 87 rendering; a page still held from a visit before was
|
|
1112
|
+
* 15 ms, a reveal. The render is the page's size on the phone's CPU, and
|
|
1113
|
+
* nothing in the payload's path shrinks it - but it can be paid before the
|
|
1114
|
+
* finger lifts. A touch leads its click by 100-300 ms; a settled hover, by
|
|
1115
|
+
* 200 or more. React renders a hidden Activity at idle priority, so the
|
|
1116
|
+
* work yields to scrolling, and the navigation then hands the boundary the
|
|
1117
|
+
* same tree it prerendered: React bails out of the whole subtree and flips
|
|
1118
|
+
* it visible.
|
|
1119
|
+
*
|
|
1120
|
+
* Only a segment at depth 1 or more, rendered against the chain held now: a
|
|
1121
|
+
* whole document replaces the root, and a slot is a region of a page not on
|
|
1122
|
+
* screen yet. A prefetch that landed on a redirect - a guarded link, on a
|
|
1123
|
+
* landing page all of them - warms the destination it prefetched instead:
|
|
1124
|
+
* the tap on Sign in decoded the login page and loaded its chunks after the
|
|
1125
|
+
* click, 154 ms of a 225 ms tap.
|
|
1126
|
+
*/
|
|
1127
|
+
function warm(entry, cacheKey) {
|
|
966
1128
|
// The tree is read by the navigation that takes the entry, and a decode
|
|
967
1129
|
// that fails - a chunk the deploy no longer serves - fails there, where it
|
|
968
1130
|
// is handled. Here the rejection has nobody to reach.
|
|
969
|
-
entry.
|
|
1131
|
+
entry.body
|
|
1132
|
+
.then((text) => {
|
|
1133
|
+
if (entry.redirectTo) {
|
|
1134
|
+
const destination = cache.get(retentionKeyFor(entry.redirectTo, matchIntercept(entry.redirectTo)));
|
|
1135
|
+
if (destination)
|
|
1136
|
+
warm(destination, retentionKeyFor(entry.redirectTo, matchIntercept(entry.redirectTo)));
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
if (text === null)
|
|
1140
|
+
return;
|
|
1141
|
+
// The page about to show: its pictures ahead of every other page's.
|
|
1142
|
+
preloadImages(text, "high");
|
|
1143
|
+
return entry.tree.then((tree) => {
|
|
1144
|
+
if (entry.slot !== null ||
|
|
1145
|
+
entry.segmentDepth === 0 ||
|
|
1146
|
+
!isUsable(entry, claimedChain(null)) ||
|
|
1147
|
+
cache.get(cacheKey) !== entry)
|
|
1148
|
+
return;
|
|
1149
|
+
onPrerender?.(tree, cacheKey, entry.segmentDepth);
|
|
1150
|
+
});
|
|
1151
|
+
})
|
|
1152
|
+
.catch(() => { });
|
|
970
1153
|
}
|
|
971
1154
|
function prefetchUrl(cacheKey, url, ttl, interceptSlot, refererUrl, held = { intent: false, explicitTtl: false }) {
|
|
972
1155
|
const chain = claimedChain(interceptSlot ?? null);
|
|
@@ -974,7 +1157,7 @@ function prefetchUrl(cacheKey, url, ttl, interceptSlot, refererUrl, held = { int
|
|
|
974
1157
|
if (existing && existing.expiresAt > Date.now()) {
|
|
975
1158
|
// Fetched as it came into view, undecoded; the touch says decode it.
|
|
976
1159
|
if (held.intent)
|
|
977
|
-
warm(existing);
|
|
1160
|
+
warm(existing, cacheKey);
|
|
978
1161
|
return;
|
|
979
1162
|
}
|
|
980
1163
|
// A navigation is already fetching it; the answer is on its way.
|
|
@@ -1043,8 +1226,13 @@ function prefetchUrl(cacheKey, url, ttl, interceptSlot, refererUrl, held = { int
|
|
|
1043
1226
|
entry.expiresAt = Math.max(entry.expiresAt, Date.now() + STATIC_PREFETCH_TTL);
|
|
1044
1227
|
}
|
|
1045
1228
|
// The bytes, not the page: decoding is the navigation's, see CacheEntry
|
|
1046
|
-
// - unless the visitor is already on the way, see prefetch().
|
|
1047
|
-
|
|
1229
|
+
// - unless the visitor is already on the way, see prefetch(). The
|
|
1230
|
+
// pictures the page shows are asked for now, though: on a phone they
|
|
1231
|
+
// take longer than the touch-to-click a hidden render has.
|
|
1232
|
+
return response.text().then((text) => {
|
|
1233
|
+
preloadImages(text);
|
|
1234
|
+
return text;
|
|
1235
|
+
});
|
|
1048
1236
|
})
|
|
1049
1237
|
.catch(() => {
|
|
1050
1238
|
entry.failed = true;
|
|
@@ -1060,7 +1248,7 @@ function prefetchUrl(cacheKey, url, ttl, interceptSlot, refererUrl, held = { int
|
|
|
1060
1248
|
});
|
|
1061
1249
|
cache.set(cacheKey, entry);
|
|
1062
1250
|
if (held.intent)
|
|
1063
|
-
warm(entry);
|
|
1251
|
+
warm(entry, cacheKey);
|
|
1064
1252
|
}
|
|
1065
1253
|
/**
|
|
1066
1254
|
* Drop a prefetch that is still in flight — the pointer left the link.
|