@rsc-kit/core 0.20.7 → 0.20.10
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/earlyHints.d.ts +14 -2
- package/dist/earlyHints.js +15 -8
- package/dist/earlyHints.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 +38 -8
- 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 +58 -0
- package/dist/js/segmentStore.js +134 -4
- 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/segmentStore.js
CHANGED
|
@@ -37,6 +37,10 @@
|
|
|
37
37
|
/** Pages kept alive per boundary. Four covers ordinary back-and-forth. */
|
|
38
38
|
export const RETENTION = 4;
|
|
39
39
|
const depths = new Map();
|
|
40
|
+
/** A counter every entry takes a number from, in order. */
|
|
41
|
+
let tick = 0;
|
|
42
|
+
/** The number the last mutation took: everything held before it is wrong. */
|
|
43
|
+
let invalidatedAt = 0;
|
|
40
44
|
const listeners = new Map();
|
|
41
45
|
function notify(depth) {
|
|
42
46
|
for (const listener of listeners.get(depth) ?? [])
|
|
@@ -63,7 +67,7 @@ export function getSegmentState(depth) {
|
|
|
63
67
|
function retain(entries, order, activeKey) {
|
|
64
68
|
const kept = order.slice(-RETENTION);
|
|
65
69
|
return {
|
|
66
|
-
entries: entries.filter((entry) => kept.includes(entry.key)),
|
|
70
|
+
entries: entries.filter((entry) => kept.includes(entry.key) || entry.speculative),
|
|
67
71
|
order: kept,
|
|
68
72
|
activeKey,
|
|
69
73
|
};
|
|
@@ -72,7 +76,7 @@ function put(depth, key, tree) {
|
|
|
72
76
|
const state = depths.get(depth);
|
|
73
77
|
const entries = [
|
|
74
78
|
...(state?.entries ?? []).filter((entry) => entry.key !== key),
|
|
75
|
-
{ key, tree, at: Date.now() },
|
|
79
|
+
{ key, tree, at: Date.now(), seq: ++tick },
|
|
76
80
|
];
|
|
77
81
|
const order = [...(state?.order ?? []).filter((k) => k !== key), key];
|
|
78
82
|
depths.set(depth, retain(entries, order, key));
|
|
@@ -84,6 +88,11 @@ function put(depth, key, tree) {
|
|
|
84
88
|
* render the previous page inside the new one.
|
|
85
89
|
*/
|
|
86
90
|
export function setSegment(depth, key, tree) {
|
|
91
|
+
// A guess about another page was wrong; the one about this page, if there
|
|
92
|
+
// was one, is replaced by put() below - with the same tree, when the
|
|
93
|
+
// prerender and the navigation read the same decoded payload, which is
|
|
94
|
+
// what makes the update a reveal rather than a render.
|
|
95
|
+
dropSpeculative(depth, key);
|
|
87
96
|
put(depth, key, tree);
|
|
88
97
|
const stale = [...depths.keys()].filter((d) => d > depth);
|
|
89
98
|
for (const d of stale)
|
|
@@ -92,6 +101,81 @@ export function setSegment(depth, key, tree) {
|
|
|
92
101
|
for (const d of stale)
|
|
93
102
|
notify(d);
|
|
94
103
|
}
|
|
104
|
+
function dropSpeculative(depth, except) {
|
|
105
|
+
const state = depths.get(depth);
|
|
106
|
+
if (!state?.entries.some((entry) => entry.speculative && entry.key !== except))
|
|
107
|
+
return;
|
|
108
|
+
depths.set(depth, {
|
|
109
|
+
...state,
|
|
110
|
+
entries: state.entries.filter((entry) => !entry.speculative || entry.key === except),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Render a page hidden at `depth`, before any navigation to it.
|
|
115
|
+
*
|
|
116
|
+
* The click then finds the work done: setSegment with the same tree is a
|
|
117
|
+
* bail-out for React - same element, same props - and the Activity flips
|
|
118
|
+
* from hidden to visible. What was 87 ms of rendering on a phone, after
|
|
119
|
+
* the tap, is paid before the finger lifts, at idle priority, yielding to
|
|
120
|
+
* the scroll. Never changes what is showing, never counts against
|
|
121
|
+
* retention, and a page already held needs nothing.
|
|
122
|
+
*/
|
|
123
|
+
export function prerenderSegment(depth, key, tree) {
|
|
124
|
+
const state = depths.get(depth);
|
|
125
|
+
// Nothing at this depth yet: the boundary is still showing the server's
|
|
126
|
+
// children, and it has no page key to keep them under. A guess would take
|
|
127
|
+
// the store over with no active entry to show. seedSegment runs on mount,
|
|
128
|
+
// so this is the gap between hydration and that effect; the click will
|
|
129
|
+
// render.
|
|
130
|
+
if (!state)
|
|
131
|
+
return;
|
|
132
|
+
if (state.entries.some((entry) => entry.key === key))
|
|
133
|
+
return;
|
|
134
|
+
depths.set(depth, {
|
|
135
|
+
...state,
|
|
136
|
+
entries: [
|
|
137
|
+
...state.entries.filter((entry) => !entry.speculative),
|
|
138
|
+
{ key, tree, at: Date.now(), seq: ++tick, speculative: true },
|
|
139
|
+
],
|
|
140
|
+
});
|
|
141
|
+
notify(depth);
|
|
142
|
+
}
|
|
143
|
+
/** Whether a page is rendered hidden at `depth`, ahead of a navigation to it. */
|
|
144
|
+
export function isPrerendered(depth, key) {
|
|
145
|
+
return depths.get(depth)?.entries.some((entry) => entry.key === key && entry.speculative) ?? false;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Give the page on screen at `depth` a new tree, under the key it has.
|
|
149
|
+
*
|
|
150
|
+
* For revalidate("all"): the whole document rendered again, in place. The
|
|
151
|
+
* root hands its layout the new tree, the layout hands its boundary new
|
|
152
|
+
* children, and a boundary that holds state would otherwise keep showing
|
|
153
|
+
* the store's tree and drop the new one on the floor - or, cleared first,
|
|
154
|
+
* re-key its Activity to the current url and remount everything under it.
|
|
155
|
+
* Neither. The entry that is showing takes the new tree and keeps its key,
|
|
156
|
+
* so React reconciles the page in place, and the boundary below gets its
|
|
157
|
+
* new children the same way.
|
|
158
|
+
*/
|
|
159
|
+
export function replaceActive(depth, tree) {
|
|
160
|
+
const state = depths.get(depth);
|
|
161
|
+
if (!state)
|
|
162
|
+
return;
|
|
163
|
+
const active = state.entries.find((entry) => entry.key === state.activeKey);
|
|
164
|
+
if (!active || active.tree === tree)
|
|
165
|
+
return;
|
|
166
|
+
// The tree changes; when the entry arrived does not. A layout's entry is
|
|
167
|
+
// keyed by the page it was seeded with - "/" for a document loaded at
|
|
168
|
+
// home - and stays active through every navigation below it. Stamped
|
|
169
|
+
// with the re-render's time it read as held since the mutation, and a
|
|
170
|
+
// tap on the brand link after "Add to cart" revealed it: the url went
|
|
171
|
+
// to "/" and the product stayed, because the tree under that key was
|
|
172
|
+
// now the product's document. See dropHidden and isHeld.
|
|
173
|
+
depths.set(depth, {
|
|
174
|
+
...state,
|
|
175
|
+
entries: state.entries.map((entry) => (entry === active ? { ...entry, tree } : entry)),
|
|
176
|
+
});
|
|
177
|
+
notify(depth);
|
|
178
|
+
}
|
|
95
179
|
/**
|
|
96
180
|
* Record the children the server rendered, so the page you arrived on can be
|
|
97
181
|
* returned to later. Never changes what is showing.
|
|
@@ -107,7 +191,7 @@ export function seedSegment(depth, key, tree) {
|
|
|
107
191
|
}
|
|
108
192
|
// Older than whatever is showing, so it goes to the front of the eviction
|
|
109
193
|
// order — and crucially does not become the active page.
|
|
110
|
-
depths.set(depth, retain([...state.entries, { key, tree, at: Date.now() }], [key, ...state.order.filter((k) => k !== key)], state.activeKey));
|
|
194
|
+
depths.set(depth, retain([...state.entries, { key, tree, at: Date.now(), seq: ++tick }], [key, ...state.order.filter((k) => k !== key)], state.activeKey));
|
|
111
195
|
notify(depth);
|
|
112
196
|
}
|
|
113
197
|
/**
|
|
@@ -123,6 +207,47 @@ export function seedSegment(depth, key, tree) {
|
|
|
123
207
|
* deeper boundary, so they delegate to whatever it is showing. One that does
|
|
124
208
|
* hold the key is switched to it, since that is a real change at its level.
|
|
125
209
|
*/
|
|
210
|
+
/**
|
|
211
|
+
* Whether a link to `key` would be answered by revealing a held page.
|
|
212
|
+
*
|
|
213
|
+
* The question restoreSegments answers, asked without acting on it: a
|
|
214
|
+
* prefetch of a page the boundaries still hold is a request for nothing -
|
|
215
|
+
* a navigation would reveal it. The page just left is the usual case, one
|
|
216
|
+
* wasted payload per navigation.
|
|
217
|
+
*/
|
|
218
|
+
export function isHeld(key, maxAge) {
|
|
219
|
+
const ages = [...depths.values()].flatMap((state) => state.entries.filter((entry) => entry.key === key && !entry.speculative && entry.seq > invalidatedAt).map((entry) => entry.at));
|
|
220
|
+
if (ages.length === 0)
|
|
221
|
+
return false;
|
|
222
|
+
if (maxAge === undefined)
|
|
223
|
+
return true;
|
|
224
|
+
return Date.now() - Math.min(...ages) < maxAge;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Drop every page held behind the one on screen.
|
|
228
|
+
*
|
|
229
|
+
* After a mutation: a page kept for the back button holds the data from
|
|
230
|
+
* before it, and revealing it would show a row that is gone, a name that
|
|
231
|
+
* changed. The page on screen stays - it was re-rendered by the action, or
|
|
232
|
+
* is about to be.
|
|
233
|
+
*/
|
|
234
|
+
export function dropHidden() {
|
|
235
|
+
// And the ones that stay - the active entry at each depth - are from
|
|
236
|
+
// before it too. A layout's entry is keyed by the page it was seeded
|
|
237
|
+
// with, and revealing that key later shows the page inside it as it was
|
|
238
|
+
// then. Nothing seeded before this moment is revealed again.
|
|
239
|
+
invalidatedAt = ++tick;
|
|
240
|
+
for (const [depth, state] of depths) {
|
|
241
|
+
if (state.entries.length <= 1 && !state.entries.some((entry) => entry.speculative))
|
|
242
|
+
continue;
|
|
243
|
+
depths.set(depth, {
|
|
244
|
+
entries: state.entries.filter((entry) => entry.key === state.activeKey),
|
|
245
|
+
order: state.order.filter((key) => key === state.activeKey),
|
|
246
|
+
activeKey: state.activeKey,
|
|
247
|
+
});
|
|
248
|
+
notify(depth);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
126
251
|
/**
|
|
127
252
|
* Reveal a page still being held, if it is worth revealing.
|
|
128
253
|
*
|
|
@@ -134,7 +259,11 @@ export function seedSegment(depth, key, tree) {
|
|
|
134
259
|
* just on, with the form you were filling in still filled in.
|
|
135
260
|
*/
|
|
136
261
|
export function restoreSegments(key, maxAge) {
|
|
137
|
-
|
|
262
|
+
// A guess is not a held page: revealing it would be showing prefetched
|
|
263
|
+
// data as the page the visitor was on. The navigation takes the
|
|
264
|
+
// prerendered tree through setSegment instead, where it is a reveal too.
|
|
265
|
+
// Nor is a page from before a mutation - see dropHidden.
|
|
266
|
+
const holding = [...depths.keys()].filter((d) => depths.get(d).entries.some((entry) => entry.key === key && !entry.speculative && entry.seq > invalidatedAt));
|
|
138
267
|
if (holding.length === 0)
|
|
139
268
|
return false;
|
|
140
269
|
if (maxAge !== undefined) {
|
|
@@ -155,6 +284,7 @@ export function restoreSegments(key, maxAge) {
|
|
|
155
284
|
notify(d);
|
|
156
285
|
}
|
|
157
286
|
for (const d of holding) {
|
|
287
|
+
dropSpeculative(d);
|
|
158
288
|
const state = depths.get(d);
|
|
159
289
|
depths.set(d, retain(state.entries, [...state.order.filter((k) => k !== key), key], key));
|
|
160
290
|
notify(d);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"segmentStore.js","sourceRoot":"","sources":["../../src/js/segmentStore.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What each segment boundary is showing, and what it is keeping alive behind it.\n *\n * A navigation replaces one segment. Keeping the previous one mounted — hidden,\n * not unmounted — is what lets going back restore it with its client state:\n * the half-typed form, the open disclosure, the scrolled list. Unmounting\n * throws all of that away, which is what replacing the root used to do.\n *\n * Entries are keyed by page (the URL, or its intercept variant), so a boundary\n * can hold several and reveal one. Empty is meaningful: a boundary with nothing\n * stored renders the children the server gave it.\n *\n * A store rather than state, and not a preference — three things rule state out.\n * A boundary is inserted between every layout level, so there are several and a\n * navigation targets one by depth; they are separated by server components, so\n * no setter can be threaded down to them, because a function does not cross\n * that boundary; and navigate.ts is a plain module with no component instance\n * to call one on. Addressing a component you hold no reference to is what an\n * external store is for.\n *\n * Underneath it is the wire protocol. A partial navigation sends only the\n * segment that changed — see the X-RSC-Segments headers — so there is nothing\n * for a root to re-render with even if it held the state. Next.js keeps its\n * router in useState and derives every segment from it; that is the same trade\n * in the other direction.\n *\n * What it costs: React pins an external store's updates to synchronous\n * priority, because a store cannot be safely time-sliced. Synchronous is never\n * a transition, and anything that only runs for one — React's <ViewTransition>\n * among them — never ran for a navigation.\n *\n * Which is why SegmentBoundary does not read this with useSyncExternalStore\n * any more. The store still does the addressing, which is the part only it can\n * do; the boundary copies into state, so the render is a transition. See the\n * view transitions guide.\n */\n\ntype Tree = unknown;\ntype Listener = () => void;\n\ninterface Entry {\n key: string;\n tree: Tree;\n /**\n * When this tree arrived, so a link can decide whether it is still worth\n * revealing. The back button never asks — it means \"the page I was on\",\n * however long ago that was.\n */\n at: number;\n}\n\n/**\n * Immutable: useSyncExternalStore compares snapshots by identity, so a new\n * object per read reads as \"changed every render\" and loops forever. Every\n * mutation replaces this wholesale; nothing edits one in place.\n */\ninterface DepthState {\n readonly entries: readonly Entry[];\n readonly activeKey: string;\n /** Most recently shown last; eviction takes from the front. */\n readonly order: readonly string[];\n}\n\n/** Pages kept alive per boundary. Four covers ordinary back-and-forth. */\nexport const RETENTION = 4;\n\nconst depths = new Map<number, DepthState>();\nconst listeners = new Map<number, Set<Listener>>();\n\nfunction notify(depth: number): void {\n for (const listener of listeners.get(depth) ?? []) listener();\n}\n\nexport function subscribeToSegment(\n depth: number,\n listener: Listener,\n): () => void {\n let set = listeners.get(depth);\n\n if (!set) {\n set = new Set();\n listeners.set(depth, set);\n }\n\n set.add(listener);\n\n return () => {\n set!.delete(listener);\n if (set!.size === 0) listeners.delete(depth);\n };\n}\n\n/** Everything a boundary at this depth needs to render, or null for \"use children\". */\nexport function getSegmentState(depth: number): DepthState | null {\n return depths.get(depth) ?? null;\n}\n\n/** Apply the retention window to a candidate state. */\nfunction retain(\n entries: readonly Entry[],\n order: readonly string[],\n activeKey: string,\n): DepthState {\n const kept = order.slice(-RETENTION);\n\n return {\n entries: entries.filter((entry) => kept.includes(entry.key)),\n order: kept,\n activeKey,\n };\n}\n\nfunction put(depth: number, key: string, tree: Tree): void {\n const state = depths.get(depth);\n const entries = [\n ...(state?.entries ?? []).filter((entry) => entry.key !== key),\n { key, tree, at: Date.now() },\n ];\n const order = [...(state?.order ?? []).filter((k) => k !== key), key];\n\n depths.set(depth, retain(entries, order, key));\n}\n\n/**\n * Show `tree` at `depth` for `key`, retaining what was there.\n *\n * Deeper segments belonged to the page being replaced; leaving them would\n * render the previous page inside the new one.\n */\nexport function setSegment(depth: number, key: string, tree: Tree): void {\n put(depth, key, tree);\n\n const stale = [...depths.keys()].filter((d) => d > depth);\n for (const d of stale) depths.delete(d);\n\n notify(depth);\n for (const d of stale) notify(d);\n}\n\n/**\n * Record the children the server rendered, so the page you arrived on can be\n * returned to later. Never changes what is showing.\n */\nexport function seedSegment(depth: number, key: string, tree: Tree): void {\n const state = depths.get(depth);\n\n if (state?.entries.some((entry) => entry.key === key)) return;\n\n if (!state) {\n put(depth, key, tree);\n notify(depth);\n\n return;\n }\n\n // Older than whatever is showing, so it goes to the front of the eviction\n // order — and crucially does not become the active page.\n depths.set(\n depth,\n retain(\n [...state.entries, { key, tree, at: Date.now() }],\n [key, ...state.order.filter((k) => k !== key)],\n state.activeKey,\n ),\n );\n\n notify(depth);\n}\n\n/**\n * Reveal a page the boundaries are still holding, without asking the server.\n *\n * Restoring is anchored on the deepest boundary that can show the page. Deeper\n * ones than that belonged to the page being left — a section with its own\n * layout adds a boundary the page you are going back to never had — so they\n * are dropped, exactly as setSegment drops them. Requiring every boundary to\n * hold the key instead made any such page refuse to restore.\n *\n * Shallower boundaries need no key of their own: their trees contain the\n * deeper boundary, so they delegate to whatever it is showing. One that does\n * hold the key is switched to it, since that is a real change at its level.\n */\n/**\n * Reveal a page still being held, if it is worth revealing.\n *\n * `maxAge` is what a link passes and the back button does not. Going back is\n * unambiguous — it names a moment, and the page from that moment is the right\n * answer however old. A link says \"go here\", and answering it with a tree from\n * twenty minutes ago is stale data presented as fresh, which is the objection\n * this design started with. Recent enough, and it is the same page you were\n * just on, with the form you were filling in still filled in.\n */\nexport function restoreSegments(key: string, maxAge?: number): boolean {\n const holding = [...depths.keys()].filter((d) =>\n depths.get(d)!.entries.some((entry) => entry.key === key),\n );\n\n if (holding.length === 0) return false;\n\n if (maxAge !== undefined) {\n const ages = holding.flatMap((d) =>\n depths\n .get(d)!\n .entries.filter((entry) => entry.key === key)\n .map((entry) => entry.at),\n );\n\n // The oldest layer decides: revealing a fresh page under a stale layout\n // would be a chain nobody rendered together.\n // >= rather than >, so a window of 0 means never rather than \"only within\n // the same millisecond\".\n if (Date.now() - Math.min(...ages) >= maxAge) return false;\n }\n\n const anchor = Math.max(...holding);\n\n for (const d of [...depths.keys()].filter((d) => d > anchor)) {\n depths.delete(d);\n notify(d);\n }\n\n for (const d of holding) {\n const state = depths.get(d)!;\n\n depths.set(\n d,\n retain(\n state.entries,\n [...state.order.filter((k) => k !== key), key],\n key,\n ),\n );\n notify(d);\n }\n\n return true;\n}\n\n/**\n * Drop everything, so boundaries fall back to their server-given children.\n *\n * A deployment invalidates them all: a segment from the previous build has no\n * claim on being correct for this one.\n */\nexport function clearSegments(): void {\n const all = [...depths.keys()];\n depths.clear();\n for (const depth of all) notify(depth);\n}\n\n/**\n * Whether a navigation has happened in this document.\n *\n * The first commit after hydration is the boundary taking over its\n * server-rendered children - the same page, re-keyed for retention. A view\n * transition for that commit fades the page into itself: the blank-then-\n * content a first load or a reload showed. Inertia transitions on visits and\n * never on load; so does this. The router notes the first navigation before\n * it updates a segment, and the boundary animates from then on.\n */\nlet navigated = false;\n\nexport function noteNavigation(): void {\n navigated = true;\n}\n\nexport function navigatedOnce(): boolean {\n return navigated;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"segmentStore.js","sourceRoot":"","sources":["../../src/js/segmentStore.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What each segment boundary is showing, and what it is keeping alive behind it.\n *\n * A navigation replaces one segment. Keeping the previous one mounted — hidden,\n * not unmounted — is what lets going back restore it with its client state:\n * the half-typed form, the open disclosure, the scrolled list. Unmounting\n * throws all of that away, which is what replacing the root used to do.\n *\n * Entries are keyed by page (the URL, or its intercept variant), so a boundary\n * can hold several and reveal one. Empty is meaningful: a boundary with nothing\n * stored renders the children the server gave it.\n *\n * A store rather than state, and not a preference — three things rule state out.\n * A boundary is inserted between every layout level, so there are several and a\n * navigation targets one by depth; they are separated by server components, so\n * no setter can be threaded down to them, because a function does not cross\n * that boundary; and navigate.ts is a plain module with no component instance\n * to call one on. Addressing a component you hold no reference to is what an\n * external store is for.\n *\n * Underneath it is the wire protocol. A partial navigation sends only the\n * segment that changed — see the X-RSC-Segments headers — so there is nothing\n * for a root to re-render with even if it held the state. Next.js keeps its\n * router in useState and derives every segment from it; that is the same trade\n * in the other direction.\n *\n * What it costs: React pins an external store's updates to synchronous\n * priority, because a store cannot be safely time-sliced. Synchronous is never\n * a transition, and anything that only runs for one — React's <ViewTransition>\n * among them — never ran for a navigation.\n *\n * Which is why SegmentBoundary does not read this with useSyncExternalStore\n * any more. The store still does the addressing, which is the part only it can\n * do; the boundary copies into state, so the render is a transition. See the\n * view transitions guide.\n */\n\ntype Tree = unknown;\ntype Listener = () => void;\n\ninterface Entry {\n key: string;\n tree: Tree;\n /**\n * When this tree arrived, so a link can decide whether it is still worth\n * revealing. The back button never asks — it means \"the page I was on\",\n * however long ago that was.\n */\n at: number;\n /**\n * Its place in the order things happened, for the question \"was this\n * here before the mutation?\" - which a clock answers wrongly for a seed\n * and a write in the same millisecond, and a test hit exactly that.\n */\n seq: number;\n /**\n * Rendered before the click, hidden, on the strength of a touch or a\n * settled hover - see prerenderSegment. Outside the retention window: it\n * is a guess, and a guess must not evict a page the visitor was on. One\n * per depth; the next guess replaces it, and a navigation to anything\n * else drops it.\n */\n speculative?: boolean;\n}\n\n/**\n * Immutable: useSyncExternalStore compares snapshots by identity, so a new\n * object per read reads as \"changed every render\" and loops forever. Every\n * mutation replaces this wholesale; nothing edits one in place.\n */\ninterface DepthState {\n readonly entries: readonly Entry[];\n readonly activeKey: string;\n /** Most recently shown last; eviction takes from the front. */\n readonly order: readonly string[];\n}\n\n/** Pages kept alive per boundary. Four covers ordinary back-and-forth. */\nexport const RETENTION = 4;\n\nconst depths = new Map<number, DepthState>();\n/** A counter every entry takes a number from, in order. */\nlet tick = 0;\n/** The number the last mutation took: everything held before it is wrong. */\nlet invalidatedAt = 0;\nconst listeners = new Map<number, Set<Listener>>();\n\nfunction notify(depth: number): void {\n for (const listener of listeners.get(depth) ?? []) listener();\n}\n\nexport function subscribeToSegment(\n depth: number,\n listener: Listener,\n): () => void {\n let set = listeners.get(depth);\n\n if (!set) {\n set = new Set();\n listeners.set(depth, set);\n }\n\n set.add(listener);\n\n return () => {\n set!.delete(listener);\n if (set!.size === 0) listeners.delete(depth);\n };\n}\n\n/** Everything a boundary at this depth needs to render, or null for \"use children\". */\nexport function getSegmentState(depth: number): DepthState | null {\n return depths.get(depth) ?? null;\n}\n\n/** Apply the retention window to a candidate state. */\nfunction retain(\n entries: readonly Entry[],\n order: readonly string[],\n activeKey: string,\n): DepthState {\n const kept = order.slice(-RETENTION);\n\n return {\n entries: entries.filter((entry) => kept.includes(entry.key) || entry.speculative),\n order: kept,\n activeKey,\n };\n}\n\nfunction put(depth: number, key: string, tree: Tree): void {\n const state = depths.get(depth);\n const entries = [\n ...(state?.entries ?? []).filter((entry) => entry.key !== key),\n { key, tree, at: Date.now(), seq: ++tick },\n ];\n const order = [...(state?.order ?? []).filter((k) => k !== key), key];\n\n depths.set(depth, retain(entries, order, key));\n}\n\n/**\n * Show `tree` at `depth` for `key`, retaining what was there.\n *\n * Deeper segments belonged to the page being replaced; leaving them would\n * render the previous page inside the new one.\n */\nexport function setSegment(depth: number, key: string, tree: Tree): void {\n // A guess about another page was wrong; the one about this page, if there\n // was one, is replaced by put() below - with the same tree, when the\n // prerender and the navigation read the same decoded payload, which is\n // what makes the update a reveal rather than a render.\n dropSpeculative(depth, key);\n put(depth, key, tree);\n\n const stale = [...depths.keys()].filter((d) => d > depth);\n for (const d of stale) depths.delete(d);\n\n notify(depth);\n for (const d of stale) notify(d);\n}\n\nfunction dropSpeculative(depth: number, except?: string): void {\n const state = depths.get(depth);\n\n if (!state?.entries.some((entry) => entry.speculative && entry.key !== except)) return;\n\n depths.set(depth, {\n ...state,\n entries: state.entries.filter((entry) => !entry.speculative || entry.key === except),\n });\n}\n\n/**\n * Render a page hidden at `depth`, before any navigation to it.\n *\n * The click then finds the work done: setSegment with the same tree is a\n * bail-out for React - same element, same props - and the Activity flips\n * from hidden to visible. What was 87 ms of rendering on a phone, after\n * the tap, is paid before the finger lifts, at idle priority, yielding to\n * the scroll. Never changes what is showing, never counts against\n * retention, and a page already held needs nothing.\n */\nexport function prerenderSegment(depth: number, key: string, tree: Tree): void {\n const state = depths.get(depth);\n\n // Nothing at this depth yet: the boundary is still showing the server's\n // children, and it has no page key to keep them under. A guess would take\n // the store over with no active entry to show. seedSegment runs on mount,\n // so this is the gap between hydration and that effect; the click will\n // render.\n if (!state) return;\n\n if (state.entries.some((entry) => entry.key === key)) return;\n\n depths.set(depth, {\n ...state,\n entries: [\n ...state.entries.filter((entry) => !entry.speculative),\n { key, tree, at: Date.now(), seq: ++tick, speculative: true },\n ],\n });\n\n notify(depth);\n}\n\n/** Whether a page is rendered hidden at `depth`, ahead of a navigation to it. */\nexport function isPrerendered(depth: number, key: string): boolean {\n return depths.get(depth)?.entries.some((entry) => entry.key === key && entry.speculative) ?? false;\n}\n\n/**\n * Give the page on screen at `depth` a new tree, under the key it has.\n *\n * For revalidate(\"all\"): the whole document rendered again, in place. The\n * root hands its layout the new tree, the layout hands its boundary new\n * children, and a boundary that holds state would otherwise keep showing\n * the store's tree and drop the new one on the floor - or, cleared first,\n * re-key its Activity to the current url and remount everything under it.\n * Neither. The entry that is showing takes the new tree and keeps its key,\n * so React reconciles the page in place, and the boundary below gets its\n * new children the same way.\n */\nexport function replaceActive(depth: number, tree: Tree): void {\n const state = depths.get(depth);\n\n if (!state) return;\n\n const active = state.entries.find((entry) => entry.key === state.activeKey);\n\n if (!active || active.tree === tree) return;\n\n // The tree changes; when the entry arrived does not. A layout's entry is\n // keyed by the page it was seeded with - \"/\" for a document loaded at\n // home - and stays active through every navigation below it. Stamped\n // with the re-render's time it read as held since the mutation, and a\n // tap on the brand link after \"Add to cart\" revealed it: the url went\n // to \"/\" and the product stayed, because the tree under that key was\n // now the product's document. See dropHidden and isHeld.\n depths.set(depth, {\n ...state,\n entries: state.entries.map((entry) => (entry === active ? { ...entry, tree } : entry)),\n });\n notify(depth);\n}\n\n/**\n * Record the children the server rendered, so the page you arrived on can be\n * returned to later. Never changes what is showing.\n */\nexport function seedSegment(depth: number, key: string, tree: Tree): void {\n const state = depths.get(depth);\n\n if (state?.entries.some((entry) => entry.key === key)) return;\n\n if (!state) {\n put(depth, key, tree);\n notify(depth);\n\n return;\n }\n\n // Older than whatever is showing, so it goes to the front of the eviction\n // order — and crucially does not become the active page.\n depths.set(\n depth,\n retain(\n [...state.entries, { key, tree, at: Date.now(), seq: ++tick }],\n [key, ...state.order.filter((k) => k !== key)],\n state.activeKey,\n ),\n );\n\n notify(depth);\n}\n\n/**\n * Reveal a page the boundaries are still holding, without asking the server.\n *\n * Restoring is anchored on the deepest boundary that can show the page. Deeper\n * ones than that belonged to the page being left — a section with its own\n * layout adds a boundary the page you are going back to never had — so they\n * are dropped, exactly as setSegment drops them. Requiring every boundary to\n * hold the key instead made any such page refuse to restore.\n *\n * Shallower boundaries need no key of their own: their trees contain the\n * deeper boundary, so they delegate to whatever it is showing. One that does\n * hold the key is switched to it, since that is a real change at its level.\n */\n/**\n * Whether a link to `key` would be answered by revealing a held page.\n *\n * The question restoreSegments answers, asked without acting on it: a\n * prefetch of a page the boundaries still hold is a request for nothing -\n * a navigation would reveal it. The page just left is the usual case, one\n * wasted payload per navigation.\n */\nexport function isHeld(key: string, maxAge?: number): boolean {\n const ages = [...depths.values()].flatMap((state) =>\n state.entries.filter((entry) => entry.key === key && !entry.speculative && entry.seq > invalidatedAt).map((entry) => entry.at),\n );\n\n if (ages.length === 0) return false;\n if (maxAge === undefined) return true;\n\n return Date.now() - Math.min(...ages) < maxAge;\n}\n\n/**\n * Drop every page held behind the one on screen.\n *\n * After a mutation: a page kept for the back button holds the data from\n * before it, and revealing it would show a row that is gone, a name that\n * changed. The page on screen stays - it was re-rendered by the action, or\n * is about to be.\n */\nexport function dropHidden(): void {\n // And the ones that stay - the active entry at each depth - are from\n // before it too. A layout's entry is keyed by the page it was seeded\n // with, and revealing that key later shows the page inside it as it was\n // then. Nothing seeded before this moment is revealed again.\n invalidatedAt = ++tick;\n\n for (const [depth, state] of depths) {\n if (state.entries.length <= 1 && !state.entries.some((entry) => entry.speculative)) continue;\n\n depths.set(depth, {\n entries: state.entries.filter((entry) => entry.key === state.activeKey),\n order: state.order.filter((key) => key === state.activeKey),\n activeKey: state.activeKey,\n });\n notify(depth);\n }\n}\n\n/**\n * Reveal a page still being held, if it is worth revealing.\n *\n * `maxAge` is what a link passes and the back button does not. Going back is\n * unambiguous — it names a moment, and the page from that moment is the right\n * answer however old. A link says \"go here\", and answering it with a tree from\n * twenty minutes ago is stale data presented as fresh, which is the objection\n * this design started with. Recent enough, and it is the same page you were\n * just on, with the form you were filling in still filled in.\n */\nexport function restoreSegments(key: string, maxAge?: number): boolean {\n // A guess is not a held page: revealing it would be showing prefetched\n // data as the page the visitor was on. The navigation takes the\n // prerendered tree through setSegment instead, where it is a reveal too.\n // Nor is a page from before a mutation - see dropHidden.\n const holding = [...depths.keys()].filter((d) =>\n depths.get(d)!.entries.some((entry) => entry.key === key && !entry.speculative && entry.seq > invalidatedAt),\n );\n\n if (holding.length === 0) return false;\n\n if (maxAge !== undefined) {\n const ages = holding.flatMap((d) =>\n depths\n .get(d)!\n .entries.filter((entry) => entry.key === key)\n .map((entry) => entry.at),\n );\n\n // The oldest layer decides: revealing a fresh page under a stale layout\n // would be a chain nobody rendered together.\n // >= rather than >, so a window of 0 means never rather than \"only within\n // the same millisecond\".\n if (Date.now() - Math.min(...ages) >= maxAge) return false;\n }\n\n const anchor = Math.max(...holding);\n\n for (const d of [...depths.keys()].filter((d) => d > anchor)) {\n depths.delete(d);\n notify(d);\n }\n\n for (const d of holding) {\n dropSpeculative(d);\n\n const state = depths.get(d)!;\n\n depths.set(\n d,\n retain(\n state.entries,\n [...state.order.filter((k) => k !== key), key],\n key,\n ),\n );\n notify(d);\n }\n\n return true;\n}\n\n/**\n * Drop everything, so boundaries fall back to their server-given children.\n *\n * A deployment invalidates them all: a segment from the previous build has no\n * claim on being correct for this one.\n */\nexport function clearSegments(): void {\n const all = [...depths.keys()];\n depths.clear();\n for (const depth of all) notify(depth);\n}\n\n/**\n * Whether a navigation has happened in this document.\n *\n * The first commit after hydration is the boundary taking over its\n * server-rendered children - the same page, re-keyed for retention. A view\n * transition for that commit fades the page into itself: the blank-then-\n * content a first load or a reload showed. Inertia transitions on visits and\n * never on load; so does this. The router notes the first navigation before\n * it updates a segment, and the boundary animates from then on.\n */\nlet navigated = false;\n\nexport function noteNavigation(): void {\n navigated = true;\n}\n\nexport function navigatedOnce(): boolean {\n return navigated;\n}\n"]}
|
package/dist/js/staleAssets.d.ts
CHANGED
|
@@ -23,5 +23,15 @@ export declare function isStaleAssetError(error: unknown): boolean;
|
|
|
23
23
|
* place is the one refused.
|
|
24
24
|
*/
|
|
25
25
|
export declare function loadDocumentOnce(href?: string): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Say why the router is loading a document, before it does.
|
|
28
|
+
*
|
|
29
|
+
* `rsc-kit:document-load`, with the url and the reason: a document load is
|
|
30
|
+
* the one thing the router does that looks like a bug when it was a
|
|
31
|
+
* decision, and a page that reloaded under a tester with nothing in the
|
|
32
|
+
* console had no way to say which decision. An app listens to record it; a
|
|
33
|
+
* test listens to assert on it.
|
|
34
|
+
*/
|
|
35
|
+
export declare function announceDocumentLoad(url: string, reason: string): void;
|
|
26
36
|
/** True when the page is being reloaded for it; false when the error is something else, or reloading already failed. */
|
|
27
37
|
export declare function recoverFromStaleAssets(error: unknown): boolean;
|
package/dist/js/staleAssets.js
CHANGED
|
@@ -54,12 +54,33 @@ export function loadDocumentOnce(href) {
|
|
|
54
54
|
catch {
|
|
55
55
|
// No storage: load anyway, once is the best that can be promised.
|
|
56
56
|
}
|
|
57
|
-
if (href === undefined)
|
|
57
|
+
if (href === undefined) {
|
|
58
|
+
announceDocumentLoad(window.location.href, "reload");
|
|
58
59
|
window.location.reload();
|
|
59
|
-
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
announceDocumentLoad(href, "stale-or-newer-build");
|
|
60
63
|
window.location.href = href;
|
|
64
|
+
}
|
|
61
65
|
return true;
|
|
62
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Say why the router is loading a document, before it does.
|
|
69
|
+
*
|
|
70
|
+
* `rsc-kit:document-load`, with the url and the reason: a document load is
|
|
71
|
+
* the one thing the router does that looks like a bug when it was a
|
|
72
|
+
* decision, and a page that reloaded under a tester with nothing in the
|
|
73
|
+
* console had no way to say which decision. An app listens to record it; a
|
|
74
|
+
* test listens to assert on it.
|
|
75
|
+
*/
|
|
76
|
+
export function announceDocumentLoad(url, reason) {
|
|
77
|
+
try {
|
|
78
|
+
window.dispatchEvent(new CustomEvent("rsc-kit:document-load", { detail: { url, reason } }));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Nothing to tell.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
63
84
|
/** True when the page is being reloaded for it; false when the error is something else, or reloading already failed. */
|
|
64
85
|
export function recoverFromStaleAssets(error) {
|
|
65
86
|
if (!isStaleAssetError(error))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"staleAssets.js","sourceRoot":"","sources":["../../src/js/staleAssets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,6EAA6E;AAC7E,2EAA2E;AAC3E,4EAA4E;AAC5E,mEAAmE;AACnE,MAAM,YAAY,GAChB,4MAA4M,CAAC;AAE/M,MAAM,QAAQ,GAAG,kBAAkB,CAAC;AACpC,MAAM,SAAS,GAAG,MAAM,CAAC;AAEzB;;;;;GAKG;AACH,MAAM,WAAW,GAAG,4EAA4E,CAAC;AAEjG,MAAM,UAAU,iBAAiB,CAAC,KAAc;IAC9C,MAAM,OAAO,GAAG,MAAM,CAAE,KAAqC,EAAE,OAAO,IAAI,KAAK,CAAC,CAAC;IAEjF,IAAI,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC;IAE5C,OAAO,CAAC,CAAC,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AAC7D,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAa;IAC5C,IAAI,OAAO,MAAM,KAAK,WAAW;QAAE,OAAO,KAAK,CAAC;IAEhD,MAAM,IAAI,GAAG,GAAG,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;IAEnD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAEvD,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,SAAS;YAAE,OAAO,KAAK,CAAC;QAEhD,cAAc,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IACnD,CAAC;IAAC,MAAM,CAAC;QACP,kEAAkE;IACpE,CAAC;IAED,IAAI,IAAI,KAAK,SAAS;
|
|
1
|
+
{"version":3,"file":"staleAssets.js","sourceRoot":"","sources":["../../src/js/staleAssets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,6EAA6E;AAC7E,2EAA2E;AAC3E,4EAA4E;AAC5E,mEAAmE;AACnE,MAAM,YAAY,GAChB,4MAA4M,CAAC;AAE/M,MAAM,QAAQ,GAAG,kBAAkB,CAAC;AACpC,MAAM,SAAS,GAAG,MAAM,CAAC;AAEzB;;;;;GAKG;AACH,MAAM,WAAW,GAAG,4EAA4E,CAAC;AAEjG,MAAM,UAAU,iBAAiB,CAAC,KAAc;IAC9C,MAAM,OAAO,GAAG,MAAM,CAAE,KAAqC,EAAE,OAAO,IAAI,KAAK,CAAC,CAAC;IAEjF,IAAI,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC;IAE5C,OAAO,CAAC,CAAC,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AAC7D,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAa;IAC5C,IAAI,OAAO,MAAM,KAAK,WAAW;QAAE,OAAO,KAAK,CAAC;IAEhD,MAAM,IAAI,GAAG,GAAG,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;IAEnD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAEvD,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,SAAS;YAAE,OAAO,KAAK,CAAC;QAEhD,cAAc,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IACnD,CAAC;IAAC,MAAM,CAAC;QACP,kEAAkE;IACpE,CAAC;IAED,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,oBAAoB,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QACrD,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;IAC3B,CAAC;SAAM,CAAC;QACN,oBAAoB,CAAC,IAAI,EAAE,sBAAsB,CAAC,CAAC;QACnD,MAAM,CAAC,QAAQ,CAAC,IAAI,GAAG,IAAI,CAAC;IAC9B,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAW,EAAE,MAAc;IAC9D,IAAI,CAAC;QACH,MAAM,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,uBAAuB,EAAE,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC;IAC9F,CAAC;IAAC,MAAM,CAAC;QACP,mBAAmB;IACrB,CAAC;AACH,CAAC;AAED,wHAAwH;AACxH,MAAM,UAAU,sBAAsB,CAAC,KAAc;IACnD,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAE5C,OAAO,gBAAgB,EAAE,CAAC;AAC5B,CAAC","sourcesContent":["/**\n * A navigation that lands on a chunk that is no longer there.\n *\n * After a deploy the open tab still holds the old page, and its next\n * navigation asks for client components by the old hashed names, which the\n * new deploy does not serve. The dev server does the same when it\n * re-optimises dependencies and answers the old names with 504. Either way\n * the payload arrives and React fails to load the module it names, on a\n * page that was working a click ago.\n *\n * The fix is the one the browser would have applied: load the document again.\n * Once — a second failure on the same url within a few seconds means the\n * deploy is broken, not stale, and reloading forever would hide that.\n */\n\n// A reference the payload names and this client's manifest lacks is the same\n// news as a chunk that is gone: the payload is from a newer build than the\n// page. Under a service worker that serves the last build's document first,\n// it is every returning visitor's first navigation after a deploy.\nconst STALE_MODULE =\n /Failed to fetch dynamically imported module|Importing a module script failed|error loading dynamically imported module|Outdated Optimize Dep|Loading (?:CSS )?chunk|(?:client|server) reference not found/i;\n\nconst RELOADED = \"rsc-kit:reloaded\";\nconst WINDOW_MS = 10_000;\n\n/**\n * Development only: the shape a page takes when Vite re-optimised the\n * browser's dependencies underneath it. Modules already loaded hold the old\n * React, freshly loaded ones the new; a hook then reads a null dispatcher.\n * The page is over either way; loading it again is what Vite would do next.\n */\nconst MIXED_REACT = /Invalid hook call|Cannot read properties of null \\(reading 'use[A-Z]\\w*'\\)/;\n\nexport function isStaleAssetError(error: unknown): boolean {\n const message = String((error as { message?: string } | null)?.message ?? error);\n\n if (STALE_MODULE.test(message)) return true;\n\n return !!import.meta.env?.DEV && MIXED_REACT.test(message);\n}\n\n/**\n * Load a document, once: this one again, or the one at `href`.\n *\n * False when the same url was loaded this way within the window - the deploy\n * is broken, not stale, or a worker still serving the last build's document\n * answered the reload with it - and loading forever would hide that. The\n * mark is keyed by where the page is, so the second attempt from the same\n * place is the one refused.\n */\nexport function loadDocumentOnce(href?: string): boolean {\n if (typeof window === \"undefined\") return false;\n\n const mark = `${RELOADED}:${window.location.href}`;\n\n try {\n const last = Number(sessionStorage.getItem(mark) ?? 0);\n\n if (Date.now() - last < WINDOW_MS) return false;\n\n sessionStorage.setItem(mark, String(Date.now()));\n } catch {\n // No storage: load anyway, once is the best that can be promised.\n }\n\n if (href === undefined) {\n announceDocumentLoad(window.location.href, \"reload\");\n window.location.reload();\n } else {\n announceDocumentLoad(href, \"stale-or-newer-build\");\n window.location.href = href;\n }\n\n return true;\n}\n\n/**\n * Say why the router is loading a document, before it does.\n *\n * `rsc-kit:document-load`, with the url and the reason: a document load is\n * the one thing the router does that looks like a bug when it was a\n * decision, and a page that reloaded under a tester with nothing in the\n * console had no way to say which decision. An app listens to record it; a\n * test listens to assert on it.\n */\nexport function announceDocumentLoad(url: string, reason: string): void {\n try {\n window.dispatchEvent(new CustomEvent(\"rsc-kit:document-load\", { detail: { url, reason } }));\n } catch {\n // Nothing to tell.\n }\n}\n\n/** True when the page is being reloaded for it; false when the error is something else, or reloading already failed. */\nexport function recoverFromStaleAssets(error: unknown): boolean {\n if (!isStaleAssetError(error)) return false;\n\n return loadDocumentOnce();\n}\n"]}
|
|
@@ -25,6 +25,26 @@
|
|
|
25
25
|
*/
|
|
26
26
|
let observer = null;
|
|
27
27
|
const pending = new WeakMap();
|
|
28
|
+
/**
|
|
29
|
+
* How many links a page prefetches on sight before the rest wait for
|
|
30
|
+
* intent. A product page's payload is 30 KB with its related products,
|
|
31
|
+
* and a listing shows twenty-four of them: on sight, every one, that was
|
|
32
|
+
* three quarters of a megabyte per page on a phone. The first dozen in
|
|
33
|
+
* view are fetched; a link past the budget is fetched on touch or on a
|
|
34
|
+
* settled hover, a round trip before the click rather than before the
|
|
35
|
+
* scroll. The budget is the page's, and starts again on each navigation.
|
|
36
|
+
*/
|
|
37
|
+
const ON_SIGHT_PER_PAGE = 12;
|
|
38
|
+
let onSight = 0;
|
|
39
|
+
let listening = false;
|
|
40
|
+
function budgetPerPage() {
|
|
41
|
+
if (listening || typeof window === "undefined")
|
|
42
|
+
return;
|
|
43
|
+
listening = true;
|
|
44
|
+
window.addEventListener("rsc-navigate", () => {
|
|
45
|
+
onSight = 0;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
28
48
|
function savingData() {
|
|
29
49
|
const connection = navigator.connection;
|
|
30
50
|
return connection?.saveData === true;
|
|
@@ -48,8 +68,12 @@ function observerFor() {
|
|
|
48
68
|
const fire = pending.get(entry.target);
|
|
49
69
|
observer.unobserve(entry.target);
|
|
50
70
|
pending.delete(entry.target);
|
|
51
|
-
if (fire)
|
|
52
|
-
|
|
71
|
+
if (!fire)
|
|
72
|
+
continue;
|
|
73
|
+
if (onSight >= ON_SIGHT_PER_PAGE)
|
|
74
|
+
continue;
|
|
75
|
+
onSight++;
|
|
76
|
+
whenIdle(fire);
|
|
53
77
|
}
|
|
54
78
|
},
|
|
55
79
|
// A little ahead of the fold: a link about to scroll into view is one
|
|
@@ -64,6 +88,7 @@ function observerFor() {
|
|
|
64
88
|
export function prefetchWhenVisible(element, fire) {
|
|
65
89
|
if (!element || savingData())
|
|
66
90
|
return () => { };
|
|
91
|
+
budgetPerPage();
|
|
67
92
|
const io = observerFor();
|
|
68
93
|
if (!io)
|
|
69
94
|
return () => { };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"viewportPrefetch.js","sourceRoot":"","sources":["../../src/js/viewportPrefetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,IAAI,QAAQ,GAAgC,IAAI,CAAA;AAChD,MAAM,OAAO,GAAG,IAAI,OAAO,EAAuB,CAAA;AAElD,SAAS,UAAU;IACjB,MAAM,UAAU,GAAI,SAAqD,CAAC,UAAU,CAAA;IAEpF,OAAO,UAAU,EAAE,QAAQ,KAAK,IAAI,CAAA;AACtC,CAAC;AAED,SAAS,QAAQ,CAAC,EAAc;IAC9B,MAAM,IAAI,GAAI,MAAyF,CAAC,mBAAmB,CAAA;IAE3H,IAAI,IAAI;QAAE,IAAI,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAA;;QAChC,UAAU,CAAC,EAAE,EAAE,EAAE,CAAC,CAAA;AACzB,CAAC;AAED,SAAS,WAAW;IAClB,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAA;IAC7B,IAAI,OAAO,oBAAoB,KAAK,WAAW;QAAE,OAAO,IAAI,CAAA;IAE5D,QAAQ,GAAG,IAAI,oBAAoB,CACjC,CAAC,OAAO,EAAE,EAAE;QACV,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,IAAI,CAAC,KAAK,CAAC,cAAc;gBAAE,SAAQ;YAEnC,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;YAEtC,QAAS,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;YACjC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;YAE5B,IAAI,IAAI;gBAAE,QAAQ,CAAC,IAAI,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"viewportPrefetch.js","sourceRoot":"","sources":["../../src/js/viewportPrefetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,IAAI,QAAQ,GAAgC,IAAI,CAAA;AAChD,MAAM,OAAO,GAAG,IAAI,OAAO,EAAuB,CAAA;AAElD;;;;;;;;GAQG;AACH,MAAM,iBAAiB,GAAG,EAAE,CAAA;AAC5B,IAAI,OAAO,GAAG,CAAC,CAAA;AACf,IAAI,SAAS,GAAG,KAAK,CAAA;AAErB,SAAS,aAAa;IACpB,IAAI,SAAS,IAAI,OAAO,MAAM,KAAK,WAAW;QAAE,OAAM;IAEtD,SAAS,GAAG,IAAI,CAAA;IAChB,MAAM,CAAC,gBAAgB,CAAC,cAAc,EAAE,GAAG,EAAE;QAC3C,OAAO,GAAG,CAAC,CAAA;IACb,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,SAAS,UAAU;IACjB,MAAM,UAAU,GAAI,SAAqD,CAAC,UAAU,CAAA;IAEpF,OAAO,UAAU,EAAE,QAAQ,KAAK,IAAI,CAAA;AACtC,CAAC;AAED,SAAS,QAAQ,CAAC,EAAc;IAC9B,MAAM,IAAI,GAAI,MAAyF,CAAC,mBAAmB,CAAA;IAE3H,IAAI,IAAI;QAAE,IAAI,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAA;;QAChC,UAAU,CAAC,EAAE,EAAE,EAAE,CAAC,CAAA;AACzB,CAAC;AAED,SAAS,WAAW;IAClB,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAA;IAC7B,IAAI,OAAO,oBAAoB,KAAK,WAAW;QAAE,OAAO,IAAI,CAAA;IAE5D,QAAQ,GAAG,IAAI,oBAAoB,CACjC,CAAC,OAAO,EAAE,EAAE;QACV,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,IAAI,CAAC,KAAK,CAAC,cAAc;gBAAE,SAAQ;YAEnC,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;YAEtC,QAAS,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;YACjC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;YAE5B,IAAI,CAAC,IAAI;gBAAE,SAAQ;YACnB,IAAI,OAAO,IAAI,iBAAiB;gBAAE,SAAQ;YAE1C,OAAO,EAAE,CAAA;YACT,QAAQ,CAAC,IAAI,CAAC,CAAA;QAChB,CAAC;IACH,CAAC;IACD,sEAAsE;IACtE,sBAAsB;IACtB,EAAE,UAAU,EAAE,OAAO,EAAE,CACxB,CAAA;IAED,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAAuB,EAAE,IAAgB;IAC3E,IAAI,CAAC,OAAO,IAAI,UAAU,EAAE;QAAE,OAAO,GAAG,EAAE,GAAE,CAAC,CAAA;IAE7C,aAAa,EAAE,CAAA;IAEf,MAAM,EAAE,GAAG,WAAW,EAAE,CAAA;IAExB,IAAI,CAAC,EAAE;QAAE,OAAO,GAAG,EAAE,GAAE,CAAC,CAAA;IAExB,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;IAC1B,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;IAEnB,OAAO,GAAG,EAAE;QACV,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;QACvB,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA;IACvB,CAAC,CAAA;AACH,CAAC","sourcesContent":["/**\n * Prefetching a link as it comes into view.\n *\n * A navigation is as fast as what is already in the browser when the click\n * lands, and a hover is too late to fetch that: a pointer that has settled\n * on a link is 200-300 ms from clicking it, and on a phone the first signal\n * is touchstart, 100-300 ms before the click - about a round trip either\n * way, so a fetch started there has barely left when the navigation needs\n * it. Next prefetches the links on screen instead, and that is what makes\n * its clicks feel like a single-page app's: the payload was there before\n * the pointer moved. So, on every device, the links on screen are fetched -\n * the bytes only; decoding a payload loads the chunks it names, and that\n * waits for the hover or the touch that says which link is meant. It used\n * to be phones only, with a hover-capable device left to the hover: a click\n * that came quicker than the round trip waited for it, and desktop was\n * fast but not instant.\n *\n * Not when the visitor asked for less data: Save-Data is the one signal a\n * browser gives that speculative requests are unwelcome.\n *\n * One observer for every link, and the work is done when the browser is\n * idle - a list scrolled into view is many links at once, and the browser's\n * per-origin connections should not all be taken by pages nobody has tapped\n * yet. Each link prefetches once; the router keeps the payload for its TTL.\n */\n\nlet observer: IntersectionObserver | null = null\nconst pending = new WeakMap<Element, () => void>()\n\n/**\n * How many links a page prefetches on sight before the rest wait for\n * intent. A product page's payload is 30 KB with its related products,\n * and a listing shows twenty-four of them: on sight, every one, that was\n * three quarters of a megabyte per page on a phone. The first dozen in\n * view are fetched; a link past the budget is fetched on touch or on a\n * settled hover, a round trip before the click rather than before the\n * scroll. The budget is the page's, and starts again on each navigation.\n */\nconst ON_SIGHT_PER_PAGE = 12\nlet onSight = 0\nlet listening = false\n\nfunction budgetPerPage(): void {\n if (listening || typeof window === \"undefined\") return\n\n listening = true\n window.addEventListener(\"rsc-navigate\", () => {\n onSight = 0\n })\n}\n\nfunction savingData(): boolean {\n const connection = (navigator as { connection?: { saveData?: boolean } }).connection\n\n return connection?.saveData === true\n}\n\nfunction whenIdle(fn: () => void): void {\n const idle = (window as { requestIdleCallback?: (fn: () => void, opts?: { timeout: number }) => void }).requestIdleCallback\n\n if (idle) idle(fn, { timeout: 1000 })\n else setTimeout(fn, 50)\n}\n\nfunction observerFor(): IntersectionObserver | null {\n if (observer) return observer\n if (typeof IntersectionObserver === \"undefined\") return null\n\n observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (!entry.isIntersecting) continue\n\n const fire = pending.get(entry.target)\n\n observer!.unobserve(entry.target)\n pending.delete(entry.target)\n\n if (!fire) continue\n if (onSight >= ON_SIGHT_PER_PAGE) continue\n\n onSight++\n whenIdle(fire)\n }\n },\n // A little ahead of the fold: a link about to scroll into view is one\n // about to be tapped.\n { rootMargin: \"200px\" },\n )\n\n return observer\n}\n\n/**\n * Prefetch when the link is on screen. Returns the function that stops\n * watching; a no-op where this does not apply.\n */\nexport function prefetchWhenVisible(element: Element | null, fire: () => void): () => void {\n if (!element || savingData()) return () => {}\n\n budgetPerPage()\n\n const io = observerFor()\n\n if (!io) return () => {}\n\n pending.set(element, fire)\n io.observe(element)\n\n return () => {\n pending.delete(element)\n io.unobserve(element)\n }\n}\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The head of a stored shell, corrected for the url it is served for.
|
|
3
|
+
*
|
|
4
|
+
* A shell for a route that listed no urls is one file for every url the
|
|
5
|
+
* route matches, so its <title> is whatever the build could know without a
|
|
6
|
+
* url: the layouts' metadata, once the page's own generateMetadata was seen
|
|
7
|
+
* to read the params and left out. The host serving it does know the url,
|
|
8
|
+
* and the page's metadata is a function of it - so the title and the
|
|
9
|
+
* description are written into the head here, as a string edit on the way
|
|
10
|
+
* out, before React resumes the holes below. The client's DocumentTitle
|
|
11
|
+
* sets the title again after hydration; this is for the tab before that,
|
|
12
|
+
* and for whoever reads the document without running it.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* `shell` with the given title and description in its head. Only the two
|
|
16
|
+
* that every share preview and every tab reads; the rest of a page's
|
|
17
|
+
* metadata rides with the payload.
|
|
18
|
+
*/
|
|
19
|
+
export declare function withHead(shell: string, metadata: {
|
|
20
|
+
title?: unknown;
|
|
21
|
+
description?: unknown;
|
|
22
|
+
} | null): string;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The head of a stored shell, corrected for the url it is served for.
|
|
3
|
+
*
|
|
4
|
+
* A shell for a route that listed no urls is one file for every url the
|
|
5
|
+
* route matches, so its <title> is whatever the build could know without a
|
|
6
|
+
* url: the layouts' metadata, once the page's own generateMetadata was seen
|
|
7
|
+
* to read the params and left out. The host serving it does know the url,
|
|
8
|
+
* and the page's metadata is a function of it - so the title and the
|
|
9
|
+
* description are written into the head here, as a string edit on the way
|
|
10
|
+
* out, before React resumes the holes below. The client's DocumentTitle
|
|
11
|
+
* sets the title again after hydration; this is for the tab before that,
|
|
12
|
+
* and for whoever reads the document without running it.
|
|
13
|
+
*/
|
|
14
|
+
function escapeHtml(text) {
|
|
15
|
+
return text
|
|
16
|
+
.replace(/&/g, "&")
|
|
17
|
+
.replace(/</g, "<")
|
|
18
|
+
.replace(/>/g, ">")
|
|
19
|
+
.replace(/"/g, """);
|
|
20
|
+
}
|
|
21
|
+
const TITLE = /<title(?:\s[^>]*)?>[\s\S]*?<\/title>/i;
|
|
22
|
+
const DESCRIPTION = /<meta\s+name=(?:"description"|'description'|description)\s+content=(?:"[^"]*"|'[^']*'|[^\s>]*)\s*\/?>/i;
|
|
23
|
+
const HEAD_END = /<\/head>/i;
|
|
24
|
+
/**
|
|
25
|
+
* `shell` with the given title and description in its head. Only the two
|
|
26
|
+
* that every share preview and every tab reads; the rest of a page's
|
|
27
|
+
* metadata rides with the payload.
|
|
28
|
+
*/
|
|
29
|
+
export function withHead(shell, metadata) {
|
|
30
|
+
if (!metadata)
|
|
31
|
+
return shell;
|
|
32
|
+
let out = shell;
|
|
33
|
+
if (metadata.title != null) {
|
|
34
|
+
const tag = `<title>${escapeHtml(String(metadata.title))}</title>`;
|
|
35
|
+
out = TITLE.test(out) ? out.replace(TITLE, tag) : out.replace(HEAD_END, `${tag}</head>`);
|
|
36
|
+
}
|
|
37
|
+
if (metadata.description != null) {
|
|
38
|
+
const tag = `<meta name="description" content="${escapeHtml(String(metadata.description))}"/>`;
|
|
39
|
+
out = DESCRIPTION.test(out) ? out.replace(DESCRIPTION, tag) : out.replace(HEAD_END, `${tag}</head>`);
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=shellHead.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shellHead.js","sourceRoot":"","sources":["../src/shellHead.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,SAAS,UAAU,CAAC,IAAY;IAC9B,OAAO,IAAI;SACR,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AAC7B,CAAC;AAED,MAAM,KAAK,GAAG,uCAAuC,CAAC;AACtD,MAAM,WAAW,GAAG,wGAAwG,CAAC;AAC7H,MAAM,QAAQ,GAAG,WAAW,CAAC;AAE7B;;;;GAIG;AACH,MAAM,UAAU,QAAQ,CACtB,KAAa,EACb,QAA2D;IAE3D,IAAI,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAC;IAE5B,IAAI,GAAG,GAAG,KAAK,CAAC;IAEhB,IAAI,QAAQ,CAAC,KAAK,IAAI,IAAI,EAAE,CAAC;QAC3B,MAAM,GAAG,GAAG,UAAU,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,UAAU,CAAC;QAEnE,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,GAAG,SAAS,CAAC,CAAC;IAC3F,CAAC;IAED,IAAI,QAAQ,CAAC,WAAW,IAAI,IAAI,EAAE,CAAC;QACjC,MAAM,GAAG,GAAG,qCAAqC,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,KAAK,CAAC;QAE/F,GAAG,GAAG,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,GAAG,SAAS,CAAC,CAAC;IACvG,CAAC;IAED,OAAO,GAAG,CAAC;AACb,CAAC","sourcesContent":["/**\n * The head of a stored shell, corrected for the url it is served for.\n *\n * A shell for a route that listed no urls is one file for every url the\n * route matches, so its <title> is whatever the build could know without a\n * url: the layouts' metadata, once the page's own generateMetadata was seen\n * to read the params and left out. The host serving it does know the url,\n * and the page's metadata is a function of it - so the title and the\n * description are written into the head here, as a string edit on the way\n * out, before React resumes the holes below. The client's DocumentTitle\n * sets the title again after hydration; this is for the tab before that,\n * and for whoever reads the document without running it.\n */\n\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\");\n}\n\nconst TITLE = /<title(?:\\s[^>]*)?>[\\s\\S]*?<\\/title>/i;\nconst DESCRIPTION = /<meta\\s+name=(?:\"description\"|'description'|description)\\s+content=(?:\"[^\"]*\"|'[^']*'|[^\\s>]*)\\s*\\/?>/i;\nconst HEAD_END = /<\\/head>/i;\n\n/**\n * `shell` with the given title and description in its head. Only the two\n * that every share preview and every tab reads; the rest of a page's\n * metadata rides with the payload.\n */\nexport function withHead(\n shell: string,\n metadata: { title?: unknown; description?: unknown } | null,\n): string {\n if (!metadata) return shell;\n\n let out = shell;\n\n if (metadata.title != null) {\n const tag = `<title>${escapeHtml(String(metadata.title))}</title>`;\n\n out = TITLE.test(out) ? out.replace(TITLE, tag) : out.replace(HEAD_END, `${tag}</head>`);\n }\n\n if (metadata.description != null) {\n const tag = `<meta name=\"description\" content=\"${escapeHtml(String(metadata.description))}\"/>`;\n\n out = DESCRIPTION.test(out) ? out.replace(DESCRIPTION, tag) : out.replace(HEAD_END, `${tag}</head>`);\n }\n\n return out;\n}\n"]}
|