@rsc-kit/core 0.30.20 → 0.30.22
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/js/Form.js +19 -2
- package/dist/js/Form.js.map +1 -1
- package/dist/js/createViteRscApp.js +2 -2
- package/dist/js/createViteRscApp.js.map +1 -1
- package/dist/js/navigate.d.ts +9 -2
- package/dist/js/navigate.js +86 -20
- package/dist/js/navigate.js.map +1 -1
- package/dist/js/segmentStore.d.ts +1 -1
- package/dist/js/segmentStore.js +14 -9
- package/dist/js/segmentStore.js.map +1 -1
- package/dist/routes.d.ts +13 -1
- package/dist/routes.js.map +1 -1
- package/dist/vite.d.ts +11 -0
- package/dist/vite.js +30 -4
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
package/dist/js/navigate.js
CHANGED
|
@@ -320,10 +320,12 @@ export function setCallServer(fn) {
|
|
|
320
320
|
callServerFn = fn;
|
|
321
321
|
}
|
|
322
322
|
/**
|
|
323
|
-
* The urls a route.ts answers,
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
323
|
+
* The urls a route.ts answers, and everything under a prefix the app named in
|
|
324
|
+
* `backendPaths`, so a link to one is treated as the anchor it is: no prefetch
|
|
325
|
+
* - a hover must not run a route, or start an OAuth flow - and a full
|
|
326
|
+
* navigation rather than a payload fetch, since the answer is a Response or
|
|
327
|
+
* the backend's, not a page. Baked into the generated browser entry from the
|
|
328
|
+
* route tree and the config.
|
|
327
329
|
*/
|
|
328
330
|
let apiRoutePatterns = [];
|
|
329
331
|
export function setApiRoutes(patterns) {
|
|
@@ -334,7 +336,22 @@ export function setApiRoutes(patterns) {
|
|
|
334
336
|
.join("/") +
|
|
335
337
|
"/?$"));
|
|
336
338
|
}
|
|
337
|
-
/**
|
|
339
|
+
/**
|
|
340
|
+
* Leave for a document load, and say why.
|
|
341
|
+
*
|
|
342
|
+
* In place of the entry being left when the navigation was asked to replace
|
|
343
|
+
* it. A redirect from inside a boundary is performed after its page was
|
|
344
|
+
* shown and pushed, and `href` would keep that url in history: Back from where
|
|
345
|
+
* it went returns to a page that redirects again.
|
|
346
|
+
*/
|
|
347
|
+
function loadDocument(url, reason, replace) {
|
|
348
|
+
announceDocumentLoad(url, reason);
|
|
349
|
+
if (replace)
|
|
350
|
+
window.location.replace(url);
|
|
351
|
+
else
|
|
352
|
+
window.location.href = url;
|
|
353
|
+
}
|
|
354
|
+
/** Whether a url is one a route.ts answers, or the backend's by name. */
|
|
338
355
|
export function isApiRoute(url) {
|
|
339
356
|
if (apiRoutePatterns.length === 0)
|
|
340
357
|
return false;
|
|
@@ -500,7 +517,10 @@ notFound = false) {
|
|
|
500
517
|
* hoists document metadata into <head>. Nothing needs injecting from headers.
|
|
501
518
|
*/
|
|
502
519
|
function deserializeResponse(response) {
|
|
503
|
-
return
|
|
520
|
+
return deserializeStream(response.body);
|
|
521
|
+
}
|
|
522
|
+
function deserializeStream(stream) {
|
|
523
|
+
return flightDeserializer(stream, {
|
|
504
524
|
callServer: callServerFn ??
|
|
505
525
|
(async () => {
|
|
506
526
|
throw new Error("Server actions not initialized");
|
|
@@ -578,8 +598,7 @@ export async function navigate(url, opts) {
|
|
|
578
598
|
// advice. Not for a restore: going back to a page still held asks the
|
|
579
599
|
// server for nothing.
|
|
580
600
|
if (!opts?.restore && isUpdated()) {
|
|
581
|
-
|
|
582
|
-
window.location.href = url;
|
|
601
|
+
loadDocument(url, "newer-build", opts?.replace);
|
|
583
602
|
return;
|
|
584
603
|
}
|
|
585
604
|
if (redirectsFollowed > MAX_REDIRECTS) {
|
|
@@ -587,15 +606,15 @@ export async function navigate(url, opts) {
|
|
|
587
606
|
}
|
|
588
607
|
// External URLs can't be fetched (CORS) — go directly to full page navigation
|
|
589
608
|
if (isExternalUrl(url)) {
|
|
590
|
-
|
|
591
|
-
window.location.href = url;
|
|
609
|
+
loadDocument(url, "external", opts?.replace);
|
|
592
610
|
return;
|
|
593
611
|
}
|
|
594
612
|
// A route.ts answers with a Response, not a page: a download, a redirect
|
|
595
613
|
// that decides where someone belongs, a sign-out. The browser goes there.
|
|
614
|
+
// So does a url the app named as its backend's, which may bounce to another
|
|
615
|
+
// site - an OAuth start - that a fetch cannot follow.
|
|
596
616
|
if (isApiRoute(url)) {
|
|
597
|
-
|
|
598
|
-
window.location.href = url;
|
|
617
|
+
loadDocument(url, "api-route", opts?.replace);
|
|
599
618
|
return;
|
|
600
619
|
}
|
|
601
620
|
// Hash-only URLs — let the browser handle scrolling natively
|
|
@@ -713,7 +732,6 @@ export async function navigate(url, opts) {
|
|
|
713
732
|
// open over the page behind it.
|
|
714
733
|
const usable = isUsable(cached, chain);
|
|
715
734
|
if (usable) {
|
|
716
|
-
treePromise = cached.tree;
|
|
717
735
|
// Read after the tree resolves, not now. A prefetch fills these in when
|
|
718
736
|
// its response lands, and a click can land first — hover a modal link
|
|
719
737
|
// and click it quickly and the entry still says depth 0, so the
|
|
@@ -722,6 +740,26 @@ export async function navigate(url, opts) {
|
|
|
722
740
|
// waiting for it is what makes them true.
|
|
723
741
|
reused = cached;
|
|
724
742
|
cache.delete(cacheKey);
|
|
743
|
+
if (reused.settled) {
|
|
744
|
+
treePromise = reused.tree;
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
// Still arriving, so read it as it comes - see follow(). The request
|
|
748
|
+
// is this navigation's from here: leaving the link, or an action
|
|
749
|
+
// dropping the other prefetches, must not cut short a page the
|
|
750
|
+
// visitor is waiting on, and being overtaken by another navigation
|
|
751
|
+
// must stop it as it would a request of its own.
|
|
752
|
+
const arriving = reused;
|
|
753
|
+
prefetchControllers.delete(cacheKey);
|
|
754
|
+
controller.signal.addEventListener("abort", () => arriving.cancel(), { once: true });
|
|
755
|
+
// A request that dies is asked again, as a prefetch that died before
|
|
756
|
+
// the click is; the entry says which it was once it has settled.
|
|
757
|
+
treePromise = arriving.follow().catch((error) => arriving.body.then(() => {
|
|
758
|
+
if (arriving.failed)
|
|
759
|
+
return null;
|
|
760
|
+
throw error;
|
|
761
|
+
}));
|
|
762
|
+
}
|
|
725
763
|
}
|
|
726
764
|
else {
|
|
727
765
|
cache.delete(cacheKey);
|
|
@@ -762,8 +800,7 @@ export async function navigate(url, opts) {
|
|
|
762
800
|
const contentType = response.headers.get("Content-Type") ?? "";
|
|
763
801
|
if (staticPayloadSuffix === null &&
|
|
764
802
|
!contentType.includes("text/x-component")) {
|
|
765
|
-
|
|
766
|
-
window.location.href = url;
|
|
803
|
+
loadDocument(url, `not-a-payload:${response.status}:${contentType.split(";")[0]}`, opts?.replace);
|
|
767
804
|
return;
|
|
768
805
|
}
|
|
769
806
|
// Headers on a server; worked out locally on a static host, where there
|
|
@@ -850,11 +887,12 @@ export async function navigate(url, opts) {
|
|
|
850
887
|
// moved: the entry left behind can never be gone back to, and mounting a
|
|
851
888
|
// new one showed every fallback in the page - a new chat's first message
|
|
852
889
|
// redirects /chat to /chat?c=25 this way. A push keeps its own page, so
|
|
853
|
-
// Back still restores the one it came from
|
|
854
|
-
|
|
890
|
+
// Back still restores the one it came from - except a search on the page
|
|
891
|
+
// it is on, which asks for the same place and keeps the old page behind it.
|
|
892
|
+
const inPlaceOf = (opts?.replace || opts?.inPlace) && !interceptSlot && new URL(url, window.location.origin).pathname === new URL(previousUrl, window.location.origin).pathname
|
|
855
893
|
? retentionKey(previousUrl, null)
|
|
856
894
|
: null;
|
|
857
|
-
onNavigate?.(tree, activityKey, segmentDepth, inPlaceOf);
|
|
895
|
+
onNavigate?.(tree, activityKey, segmentDepth, inPlaceOf, !opts?.replace);
|
|
858
896
|
if (!opts?.preserveScroll && !interceptSlot) {
|
|
859
897
|
// Wait for React to commit the DOM update before scrolling.
|
|
860
898
|
// Intercepted navigations preserve scroll (e.g. modal over current page).
|
|
@@ -871,8 +909,7 @@ export async function navigate(url, opts) {
|
|
|
871
909
|
// A chunk the deploy no longer serves: the browser would have loaded the
|
|
872
910
|
// document, and the new names with it. Do what it would have done.
|
|
873
911
|
if (isStaleAssetError(err)) {
|
|
874
|
-
|
|
875
|
-
window.location.href = url;
|
|
912
|
+
loadDocument(url, `stale-asset:${String(err?.message ?? err).slice(0, 120)}`, opts?.replace);
|
|
876
913
|
return;
|
|
877
914
|
}
|
|
878
915
|
// Without this a navigation that fails does nothing observable: the click
|
|
@@ -1172,6 +1209,11 @@ function warm(entry, cacheKey, followed = new Set()) {
|
|
|
1172
1209
|
return;
|
|
1173
1210
|
// The page about to show: its pictures ahead of every other page's.
|
|
1174
1211
|
preloadImages(text, "high");
|
|
1212
|
+
// Taken by a click before it had all arrived, which read it as it came:
|
|
1213
|
+
// the page is on its way to the screen, and decoding it again here would
|
|
1214
|
+
// be the same chunks asked for twice.
|
|
1215
|
+
if (cache.get(cacheKey) !== entry)
|
|
1216
|
+
return;
|
|
1175
1217
|
return entry.tree.then((tree) => {
|
|
1176
1218
|
if (entry.slot !== null ||
|
|
1177
1219
|
entry.segmentDepth === 0 ||
|
|
@@ -1200,11 +1242,24 @@ function prefetchUrl(cacheKey, url, ttl, interceptSlot, refererUrl, held = { int
|
|
|
1200
1242
|
const controller = new AbortController();
|
|
1201
1243
|
prefetchControllers.set(cacheKey, controller);
|
|
1202
1244
|
let decoded = null;
|
|
1245
|
+
// Settled once the response has said whether there is a page to follow: its
|
|
1246
|
+
// bytes as they arrive, or nothing for a redirect or a failure.
|
|
1247
|
+
let arriving;
|
|
1248
|
+
let followed = false;
|
|
1249
|
+
const followable = new Promise((resolve) => {
|
|
1250
|
+
arriving = resolve;
|
|
1251
|
+
});
|
|
1203
1252
|
const entry = {
|
|
1204
1253
|
body: Promise.resolve(null),
|
|
1205
1254
|
get tree() {
|
|
1206
1255
|
return (decoded ??= this.body.then((text) => (text === null ? null : deserializeResponse(new Response(text)))));
|
|
1207
1256
|
},
|
|
1257
|
+
settled: false,
|
|
1258
|
+
follow() {
|
|
1259
|
+
followed = true;
|
|
1260
|
+
return followable.then((stream) => (stream ? deserializeStream(stream) : null));
|
|
1261
|
+
},
|
|
1262
|
+
cancel: () => controller.abort(),
|
|
1208
1263
|
expiresAt: Date.now() + ttl,
|
|
1209
1264
|
segmentDepth: 0,
|
|
1210
1265
|
layouts: null,
|
|
@@ -1237,6 +1292,7 @@ function prefetchUrl(cacheKey, url, ttl, interceptSlot, refererUrl, held = { int
|
|
|
1237
1292
|
entry.redirectTo = redirectTo;
|
|
1238
1293
|
if (!isExternalUrl(redirectTo) && isSafeRedirect(redirectTo))
|
|
1239
1294
|
prefetch(redirectTo, ttl);
|
|
1295
|
+
arriving(null);
|
|
1240
1296
|
return null;
|
|
1241
1297
|
}
|
|
1242
1298
|
entry.slot = response.headers.get("X-RSC-Revalidate");
|
|
@@ -1261,7 +1317,16 @@ function prefetchUrl(cacheKey, url, ttl, interceptSlot, refererUrl, held = { int
|
|
|
1261
1317
|
// - unless the visitor is already on the way, see prefetch(). The
|
|
1262
1318
|
// pictures the page shows are asked for now, though: on a phone they
|
|
1263
1319
|
// take longer than the touch-to-click a hidden render has.
|
|
1320
|
+
//
|
|
1321
|
+
// Read twice: whole, for the cache, and from the first byte by a click
|
|
1322
|
+
// that lands before the last of them - see follow(). The second read is
|
|
1323
|
+
// let go as soon as the first is complete and nobody took it.
|
|
1324
|
+
const copy = response.clone().body;
|
|
1325
|
+
arriving(copy);
|
|
1264
1326
|
return response.text().then((text) => {
|
|
1327
|
+
entry.settled = true;
|
|
1328
|
+
if (!followed)
|
|
1329
|
+
copy?.cancel();
|
|
1265
1330
|
preloadImages(text);
|
|
1266
1331
|
// And the code for its client components, so the first visit to a
|
|
1267
1332
|
// page does not wait on a chunk after it has arrived.
|
|
@@ -1272,6 +1337,7 @@ function prefetchUrl(cacheKey, url, ttl, interceptSlot, refererUrl, held = { int
|
|
|
1272
1337
|
.catch(() => {
|
|
1273
1338
|
entry.failed = true;
|
|
1274
1339
|
cache.delete(cacheKey);
|
|
1340
|
+
arriving(null);
|
|
1275
1341
|
return null;
|
|
1276
1342
|
})
|
|
1277
1343
|
.finally(() => {
|