@rangojs/router 0.34.0 → 0.34.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/rango.js +1 -1
- package/dist/bin/{runtime-discovery-2ZTYRZ3V.js → runtime-discovery-RY6NMQM3.js} +1 -1
- package/dist/bin/{temp-rsc-server-EUNKRFJD.js → temp-rsc-server-F7AHOPV4.js} +1 -1
- package/dist/types/loader-store.d.ts +19 -7
- package/dist/types/router/match-context.d.ts +12 -0
- package/dist/types/router/navigation-snapshot.d.ts +17 -0
- package/dist/vite/index.js +1 -1
- package/package.json +1 -1
- package/skills/hooks/data.md +4 -1
- package/src/loader-store.ts +28 -12
- package/src/router/match-api.ts +2 -0
- package/src/router/match-context.ts +22 -1
- package/src/router/match-middleware/cache-lookup.ts +17 -8
- package/src/router/match-middleware/intercept-resolution.ts +3 -3
- package/src/router/navigation-snapshot.ts +39 -8
- package/src/use-loader.tsx +5 -3
package/dist/bin/rango.js
CHANGED
|
@@ -229,7 +229,7 @@ ${formatNestedRouterConflictError(nestedRouterConflict, "[rango]")}
|
|
|
229
229
|
}
|
|
230
230
|
let discoverAndWriteRouteTypes;
|
|
231
231
|
try {
|
|
232
|
-
const mod = await import("./runtime-discovery-
|
|
232
|
+
const mod = await import("./runtime-discovery-RY6NMQM3.js");
|
|
233
233
|
discoverAndWriteRouteTypes = mod.discoverAndWriteRouteTypes;
|
|
234
234
|
} catch (err) {
|
|
235
235
|
if (err.code === "ERR_MODULE_NOT_FOUND" || err.code === "MODULE_NOT_FOUND") {
|
|
@@ -163,7 +163,7 @@ async function discoverAndWriteRouteTypes(opts) {
|
|
|
163
163
|
"Runtime discovery requires '@vitejs/plugin-rsc'. Install it with: pnpm add -D @vitejs/plugin-rsc"
|
|
164
164
|
);
|
|
165
165
|
}
|
|
166
|
-
const { createTempRscServer } = await import("./temp-rsc-server-
|
|
166
|
+
const { createTempRscServer } = await import("./temp-rsc-server-F7AHOPV4.js");
|
|
167
167
|
const { createDiscoveryState } = await import("./state-ACD2MGKJ.js");
|
|
168
168
|
const { discoverClientUrlProjections } = await import("./client-urls-projection-4Q5EQDAL.js");
|
|
169
169
|
const {
|
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
* cancelled on resubscribe so a StrictMode / transition remount does not
|
|
29
29
|
* reclaim a bucket that is about to be reused, and is held until any
|
|
30
30
|
* in-flight load settles.
|
|
31
|
+
* - Owned buckets (one reader's private refresh-group bucket,
|
|
32
|
+
* `useLoader(L, { refreshGroup })` without a key) are sticky while that
|
|
33
|
+
* reader is mounted and dropped like an ephemeral bucket once it leaves:
|
|
34
|
+
* no other reader names the bucket, and a remount takes a new one.
|
|
31
35
|
*
|
|
32
36
|
* Each router's NavigationStore owns one (navigation-store.ts), and hooks reach
|
|
33
37
|
* it through NavigationStoreContext: the router store is the single source of
|
|
@@ -79,6 +83,13 @@ export interface SubscribeOptions {
|
|
|
79
83
|
* subscriber attaches, and from then on resets via `clearFamily`.
|
|
80
84
|
*/
|
|
81
85
|
ephemeral?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* When true, the bucket belongs to this subscriber alone (a private
|
|
88
|
+
* refresh-group bucket, which no other reader names): it is reset by
|
|
89
|
+
* `clearFamily` while subscribed, and dropped after the subscriber leaves
|
|
90
|
+
* even when sticky. Read at unsubscribe, so pass it on every subscribe.
|
|
91
|
+
*/
|
|
92
|
+
owned?: boolean;
|
|
82
93
|
/**
|
|
83
94
|
* Cross-loader refresh group name(s). Tags this bucket so `refreshGroups(name)`
|
|
84
95
|
* can refresh it alongside buckets of other loaders. A bucket may be tagged
|
|
@@ -130,8 +141,8 @@ export declare class LoaderStore {
|
|
|
130
141
|
* Subscribe to entry changes for `bucketKey`.
|
|
131
142
|
* Returns an unsubscribe function. A sticky bucket's entry is kept around even
|
|
132
143
|
* after the last subscriber leaves so that an in-flight `load()` can still
|
|
133
|
-
* commit if the consumer remounts. An ephemeral bucket is dropped
|
|
134
|
-
* last subscriber leaves (deferred, see maybeScheduleRefcountClear).
|
|
144
|
+
* commit if the consumer remounts. An ephemeral or owned bucket is dropped
|
|
145
|
+
* once its last subscriber leaves (deferred, see maybeScheduleRefcountClear).
|
|
135
146
|
*/
|
|
136
147
|
subscribe(bucketKey: string, cb: () => void, options?: SubscribeOptions): () => void;
|
|
137
148
|
private addToGroup;
|
|
@@ -149,14 +160,14 @@ export declare class LoaderStore {
|
|
|
149
160
|
*/
|
|
150
161
|
refreshGroups(groups: string | string[]): Promise<void>;
|
|
151
162
|
/**
|
|
152
|
-
* Schedule a deferred drop for an ad-hoc (never-sticky)
|
|
153
|
-
* subscriber just left. Deferred to a microtask so a StrictMode /
|
|
154
|
-
* remount that resubscribes in the same tick cancels it.
|
|
163
|
+
* Schedule a deferred drop for an ad-hoc (never-sticky) or owned bucket
|
|
164
|
+
* whose last subscriber just left. Deferred to a microtask so a StrictMode /
|
|
165
|
+
* transition remount that resubscribes in the same tick cancels it.
|
|
155
166
|
*/
|
|
156
167
|
private maybeScheduleRefcountClear;
|
|
157
168
|
/** Remove a bucket entry entirely and prune it from its family/group indexes. */
|
|
158
169
|
private dropBucket;
|
|
159
|
-
/** Drop an ephemeral bucket that settled with no subscribers left. */
|
|
170
|
+
/** Drop an ephemeral or owned bucket that settled with no subscribers left. */
|
|
160
171
|
private dropIfSettled;
|
|
161
172
|
/**
|
|
162
173
|
* Returns the current snapshot for `bucketKey`. Stable reference between
|
|
@@ -205,7 +216,8 @@ export declare class LoaderStore {
|
|
|
205
216
|
* Reset a single bucket entry. Bumps `latestRequestId` so any in-flight
|
|
206
217
|
* `load()` whose promise is still pending will fail its gate when it resolves
|
|
207
218
|
* and be dropped — prevents pre-navigation loads from clobbering the new
|
|
208
|
-
* route's context. The entry itself is kept (sticky-bucket semantics)
|
|
219
|
+
* route's context. The entry itself is kept (sticky-bucket semantics),
|
|
220
|
+
* unless it was only waiting for that request to be dropped.
|
|
209
221
|
*/
|
|
210
222
|
clear(bucketKey: string): void;
|
|
211
223
|
/**
|
|
@@ -112,6 +112,7 @@ import type { CacheScope } from "../cache/cache-scope.js";
|
|
|
112
112
|
import type { EntryData, InterceptSelectorContext, MetricsStore } from "../server/context.js";
|
|
113
113
|
import type { HandlerContext, ResolvedSegment } from "../types.js";
|
|
114
114
|
import type { RouteMatchResult } from "./pattern-matching.js";
|
|
115
|
+
import type { PrevPage } from "./navigation-snapshot.js";
|
|
115
116
|
import type { InterceptResult } from "./router-context.js";
|
|
116
117
|
import type { ResolutionPassInput } from "./segment-resolution/resolution-pass.js";
|
|
117
118
|
import type { ActionContext } from "./types.js";
|
|
@@ -130,7 +131,10 @@ export interface MatchContext<TEnv = any> {
|
|
|
130
131
|
heldBoundary?: string;
|
|
131
132
|
prevUrl: URL;
|
|
132
133
|
prevParams: Record<string, string>;
|
|
134
|
+
/** The match of the page left: the intercept's URL while one is open. */
|
|
133
135
|
prevMatch: RouteMatchResult | null;
|
|
136
|
+
/** The page an open intercept's slot was rendered for. */
|
|
137
|
+
interceptPrev: PrevPage;
|
|
134
138
|
matched: RouteMatchResult;
|
|
135
139
|
manifestEntry: EntryData;
|
|
136
140
|
entries: EntryData[];
|
|
@@ -169,6 +173,14 @@ export interface MatchContext<TEnv = any> {
|
|
|
169
173
|
export declare function rendersLive(ctx: Pick<MatchContext, "isAction" | "stale">): boolean;
|
|
170
174
|
/** This match as the input of a revalidating resolution pass (resolution-pass.ts). */
|
|
171
175
|
export declare function resolutionPassInput<TEnv>(ctx: MatchContext<TEnv>): ResolutionPassInput<TEnv>;
|
|
176
|
+
/**
|
|
177
|
+
* This match as the input of the pass that resolves its intercept's slot: its
|
|
178
|
+
* held segments compare with the intercept's own URL (`interceptPrev`), not
|
|
179
|
+
* with the source page under it. From /item/2 over /list/1, a link to /item/1
|
|
180
|
+
* compared with the source's params would read as param-equal and keep item
|
|
181
|
+
* 2's slot.
|
|
182
|
+
*/
|
|
183
|
+
export declare function interceptPassInput<TEnv>(ctx: MatchContext<TEnv>): ResolutionPassInput<TEnv>;
|
|
172
184
|
/**
|
|
173
185
|
* Mutable state that flows through the pipeline
|
|
174
186
|
*/
|
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
import type { RouteMatchResult } from "./pattern-matching.js";
|
|
2
|
+
/** A page held segments were rendered for: what their revalidation compares with. */
|
|
3
|
+
export interface PrevPage {
|
|
4
|
+
url: URL;
|
|
5
|
+
params: Record<string, string>;
|
|
6
|
+
}
|
|
2
7
|
export interface NavigationSnapshot {
|
|
8
|
+
/**
|
|
9
|
+
* `prevUrl` and `prevParams`: the page the held page segments were rendered
|
|
10
|
+
* for. That is the page left, or the source page while an intercept is open
|
|
11
|
+
* (`hasInterceptSource`).
|
|
12
|
+
*/
|
|
3
13
|
prevUrl: URL;
|
|
4
14
|
/** The visitor's URL of the page left, when `prevUrl` is its rewrite target. */
|
|
5
15
|
prevVisitorUrl: URL | null;
|
|
6
16
|
prevParams: Record<string, string>;
|
|
17
|
+
/** The match of the page left: the intercept's URL while one is open. */
|
|
7
18
|
prevMatch: RouteMatchResult | null;
|
|
19
|
+
/**
|
|
20
|
+
* The page an open intercept's slot was rendered for: the intercept's own
|
|
21
|
+
* URL (match-context.ts interceptPassInput). Equals `prevUrl`/`prevParams`
|
|
22
|
+
* when the request names no intercept source.
|
|
23
|
+
*/
|
|
24
|
+
interceptPrev: PrevPage;
|
|
8
25
|
interceptContextUrl: URL;
|
|
9
26
|
interceptContextMatch: RouteMatchResult | null;
|
|
10
27
|
clientSegmentIds: string[];
|
package/dist/vite/index.js
CHANGED
|
@@ -7344,7 +7344,7 @@ function resolveVersionsFrom(table, routerId2) {
|
|
|
7344
7344
|
// package.json
|
|
7345
7345
|
var package_default = {
|
|
7346
7346
|
name: "@rangojs/router",
|
|
7347
|
-
version: "0.34.
|
|
7347
|
+
version: "0.34.1",
|
|
7348
7348
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
7349
7349
|
keywords: [
|
|
7350
7350
|
"react",
|
package/package.json
CHANGED
package/skills/hooks/data.md
CHANGED
|
@@ -261,7 +261,10 @@ tag (`"account"`) covers everything, while a finer tag (`"orders"`) targets a
|
|
|
261
261
|
subset. Sharing within a group is opt-in via `key`: members that share a `key`
|
|
262
262
|
share one value (and one fetch); a grouped reader **without** a `key` gets its own
|
|
263
263
|
private bucket, so a group refresh updates only that read and never leaks into
|
|
264
|
-
unrelated unkeyed reads of the same loader.
|
|
264
|
+
unrelated unkeyed reads of the same loader. That private bucket lives as long as
|
|
265
|
+
its read is mounted, and a hidden React `<Activity>` counts as unmounted: when it
|
|
266
|
+
shows again, the read is back on the route's data until the next refresh. To keep
|
|
267
|
+
a refreshed value across an `<Activity>` hide, give the read a `key`. A bucket may belong to several groups
|
|
265
268
|
at once (one read tagged with multiple names, or different reads tagging the same
|
|
266
269
|
keyed bucket with different names). Keep parameterized loaders on the single-loader
|
|
267
270
|
`key` — a plain-GET group refresh sends no params.
|
package/src/loader-store.ts
CHANGED
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
* cancelled on resubscribe so a StrictMode / transition remount does not
|
|
29
29
|
* reclaim a bucket that is about to be reused, and is held until any
|
|
30
30
|
* in-flight load settles.
|
|
31
|
+
* - Owned buckets (one reader's private refresh-group bucket,
|
|
32
|
+
* `useLoader(L, { refreshGroup })` without a key) are sticky while that
|
|
33
|
+
* reader is mounted and dropped like an ephemeral bucket once it leaves:
|
|
34
|
+
* no other reader names the bucket, and a remount takes a new one.
|
|
31
35
|
*
|
|
32
36
|
* Each router's NavigationStore owns one (navigation-store.ts), and hooks reach
|
|
33
37
|
* it through NavigationStoreContext: the router store is the single source of
|
|
@@ -97,6 +101,13 @@ export interface SubscribeOptions {
|
|
|
97
101
|
* subscriber attaches, and from then on resets via `clearFamily`.
|
|
98
102
|
*/
|
|
99
103
|
ephemeral?: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* When true, the bucket belongs to this subscriber alone (a private
|
|
106
|
+
* refresh-group bucket, which no other reader names): it is reset by
|
|
107
|
+
* `clearFamily` while subscribed, and dropped after the subscriber leaves
|
|
108
|
+
* even when sticky. Read at unsubscribe, so pass it on every subscribe.
|
|
109
|
+
*/
|
|
110
|
+
owned?: boolean;
|
|
100
111
|
/**
|
|
101
112
|
* Cross-loader refresh group name(s). Tags this bucket so `refreshGroups(name)`
|
|
102
113
|
* can refresh it alongside buckets of other loaders. A bucket may be tagged
|
|
@@ -130,8 +141,8 @@ interface InternalEntry {
|
|
|
130
141
|
/** Family id (loader.$$id) this bucket belongs to. */
|
|
131
142
|
loaderId: string;
|
|
132
143
|
/**
|
|
133
|
-
* True once any non-ephemeral subscriber has attached. Sticky buckets
|
|
134
|
-
*
|
|
144
|
+
* True once any non-ephemeral subscriber has attached. Sticky buckets reset
|
|
145
|
+
* via clearFamily() and are reference-count-dropped only when owned.
|
|
135
146
|
*/
|
|
136
147
|
sticky: boolean;
|
|
137
148
|
/** A deferred refcount-drop microtask is scheduled and not yet cancelled. */
|
|
@@ -223,8 +234,8 @@ export class LoaderStore {
|
|
|
223
234
|
* Subscribe to entry changes for `bucketKey`.
|
|
224
235
|
* Returns an unsubscribe function. A sticky bucket's entry is kept around even
|
|
225
236
|
* after the last subscriber leaves so that an in-flight `load()` can still
|
|
226
|
-
* commit if the consumer remounts. An ephemeral bucket is dropped
|
|
227
|
-
* last subscriber leaves (deferred, see maybeScheduleRefcountClear).
|
|
237
|
+
* commit if the consumer remounts. An ephemeral or owned bucket is dropped
|
|
238
|
+
* once its last subscriber leaves (deferred, see maybeScheduleRefcountClear).
|
|
228
239
|
*/
|
|
229
240
|
subscribe(
|
|
230
241
|
bucketKey: string,
|
|
@@ -261,7 +272,7 @@ export class LoaderStore {
|
|
|
261
272
|
// different groups stays in each group until ALL of that group's
|
|
262
273
|
// subscribers have left (order-independent).
|
|
263
274
|
for (const group of groups) this.releaseGroup(group, bucketKey, e);
|
|
264
|
-
this.maybeScheduleRefcountClear(bucketKey, e);
|
|
275
|
+
this.maybeScheduleRefcountClear(bucketKey, e, options?.owned);
|
|
265
276
|
};
|
|
266
277
|
}
|
|
267
278
|
|
|
@@ -354,17 +365,18 @@ export class LoaderStore {
|
|
|
354
365
|
}
|
|
355
366
|
|
|
356
367
|
/**
|
|
357
|
-
* Schedule a deferred drop for an ad-hoc (never-sticky)
|
|
358
|
-
* subscriber just left. Deferred to a microtask so a StrictMode /
|
|
359
|
-
* remount that resubscribes in the same tick cancels it.
|
|
368
|
+
* Schedule a deferred drop for an ad-hoc (never-sticky) or owned bucket
|
|
369
|
+
* whose last subscriber just left. Deferred to a microtask so a StrictMode /
|
|
370
|
+
* transition remount that resubscribes in the same tick cancels it.
|
|
360
371
|
*/
|
|
361
372
|
private maybeScheduleRefcountClear(
|
|
362
373
|
bucketKey: string,
|
|
363
374
|
e: InternalEntry,
|
|
375
|
+
owned: boolean | undefined,
|
|
364
376
|
): void {
|
|
365
377
|
if (e.listeners.size > 0) return;
|
|
366
|
-
// Sticky buckets persist for remount; they reset via clearFamily().
|
|
367
|
-
if (e.sticky) return;
|
|
378
|
+
// Sticky buckets persist for remount unless owned; they reset via clearFamily().
|
|
379
|
+
if (e.sticky && !owned) return;
|
|
368
380
|
if (e.pendingClear) return;
|
|
369
381
|
e.pendingClear = true;
|
|
370
382
|
queueMicrotask(() => {
|
|
@@ -393,7 +405,7 @@ export class LoaderStore {
|
|
|
393
405
|
}
|
|
394
406
|
}
|
|
395
407
|
|
|
396
|
-
/** Drop an ephemeral bucket that settled with no subscribers left. */
|
|
408
|
+
/** Drop an ephemeral or owned bucket that settled with no subscribers left. */
|
|
397
409
|
private dropIfSettled(bucketKey: string, e: InternalEntry): void {
|
|
398
410
|
if (e.clearWhenSettled && e.listeners.size === 0) {
|
|
399
411
|
e.clearWhenSettled = false;
|
|
@@ -523,7 +535,8 @@ export class LoaderStore {
|
|
|
523
535
|
* Reset a single bucket entry. Bumps `latestRequestId` so any in-flight
|
|
524
536
|
* `load()` whose promise is still pending will fail its gate when it resolves
|
|
525
537
|
* and be dropped — prevents pre-navigation loads from clobbering the new
|
|
526
|
-
* route's context. The entry itself is kept (sticky-bucket semantics)
|
|
538
|
+
* route's context. The entry itself is kept (sticky-bucket semantics),
|
|
539
|
+
* unless it was only waiting for that request to be dropped.
|
|
527
540
|
*/
|
|
528
541
|
clear(bucketKey: string): void {
|
|
529
542
|
const e = this.entries.get(bucketKey);
|
|
@@ -532,6 +545,9 @@ export class LoaderStore {
|
|
|
532
545
|
if (e.snapshot === EMPTY_SNAPSHOT) return;
|
|
533
546
|
e.snapshot = EMPTY_SNAPSHOT;
|
|
534
547
|
this.notify(e);
|
|
548
|
+
// An owned bucket left mid-flight waits for its request to settle, which
|
|
549
|
+
// the bump above stops from committing.
|
|
550
|
+
this.dropIfSettled(bucketKey, e);
|
|
535
551
|
}
|
|
536
552
|
|
|
537
553
|
/**
|
package/src/router/match-api.ts
CHANGED
|
@@ -203,6 +203,7 @@ export async function createMatchContextForFull<TEnv>(
|
|
|
203
203
|
prevUrl: cleanUrl,
|
|
204
204
|
prevParams: {},
|
|
205
205
|
prevMatch: null,
|
|
206
|
+
interceptPrev: { url: cleanUrl, params: {} },
|
|
206
207
|
matched,
|
|
207
208
|
manifestEntry: snapshot.manifestEntry,
|
|
208
209
|
entries: snapshot.entries,
|
|
@@ -520,6 +521,7 @@ export async function createMatchContextForPartial<TEnv>(
|
|
|
520
521
|
prevUrl: nav.prevUrl,
|
|
521
522
|
prevParams: nav.prevParams,
|
|
522
523
|
prevMatch: nav.prevMatch,
|
|
524
|
+
interceptPrev: nav.interceptPrev,
|
|
523
525
|
matched,
|
|
524
526
|
manifestEntry: snapshot.manifestEntry,
|
|
525
527
|
entries: snapshot.entries,
|
|
@@ -116,6 +116,7 @@ import type {
|
|
|
116
116
|
} from "../server/context.js";
|
|
117
117
|
import type { HandlerContext, ResolvedSegment } from "../types.js";
|
|
118
118
|
import type { RouteMatchResult } from "./pattern-matching.js";
|
|
119
|
+
import type { PrevPage } from "./navigation-snapshot.js";
|
|
119
120
|
import type { InterceptResult } from "./router-context.js";
|
|
120
121
|
import type { ResolutionPassInput } from "./segment-resolution/resolution-pass.js";
|
|
121
122
|
import type { ActionContext } from "./types.js";
|
|
@@ -139,10 +140,13 @@ export interface MatchContext<TEnv = any> {
|
|
|
139
140
|
/** NavigationSnapshot.heldBoundary: the walk stops above it. */
|
|
140
141
|
heldBoundary?: string;
|
|
141
142
|
|
|
142
|
-
// Previous navigation state
|
|
143
|
+
// Previous navigation state: see NavigationSnapshot.
|
|
143
144
|
prevUrl: URL;
|
|
144
145
|
prevParams: Record<string, string>;
|
|
146
|
+
/** The match of the page left: the intercept's URL while one is open. */
|
|
145
147
|
prevMatch: RouteMatchResult | null;
|
|
148
|
+
/** The page an open intercept's slot was rendered for. */
|
|
149
|
+
interceptPrev: PrevPage;
|
|
146
150
|
|
|
147
151
|
// Current route match
|
|
148
152
|
matched: RouteMatchResult;
|
|
@@ -225,6 +229,23 @@ export function resolutionPassInput<TEnv>(
|
|
|
225
229
|
};
|
|
226
230
|
}
|
|
227
231
|
|
|
232
|
+
/**
|
|
233
|
+
* This match as the input of the pass that resolves its intercept's slot: its
|
|
234
|
+
* held segments compare with the intercept's own URL (`interceptPrev`), not
|
|
235
|
+
* with the source page under it. From /item/2 over /list/1, a link to /item/1
|
|
236
|
+
* compared with the source's params would read as param-equal and keep item
|
|
237
|
+
* 2's slot.
|
|
238
|
+
*/
|
|
239
|
+
export function interceptPassInput<TEnv>(
|
|
240
|
+
ctx: MatchContext<TEnv>,
|
|
241
|
+
): ResolutionPassInput<TEnv> {
|
|
242
|
+
return {
|
|
243
|
+
...resolutionPassInput(ctx),
|
|
244
|
+
prevUrl: ctx.interceptPrev.url,
|
|
245
|
+
prevParams: ctx.interceptPrev.params,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
228
249
|
/**
|
|
229
250
|
* Mutable state that flows through the pipeline
|
|
230
251
|
*/
|
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
type MatchPipelineState,
|
|
50
50
|
} from "../match-context.js";
|
|
51
51
|
import { getRouterContext, type RouterContext } from "../router-context.js";
|
|
52
|
+
import type { PrevPage } from "../navigation-snapshot.js";
|
|
52
53
|
import { freshPassInput } from "../segment-resolution/resolution-pass.js";
|
|
53
54
|
import { observeEvent } from "../instrument.js";
|
|
54
55
|
import { pushRevalidationTraceEntry, isTraceActive } from "../logging.js";
|
|
@@ -336,7 +337,12 @@ function storedEntrySends<TEnv>(
|
|
|
336
337
|
router: RouterContext<TEnv>,
|
|
337
338
|
): (segment: ResolvedSegment) => boolean | Promise<boolean> {
|
|
338
339
|
if (ctx.isFullMatch) return () => true;
|
|
339
|
-
|
|
340
|
+
// An intercept variant's slot compares with the intercept's own page, the
|
|
341
|
+
// page under it with the source (MatchContext.interceptPrev).
|
|
342
|
+
const pagePrev: PrevPage = { url: ctx.prevUrl, params: ctx.prevParams };
|
|
343
|
+
const slotPrev = ctx.interceptPrev;
|
|
344
|
+
const pageParamsChanged = !paramsEqual(ctx.matched.params, pagePrev.params);
|
|
345
|
+
const slotParamsChanged = !paramsEqual(ctx.matched.params, slotPrev.params);
|
|
340
346
|
const resendRouteSegments = replaceable && (ctx.isAction || ctx.stale);
|
|
341
347
|
const entryRevalidateMap =
|
|
342
348
|
ctx.clientSegmentSet.size > 0
|
|
@@ -345,11 +351,11 @@ function storedEntrySends<TEnv>(
|
|
|
345
351
|
// A keep that hangs on the source's params is right for the pages with
|
|
346
352
|
// the same params only: scope a prefetch of the answer to them, as a
|
|
347
353
|
// predicate that compared every param would (#1331).
|
|
348
|
-
const heldForSource = (): false => {
|
|
354
|
+
const heldForSource = (prevParams: Record<string, string>): false => {
|
|
349
355
|
markSourceHeld(
|
|
350
356
|
ctx.handlerContext as InternalHandlerContext<any, TEnv>,
|
|
351
|
-
readsOfAllParams(
|
|
352
|
-
{ params:
|
|
357
|
+
readsOfAllParams(prevParams),
|
|
358
|
+
{ params: prevParams, routeName: undefined },
|
|
353
359
|
);
|
|
354
360
|
return false;
|
|
355
361
|
};
|
|
@@ -362,23 +368,26 @@ function storedEntrySends<TEnv>(
|
|
|
362
368
|
ctx.clientSegmentSet,
|
|
363
369
|
);
|
|
364
370
|
if (fill !== undefined) return fill;
|
|
371
|
+
const ofSlot = isInterceptSegment(segment);
|
|
372
|
+
const prev = ofSlot ? slotPrev : pagePrev;
|
|
373
|
+
const paramsChanged = ofSlot ? slotParamsChanged : pageParamsChanged;
|
|
365
374
|
if (segment.type === "route" || segment.belongsToRoute === true) {
|
|
366
375
|
if (paramsChanged || resendRouteSegments) return true;
|
|
367
376
|
// Kept on equal params, whatever the search: a source with other
|
|
368
377
|
// params is sent it.
|
|
369
|
-
return heldForSource();
|
|
378
|
+
return heldForSource(prev.params);
|
|
370
379
|
}
|
|
371
380
|
const revalidate = entryRevalidateMap?.get(segment.id)?.revalidate;
|
|
372
381
|
if (!revalidate?.length) return false;
|
|
373
382
|
// Kept on equal params: from another source the rules run and may send
|
|
374
383
|
// it.
|
|
375
|
-
if (!paramsChanged) return heldForSource();
|
|
384
|
+
if (!paramsChanged) return heldForSource(prev.params);
|
|
376
385
|
return router.evaluateRevalidation({
|
|
377
386
|
segment,
|
|
378
|
-
prevParams:
|
|
387
|
+
prevParams: prev.params,
|
|
379
388
|
getPrevSegment: null,
|
|
380
389
|
request: ctx.request,
|
|
381
|
-
prevUrl:
|
|
390
|
+
prevUrl: prev.url,
|
|
382
391
|
nextUrl: ctx.url,
|
|
383
392
|
revalidations: revalidate.map((fn, i) => ({
|
|
384
393
|
name: `revalidate${i}`,
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
*/
|
|
104
104
|
import type { ResolvedSegment } from "../../types.js";
|
|
105
105
|
import type { MatchContext, MatchPipelineState } from "../match-context.js";
|
|
106
|
-
import {
|
|
106
|
+
import { interceptPassInput } from "../match-context.js";
|
|
107
107
|
import { getRouterContext } from "../router-context.js";
|
|
108
108
|
import type { GeneratorMiddleware } from "./cache-lookup.js";
|
|
109
109
|
import { debugLog } from "../logging.js";
|
|
@@ -176,7 +176,7 @@ export function withInterceptResolution<TEnv>(
|
|
|
176
176
|
const Store = ctx.Store;
|
|
177
177
|
const interceptSegments = await Store.run(() =>
|
|
178
178
|
resolveInterceptEntry(
|
|
179
|
-
|
|
179
|
+
interceptPassInput(ctx),
|
|
180
180
|
ctx.interceptResult!.intercept,
|
|
181
181
|
ctx.interceptResult!.entry,
|
|
182
182
|
true, // belongsToRoute
|
|
@@ -232,7 +232,7 @@ async function handleCacheHitIntercept<TEnv>(
|
|
|
232
232
|
const loaderSegments = resolveInterceptLoadersOnly
|
|
233
233
|
? await ctx.Store.run(() =>
|
|
234
234
|
resolveInterceptLoadersOnly(
|
|
235
|
-
|
|
235
|
+
interceptPassInput(ctx),
|
|
236
236
|
ctx.interceptResult!.intercept,
|
|
237
237
|
ctx.interceptResult!.entry,
|
|
238
238
|
true, // belongsToRoute
|
|
@@ -3,12 +3,30 @@ import { requestHeaders } from "../server/request-headers.js";
|
|
|
3
3
|
import type { RouteMatchResult } from "./pattern-matching.js";
|
|
4
4
|
import { WIRE } from "../wire-params.js";
|
|
5
5
|
|
|
6
|
+
/** A page held segments were rendered for: what their revalidation compares with. */
|
|
7
|
+
export interface PrevPage {
|
|
8
|
+
url: URL;
|
|
9
|
+
params: Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
|
|
6
12
|
export interface NavigationSnapshot {
|
|
13
|
+
/**
|
|
14
|
+
* `prevUrl` and `prevParams`: the page the held page segments were rendered
|
|
15
|
+
* for. That is the page left, or the source page while an intercept is open
|
|
16
|
+
* (`hasInterceptSource`).
|
|
17
|
+
*/
|
|
7
18
|
prevUrl: URL;
|
|
8
19
|
/** The visitor's URL of the page left, when `prevUrl` is its rewrite target. */
|
|
9
20
|
prevVisitorUrl: URL | null;
|
|
10
21
|
prevParams: Record<string, string>;
|
|
22
|
+
/** The match of the page left: the intercept's URL while one is open. */
|
|
11
23
|
prevMatch: RouteMatchResult | null;
|
|
24
|
+
/**
|
|
25
|
+
* The page an open intercept's slot was rendered for: the intercept's own
|
|
26
|
+
* URL (match-context.ts interceptPassInput). Equals `prevUrl`/`prevParams`
|
|
27
|
+
* when the request names no intercept source.
|
|
28
|
+
*/
|
|
29
|
+
interceptPrev: PrevPage;
|
|
12
30
|
|
|
13
31
|
interceptContextUrl: URL;
|
|
14
32
|
interceptContextMatch: RouteMatchResult | null;
|
|
@@ -176,29 +194,40 @@ export async function resolveNavigation(
|
|
|
176
194
|
}
|
|
177
195
|
// An intercept's request diffs from its source, which _rsc_rt does not name.
|
|
178
196
|
const shown = interceptSourceUrl ? null : getShownRouteUrl(url);
|
|
179
|
-
const
|
|
197
|
+
const left = shown ?? visitor;
|
|
180
198
|
|
|
181
199
|
let interceptContextUrl: URL;
|
|
182
200
|
try {
|
|
183
201
|
interceptContextUrl = interceptSourceUrl
|
|
184
202
|
? withoutHash(new URL(interceptSourceUrl, url.origin))
|
|
185
|
-
:
|
|
203
|
+
: left;
|
|
186
204
|
} catch {
|
|
187
|
-
interceptContextUrl =
|
|
205
|
+
interceptContextUrl = left;
|
|
188
206
|
}
|
|
189
207
|
|
|
190
|
-
const prevMatch = await deps.findMatch(
|
|
191
|
-
const prevParams = prevMatch?.params || {};
|
|
208
|
+
const prevMatch = await deps.findMatch(left.pathname);
|
|
192
209
|
const interceptContextMatch = interceptSourceUrl
|
|
193
210
|
? await deps.findMatch(interceptContextUrl.pathname)
|
|
194
211
|
: prevMatch;
|
|
212
|
+
const interceptPrev: PrevPage = {
|
|
213
|
+
url: left,
|
|
214
|
+
params: prevMatch?.params || {},
|
|
215
|
+
};
|
|
216
|
+
// The page under an open intercept was rendered for the source, with its
|
|
217
|
+
// search, not for the intercept's URL.
|
|
218
|
+
const prev: PrevPage = interceptSourceUrl
|
|
219
|
+
? {
|
|
220
|
+
url: formSearch(new URL(interceptContextUrl)),
|
|
221
|
+
params: interceptContextMatch?.params || {},
|
|
222
|
+
}
|
|
223
|
+
: interceptPrev;
|
|
195
224
|
|
|
196
225
|
const isSameRouteNavigation = !!(
|
|
197
226
|
interceptContextMatch && interceptContextMatch.routeKey === currentRouteKey
|
|
198
227
|
);
|
|
199
228
|
|
|
200
229
|
const hasInterceptSource = !!interceptSourceUrl;
|
|
201
|
-
const effectiveFromUrl = hasInterceptSource ? interceptContextUrl :
|
|
230
|
+
const effectiveFromUrl = hasInterceptSource ? interceptContextUrl : left;
|
|
202
231
|
const effectiveFromMatch = hasInterceptSource
|
|
203
232
|
? interceptContextMatch
|
|
204
233
|
: prevMatch;
|
|
@@ -212,10 +241,11 @@ export async function resolveNavigation(
|
|
|
212
241
|
const clientSegmentSet = new Set(clientSegmentIds);
|
|
213
242
|
|
|
214
243
|
return {
|
|
215
|
-
prevUrl,
|
|
244
|
+
prevUrl: prev.url,
|
|
216
245
|
prevVisitorUrl: shown && visitor,
|
|
217
|
-
prevParams,
|
|
246
|
+
prevParams: prev.params,
|
|
218
247
|
prevMatch,
|
|
248
|
+
interceptPrev,
|
|
219
249
|
interceptContextUrl,
|
|
220
250
|
interceptContextMatch,
|
|
221
251
|
clientSegmentIds,
|
|
@@ -240,6 +270,7 @@ export function createNavigationSnapshot(
|
|
|
240
270
|
prevVisitorUrl: null,
|
|
241
271
|
prevParams: {},
|
|
242
272
|
prevMatch: null,
|
|
273
|
+
interceptPrev: { url: defaultUrl, params: {} },
|
|
243
274
|
interceptContextUrl: defaultUrl,
|
|
244
275
|
interceptContextMatch: null,
|
|
245
276
|
clientSegmentIds: [],
|
package/src/use-loader.tsx
CHANGED
|
@@ -256,12 +256,13 @@ function useLoaderInternal<T>(
|
|
|
256
256
|
[groupKey],
|
|
257
257
|
);
|
|
258
258
|
const hasGroups = groupList.length > 0;
|
|
259
|
+
// A grouped read without a key refreshes a bucket no other reader names.
|
|
260
|
+
const usesPrivateBucket = hasGroups && key === undefined;
|
|
259
261
|
const privateBucketIdRef = useRef<string | null>(null);
|
|
260
|
-
if (
|
|
262
|
+
if (usesPrivateBucket && privateBucketIdRef.current === null) {
|
|
261
263
|
privateBucketIdRef.current = `__rg${privateGroupBucketSeq++}`;
|
|
262
264
|
}
|
|
263
|
-
const effectiveKey =
|
|
264
|
-
key ?? (hasGroups ? privateBucketIdRef.current! : undefined);
|
|
265
|
+
const effectiveKey = usesPrivateBucket ? privateBucketIdRef.current! : key;
|
|
265
266
|
const bucketKey =
|
|
266
267
|
effectiveKey === undefined ? loaderId : `${loaderId}::${effectiveKey}`;
|
|
267
268
|
|
|
@@ -352,6 +353,7 @@ function useLoaderInternal<T>(
|
|
|
352
353
|
{
|
|
353
354
|
loaderId,
|
|
354
355
|
ephemeral: !hasContextData,
|
|
356
|
+
owned: usesPrivateBucket,
|
|
355
357
|
group: hasGroups ? groupList : undefined,
|
|
356
358
|
refetch: hasGroups ? groupRefetch : undefined,
|
|
357
359
|
onStreamPending: hasContextData
|