@rangojs/router 0.0.0-experimental.79 → 0.0.0-experimental.7d061845
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/README.md +120 -25
- package/dist/bin/rango.js +147 -57
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +2138 -841
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +68 -21
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +220 -30
- package/skills/caching/SKILL.md +116 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +3 -1
- package/skills/hooks/SKILL.md +229 -20
- package/skills/host-router/SKILL.md +45 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +26 -4
- package/skills/layout/SKILL.md +6 -7
- package/skills/links/SKILL.md +247 -17
- package/skills/loader/SKILL.md +219 -9
- package/skills/middleware/SKILL.md +15 -9
- package/skills/migrate-nextjs/SKILL.md +4 -2
- package/skills/migrate-react-router/SKILL.md +5 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +12 -6
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +242 -24
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +66 -9
- package/skills/route/SKILL.md +33 -4
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +751 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/testing/SKILL.md +816 -0
- package/skills/typesafety/SKILL.md +319 -27
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/event-controller.ts +86 -70
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +65 -9
- package/src/browser/navigation-client.ts +45 -25
- package/src/browser/navigation-store.ts +32 -9
- package/src/browser/navigation-transaction.ts +10 -28
- package/src/browser/partial-update.ts +52 -26
- package/src/browser/prefetch/cache.ts +124 -26
- package/src/browser/prefetch/fetch.ts +114 -38
- package/src/browser/prefetch/queue.ts +36 -5
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +18 -13
- package/src/browser/react/NavigationProvider.tsx +72 -31
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +17 -9
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +20 -8
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +22 -2
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +64 -22
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +23 -30
- package/src/browser/types.ts +21 -0
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/index.ts +2 -0
- package/src/build/route-trie.ts +2 -1
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +55 -14
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-scope.ts +28 -42
- package/src/cache/cf/cf-cache-store.ts +54 -13
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +10 -8
- package/src/context-var.ts +5 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +26 -13
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -20
- package/src/index.rsc.ts +9 -4
- package/src/index.ts +16 -6
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +21 -6
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender.ts +4 -4
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -39
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +253 -265
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +43 -15
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-types.ts +19 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -2
- package/src/router/error-handling.ts +1 -1
- package/src/router/handler-context.ts +21 -41
- package/src/router/intercept-resolution.ts +4 -18
- package/src/router/lazy-includes.ts +3 -3
- package/src/router/loader-resolution.ts +19 -2
- package/src/router/match-api.ts +4 -3
- package/src/router/match-handlers.ts +63 -20
- package/src/router/match-middleware/cache-lookup.ts +44 -91
- package/src/router/match-middleware/cache-store.ts +3 -2
- package/src/router/match-result.ts +53 -32
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +15 -26
- package/src/router/middleware.ts +99 -84
- package/src/router/pattern-matching.ts +101 -17
- package/src/router/prerender-match.ts +1 -1
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +4 -28
- package/src/router/revalidation.ts +58 -2
- package/src/router/router-interfaces.ts +45 -28
- package/src/router/router-options.ts +40 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +27 -6
- package/src/router/segment-resolution/revalidation.ts +147 -106
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/trie-matching.ts +18 -13
- package/src/router/types.ts +8 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +38 -23
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +28 -69
- package/src/rsc/helpers.ts +91 -43
- package/src/rsc/index.ts +1 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/response-route-handler.ts +46 -53
- package/src/rsc/rsc-rendering.ts +35 -51
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +17 -37
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +8 -2
- package/src/search-params.ts +4 -4
- package/src/segment-system.tsx +122 -56
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +118 -51
- package/src/server/cookie-store.ts +28 -4
- package/src/server/request-context.ts +20 -42
- package/src/ssr/index.tsx +5 -1
- package/src/static-handler.ts +1 -1
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +440 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +154 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +306 -0
- package/src/testing/e2e/server.ts +183 -0
- package/src/testing/flight-matchers.ts +104 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +332 -0
- package/src/testing/flight.entry.ts +46 -0
- package/src/testing/flight.ts +224 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +106 -0
- package/src/testing/internal/context.ts +304 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +42 -0
- package/src/testing/render-handler.ts +267 -0
- package/src/testing/render-route.tsx +565 -0
- package/src/testing/run-loader.ts +341 -0
- package/src/testing/run-middleware.ts +188 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +270 -0
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +68 -50
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +5 -6
- package/src/types/request-scope.ts +126 -0
- package/src/types/segments.ts +35 -1
- package/src/urls/include-helper.ts +10 -53
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +11 -3
- package/src/urls/path-helper.ts +17 -52
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +26 -116
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +413 -42
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +101 -51
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +67 -26
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +33 -0
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +2 -0
- package/src/vite/plugin-types.ts +67 -0
- package/src/vite/plugins/cjs-to-esm.ts +8 -7
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +28 -5
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/plugins/expose-action-id.ts +54 -30
- package/src/vite/plugins/expose-id-utils.ts +12 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +496 -486
- package/src/vite/plugins/performance-tracks.ts +29 -25
- package/src/vite/plugins/use-cache-transform.ts +65 -50
- package/src/vite/plugins/version-injector.ts +39 -23
- package/src/vite/plugins/version-plugin.ts +59 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +116 -29
- package/src/vite/router-discovery.ts +750 -100
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +21 -5
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +5 -4
- package/src/vite/utils/shared-utils.ts +107 -26
- package/src/browser/action-response-classifier.ts +0 -99
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ComponentType, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* App-shell metadata: the set of per-router fields that describe the
|
|
5
|
+
* "envelope" around the current app's segment tree. These fields are set
|
|
6
|
+
* from the initial RSC payload and must be replaced atomically when the
|
|
7
|
+
* client navigates into a different router (app switch).
|
|
8
|
+
*
|
|
9
|
+
* Intentionally NOT part of the shell (all document-lifetime):
|
|
10
|
+
* - themeConfig / initialTheme: ThemeProvider is mounted above the segment
|
|
11
|
+
* tree and must not remount on smooth transitions.
|
|
12
|
+
* - warmupEnabled: attached to the NavigationProvider's lifetime effect;
|
|
13
|
+
* toggling it mid-session would tear down and restart idle listeners.
|
|
14
|
+
* Also not serialized on every full-render path (e.g. the not-found
|
|
15
|
+
* fallback), so carrying it here would be unreliable.
|
|
16
|
+
* - prefetchCacheTTL: the not-found full-render payload does not serialize
|
|
17
|
+
* it, so a cross-app nav into a 404 would silently erase the setting.
|
|
18
|
+
* Mutable shell fields must be serialized on EVERY full-render path,
|
|
19
|
+
* otherwise absent fields are indistinguishable from "new app has no
|
|
20
|
+
* value" and the old app's value is dropped.
|
|
21
|
+
*
|
|
22
|
+
* A new document navigation (hard reload) applies these fields from the
|
|
23
|
+
* target app's initial payload.
|
|
24
|
+
*/
|
|
25
|
+
export interface AppShell {
|
|
26
|
+
/** Router identity. Used to namespace per-app client state (e.g. the
|
|
27
|
+
* rango-state localStorage key) so sibling apps on the same origin
|
|
28
|
+
* cannot observe each other's cache invalidations. */
|
|
29
|
+
routerId?: string;
|
|
30
|
+
rootLayout?: ComponentType<{ children: ReactNode }>;
|
|
31
|
+
basename?: string;
|
|
32
|
+
version?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Mutable container for the active app shell. Read-through via `get()` so
|
|
37
|
+
* closures capture the ref, not the shell, and pick up updates at call time.
|
|
38
|
+
*/
|
|
39
|
+
export interface AppShellRef {
|
|
40
|
+
get(): AppShell;
|
|
41
|
+
update(next: AppShell): void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createAppShellRef(initial: AppShell): AppShellRef {
|
|
45
|
+
let current = initial;
|
|
46
|
+
return {
|
|
47
|
+
get: () => current,
|
|
48
|
+
update: (next) => {
|
|
49
|
+
current = next;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -113,11 +113,24 @@ export type ActionStateListener = (state: TrackedActionState) => void;
|
|
|
113
113
|
export type HandleListener = () => void;
|
|
114
114
|
|
|
115
115
|
/**
|
|
116
|
-
* Internal handle state stored in controller
|
|
116
|
+
* Internal handle state stored in controller.
|
|
117
|
+
*
|
|
118
|
+
* Two segment lists are exposed because they serve different consumers:
|
|
119
|
+
*
|
|
120
|
+
* - `segmentOrder` drives handle collection (collectHandleData). Includes
|
|
121
|
+
* parallel slot ids and reorders them after their parent so later-wins
|
|
122
|
+
* collect functions (e.g. Meta) get the right precedence.
|
|
123
|
+
* - `routeSegmentIds` is the layouts-and-routes-only list documented by
|
|
124
|
+
* `useSegments().segmentIds`. Parallels and loader sub-ids are stripped;
|
|
125
|
+
* raw matched order is preserved.
|
|
126
|
+
*
|
|
127
|
+
* Both are derived from the same `matched` input on each setHandleData call
|
|
128
|
+
* so they stay in sync.
|
|
117
129
|
*/
|
|
118
130
|
export interface HandleState {
|
|
119
131
|
data: HandleData;
|
|
120
132
|
segmentOrder: string[];
|
|
133
|
+
routeSegmentIds: string[];
|
|
121
134
|
}
|
|
122
135
|
|
|
123
136
|
/**
|
|
@@ -202,6 +215,14 @@ export interface EventController {
|
|
|
202
215
|
data: HandleData,
|
|
203
216
|
matched?: string[],
|
|
204
217
|
isPartial?: boolean,
|
|
218
|
+
/**
|
|
219
|
+
* Segment ids that were re-resolved on the server this request (the
|
|
220
|
+
* partial response's `diff`). On a partial update, any existing bucket
|
|
221
|
+
* keyed under one of these ids that has no incoming entry is treated as
|
|
222
|
+
* stale and cleared. Without this, a parallel slot that revalidates but
|
|
223
|
+
* pushes nothing leaves its previous bucket in place forever.
|
|
224
|
+
*/
|
|
225
|
+
resolvedIds?: string[],
|
|
205
226
|
): void;
|
|
206
227
|
getHandleState(): HandleState;
|
|
207
228
|
|
|
@@ -247,6 +268,20 @@ function matchesActionId(
|
|
|
247
268
|
return entryActionId.endsWith(`#${subscriptionId}`);
|
|
248
269
|
}
|
|
249
270
|
|
|
271
|
+
// Coalesce rapid notifications into one microtask-deferred fan-out; the
|
|
272
|
+
// setTimeout(0) batching prevents render storms. Each notifier owns its timer
|
|
273
|
+
// so listener kinds coalesce independently.
|
|
274
|
+
function makeDebouncedNotifier(listeners: Set<() => void>): () => void {
|
|
275
|
+
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
276
|
+
return () => {
|
|
277
|
+
if (timeout !== null) clearTimeout(timeout);
|
|
278
|
+
timeout = setTimeout(() => {
|
|
279
|
+
timeout = null;
|
|
280
|
+
listeners.forEach((listener) => listener());
|
|
281
|
+
}, 0);
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
250
285
|
// ============================================================================
|
|
251
286
|
// Implementation
|
|
252
287
|
// ============================================================================
|
|
@@ -300,6 +335,7 @@ export function createEventController(
|
|
|
300
335
|
// Handle data from RSC payload
|
|
301
336
|
let handleData: HandleData = {};
|
|
302
337
|
let handleSegmentOrder: string[] = [];
|
|
338
|
+
let routeSegmentIds: string[] = [];
|
|
303
339
|
|
|
304
340
|
// Merged route params from current match
|
|
305
341
|
let routeParams: Record<string, string> = {};
|
|
@@ -312,18 +348,7 @@ export function createEventController(
|
|
|
312
348
|
const actionListeners = new Map<string, Set<ActionStateListener>>();
|
|
313
349
|
const handleListeners = new Set<HandleListener>();
|
|
314
350
|
|
|
315
|
-
|
|
316
|
-
let notifyTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
317
|
-
|
|
318
|
-
function notify() {
|
|
319
|
-
if (notifyTimeout !== null) {
|
|
320
|
-
clearTimeout(notifyTimeout);
|
|
321
|
-
}
|
|
322
|
-
notifyTimeout = setTimeout(() => {
|
|
323
|
-
notifyTimeout = null;
|
|
324
|
-
stateListeners.forEach((listener) => listener());
|
|
325
|
-
}, 0);
|
|
326
|
-
}
|
|
351
|
+
const notify = makeDebouncedNotifier(stateListeners);
|
|
327
352
|
|
|
328
353
|
// Debounce per-action notifications
|
|
329
354
|
const actionNotifyTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
|
@@ -349,18 +374,7 @@ export function createEventController(
|
|
|
349
374
|
);
|
|
350
375
|
}
|
|
351
376
|
|
|
352
|
-
|
|
353
|
-
let handleNotifyTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
354
|
-
|
|
355
|
-
function notifyHandles() {
|
|
356
|
-
if (handleNotifyTimeout !== null) {
|
|
357
|
-
clearTimeout(handleNotifyTimeout);
|
|
358
|
-
}
|
|
359
|
-
handleNotifyTimeout = setTimeout(() => {
|
|
360
|
-
handleNotifyTimeout = null;
|
|
361
|
-
handleListeners.forEach((listener) => listener());
|
|
362
|
-
}, 0);
|
|
363
|
-
}
|
|
377
|
+
const notifyHandles = makeDebouncedNotifier(handleListeners);
|
|
364
378
|
|
|
365
379
|
// ========================================================================
|
|
366
380
|
// Derived State
|
|
@@ -407,22 +421,17 @@ export function createEventController(
|
|
|
407
421
|
}
|
|
408
422
|
|
|
409
423
|
function getActionState(actionId: string): TrackedActionState {
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
const
|
|
413
|
-
.filter(
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
(a) => matchesActionId(actionId, a.actionId) && a.phase === "settling",
|
|
422
|
-
)
|
|
423
|
-
.sort((a, b) => b.startedAt - a.startedAt)[0];
|
|
424
|
-
|
|
425
|
-
const entry = activeEntry || settlingEntry;
|
|
424
|
+
// Prefer the most-recent non-settling entry; fall back to most-recent
|
|
425
|
+
// settling so a just-settled action's result/error stays readable.
|
|
426
|
+
const entry = [...inflightActions.values()]
|
|
427
|
+
.filter((a) => matchesActionId(actionId, a.actionId))
|
|
428
|
+
.reduce<ActionEntry | undefined>((best, a) => {
|
|
429
|
+
if (!best) return a;
|
|
430
|
+
const aActive = a.phase !== "settling";
|
|
431
|
+
const bActive = best.phase !== "settling";
|
|
432
|
+
if (aActive !== bActive) return aActive ? a : best;
|
|
433
|
+
return a.startedAt > best.startedAt ? a : best;
|
|
434
|
+
}, undefined);
|
|
426
435
|
|
|
427
436
|
if (!entry) {
|
|
428
437
|
return { ...DEFAULT_ACTION_STATE };
|
|
@@ -610,6 +619,19 @@ export function createEventController(
|
|
|
610
619
|
doSettle();
|
|
611
620
|
}
|
|
612
621
|
|
|
622
|
+
// streamingEnded is forced here for the "streaming never started" case so
|
|
623
|
+
// tryFinalize can run; otherwise the streaming token's end() finalizes.
|
|
624
|
+
function settleWith(result: NonNullable<typeof pendingResult>) {
|
|
625
|
+
if (!inflightActions.has(id) || settled) return;
|
|
626
|
+
actionCompleted = true;
|
|
627
|
+
entry.completed = true;
|
|
628
|
+
pendingResult = result;
|
|
629
|
+
if (entry.phase === "fetching" || streamingEnded) {
|
|
630
|
+
streamingEnded = true;
|
|
631
|
+
tryFinalize();
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
613
635
|
return {
|
|
614
636
|
id,
|
|
615
637
|
abort,
|
|
@@ -646,35 +668,11 @@ export function createEventController(
|
|
|
646
668
|
},
|
|
647
669
|
|
|
648
670
|
complete(result?: unknown) {
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
actionCompleted = true;
|
|
652
|
-
entry.completed = true;
|
|
653
|
-
pendingResult = { type: "success", value: result };
|
|
654
|
-
|
|
655
|
-
// If streaming never started or already ended, finalize immediately
|
|
656
|
-
// Otherwise wait for streaming to end
|
|
657
|
-
if (entry.phase === "fetching" || streamingEnded) {
|
|
658
|
-
streamingEnded = true; // Mark as ended if never started
|
|
659
|
-
tryFinalize();
|
|
660
|
-
}
|
|
661
|
-
// If streaming is in progress, tryFinalize() will be called when streaming ends
|
|
671
|
+
settleWith({ type: "success", value: result });
|
|
662
672
|
},
|
|
663
673
|
|
|
664
674
|
fail(error: unknown) {
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
actionCompleted = true;
|
|
668
|
-
entry.completed = true;
|
|
669
|
-
pendingResult = { type: "error", value: error };
|
|
670
|
-
|
|
671
|
-
// If streaming never started or already ended, finalize immediately
|
|
672
|
-
// Otherwise wait for streaming to end
|
|
673
|
-
if (entry.phase === "fetching" || streamingEnded) {
|
|
674
|
-
streamingEnded = true; // Mark as ended if never started
|
|
675
|
-
tryFinalize();
|
|
676
|
-
}
|
|
677
|
-
// If streaming is in progress, tryFinalize() will be called when streaming ends
|
|
675
|
+
settleWith({ type: "error", value: error });
|
|
678
676
|
},
|
|
679
677
|
|
|
680
678
|
getRevalidatedSegments(): Set<string> {
|
|
@@ -744,8 +742,15 @@ export function createEventController(
|
|
|
744
742
|
data: HandleData,
|
|
745
743
|
matched?: string[],
|
|
746
744
|
isPartial?: boolean,
|
|
745
|
+
resolvedIds?: string[],
|
|
747
746
|
): void {
|
|
748
|
-
const
|
|
747
|
+
const rawMatched = matched ?? [];
|
|
748
|
+
const newSegmentOrder = filterSegmentOrder(rawMatched);
|
|
749
|
+
// Separate list for useSegments(): "layouts and routes only" — strip
|
|
750
|
+
// parallels (".@") and loader sub-ids (D digit) without reordering.
|
|
751
|
+
const newRouteSegmentIds = rawMatched.filter(
|
|
752
|
+
(id) => !id.includes(".@") && !/D\d+\./.test(id),
|
|
753
|
+
);
|
|
749
754
|
|
|
750
755
|
if (isPartial && newSegmentOrder.length > 0) {
|
|
751
756
|
// Partial update: merge new data with existing
|
|
@@ -757,10 +762,19 @@ export function createEventController(
|
|
|
757
762
|
handleData[handleName][segmentId] = data[handleName][segmentId];
|
|
758
763
|
}
|
|
759
764
|
}
|
|
760
|
-
|
|
765
|
+
const resolvedIdSet =
|
|
766
|
+
resolvedIds && resolvedIds.length > 0 ? new Set(resolvedIds) : null;
|
|
767
|
+
// Cleanup pass:
|
|
768
|
+
// a) segment dropped from the match list — delete its bucket.
|
|
769
|
+
// b) segment was re-resolved this request but pushed nothing for
|
|
770
|
+
// this handle — its previous bucket is stale.
|
|
771
|
+
// (a) is the existing behavior; (b) requires resolvedIds.
|
|
761
772
|
for (const handleName of Object.keys(handleData)) {
|
|
762
773
|
for (const segmentId of Object.keys(handleData[handleName])) {
|
|
763
|
-
|
|
774
|
+
const droppedFromMatch = !newSegmentOrder.includes(segmentId);
|
|
775
|
+
const reresolvedWithoutPush =
|
|
776
|
+
resolvedIdSet?.has(segmentId) && !data[handleName]?.[segmentId];
|
|
777
|
+
if (droppedFromMatch || reresolvedWithoutPush) {
|
|
764
778
|
delete handleData[handleName][segmentId];
|
|
765
779
|
}
|
|
766
780
|
}
|
|
@@ -770,6 +784,7 @@ export function createEventController(
|
|
|
770
784
|
handleData = data;
|
|
771
785
|
}
|
|
772
786
|
handleSegmentOrder = newSegmentOrder;
|
|
787
|
+
routeSegmentIds = newRouteSegmentIds;
|
|
773
788
|
|
|
774
789
|
notifyHandles();
|
|
775
790
|
}
|
|
@@ -778,6 +793,7 @@ export function createEventController(
|
|
|
778
793
|
return {
|
|
779
794
|
data: handleData,
|
|
780
795
|
segmentOrder: handleSegmentOrder,
|
|
796
|
+
routeSegmentIds,
|
|
781
797
|
};
|
|
782
798
|
}
|
|
783
799
|
|
|
@@ -61,6 +61,27 @@ export function buildHistoryState(
|
|
|
61
61
|
return Object.keys(result).length > 0 ? result : null;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Stamp an `idx` on the next history entry's state and call push/replaceState.
|
|
66
|
+
* Push increments the current idx; replace keeps it. Initial entry idx is 0.
|
|
67
|
+
* Used by useRouter().back() to detect "first entry in this session" without
|
|
68
|
+
* relying on the Navigation API.
|
|
69
|
+
*/
|
|
70
|
+
export function pushHistoryWithIdx(
|
|
71
|
+
state: Record<string, unknown> | null,
|
|
72
|
+
url: string,
|
|
73
|
+
replace: boolean,
|
|
74
|
+
): void {
|
|
75
|
+
const oldIdx = (window.history.state as { idx?: number } | null)?.idx ?? 0;
|
|
76
|
+
const newIdx = replace ? oldIdx : oldIdx + 1;
|
|
77
|
+
const finalState = { ...(state ?? {}), idx: newIdx };
|
|
78
|
+
if (replace) {
|
|
79
|
+
window.history.replaceState(finalState, "", url);
|
|
80
|
+
} else {
|
|
81
|
+
window.history.pushState(finalState, "", url);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
64
85
|
/**
|
|
65
86
|
* Merge server-set location state into the current history entry.
|
|
66
87
|
* Replaces the current history state and dispatches notification event
|
package/src/browser/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// ============================================================================
|
|
2
|
-
// Browser Module - Browser entry point for
|
|
2
|
+
// Browser Module - Browser entry point for Rango
|
|
3
3
|
// ============================================================================
|
|
4
4
|
//
|
|
5
5
|
// Usage:
|
|
6
|
-
// import { initBrowserApp,
|
|
6
|
+
// import { initBrowserApp, Rango } from "rsc-router/browser";
|
|
7
7
|
//
|
|
8
8
|
// For React components (Link, useNavigation, etc.):
|
|
9
9
|
// import { Link, useNavigation, useAction, href } from "rsc-router/client";
|
|
@@ -13,6 +13,6 @@
|
|
|
13
13
|
// Browser app initialization
|
|
14
14
|
export {
|
|
15
15
|
initBrowserApp,
|
|
16
|
-
|
|
16
|
+
Rango,
|
|
17
17
|
type InitBrowserAppOptions,
|
|
18
18
|
} from "./rsc-router.js";
|
|
@@ -5,13 +5,15 @@ import type {
|
|
|
5
5
|
ResolvedSegment,
|
|
6
6
|
} from "./types.js";
|
|
7
7
|
import { setAppVersion } from "./app-version.js";
|
|
8
|
+
import { setRangoStateLocal } from "./rango-state.js";
|
|
9
|
+
import type { AppShell, AppShellRef } from "./app-shell.js";
|
|
8
10
|
import * as React from "react";
|
|
9
11
|
import { startTransition } from "react";
|
|
10
12
|
import {
|
|
11
13
|
createNavigationTransaction,
|
|
12
14
|
resolveNavigationState,
|
|
13
15
|
} from "./navigation-transaction.js";
|
|
14
|
-
import { buildHistoryState } from "./history-state.js";
|
|
16
|
+
import { buildHistoryState, pushHistoryWithIdx } from "./history-state.js";
|
|
15
17
|
import {
|
|
16
18
|
handleNavigationStart,
|
|
17
19
|
handleNavigationEnd,
|
|
@@ -48,8 +50,13 @@ export { createNavigationTransaction };
|
|
|
48
50
|
*/
|
|
49
51
|
export interface NavigationBridgeConfigWithController extends NavigationBridgeConfig {
|
|
50
52
|
eventController: EventController;
|
|
51
|
-
/** RSC version from initial payload metadata */
|
|
53
|
+
/** RSC version from initial payload metadata (fallback when appShellRef is not provided) */
|
|
52
54
|
version?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Live app-shell ref. When supplied, the bridge reads version/basename
|
|
57
|
+
* from this ref so cross-app navigations propagate correctly.
|
|
58
|
+
*/
|
|
59
|
+
appShellRef?: AppShellRef;
|
|
53
60
|
}
|
|
54
61
|
|
|
55
62
|
/**
|
|
@@ -68,9 +75,46 @@ export interface NavigationBridgeConfigWithController extends NavigationBridgeCo
|
|
|
68
75
|
export function createNavigationBridge(
|
|
69
76
|
config: NavigationBridgeConfigWithController,
|
|
70
77
|
): NavigationBridge {
|
|
71
|
-
const {
|
|
78
|
+
const {
|
|
79
|
+
store,
|
|
80
|
+
client,
|
|
81
|
+
eventController,
|
|
82
|
+
onUpdate,
|
|
83
|
+
renderSegments,
|
|
84
|
+
appShellRef,
|
|
85
|
+
} = config;
|
|
72
86
|
let version = config.version;
|
|
73
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Replace the active app-shell snapshot atomically. Called by the partial
|
|
90
|
+
* updater when a response's routerId indicates the navigation crossed
|
|
91
|
+
* into a different app. Runs the local-only side-effects tied to
|
|
92
|
+
* app-shell fields (app version, rango-state namespace) so the new app
|
|
93
|
+
* owns them after the swap. Theme, warmup, and prefetch TTL are
|
|
94
|
+
* document-lifetime and are NOT touched here.
|
|
95
|
+
*/
|
|
96
|
+
function applyAppShell(next: AppShell): void {
|
|
97
|
+
if (appShellRef) {
|
|
98
|
+
appShellRef.update(next);
|
|
99
|
+
}
|
|
100
|
+
if (next.version !== undefined) {
|
|
101
|
+
version = next.version;
|
|
102
|
+
setAppVersion(next.version);
|
|
103
|
+
// Use the local-only setter — initRangoState writes the shared
|
|
104
|
+
// localStorage key and fires a storage event in other tabs still in
|
|
105
|
+
// the old app. setRangoStateLocal only mutates this tab's in-memory
|
|
106
|
+
// cache and rebinds it to the target app's routerId-scoped key,
|
|
107
|
+
// preserving the "local-only, no broadcast/rotation" contract for
|
|
108
|
+
// smooth app-switch transitions.
|
|
109
|
+
setRangoStateLocal(next.version, next.routerId);
|
|
110
|
+
}
|
|
111
|
+
// Cross-app: prior cache entries belong to a different app's segments.
|
|
112
|
+
// Drop them locally only — do NOT broadcast invalidation or rotate the
|
|
113
|
+
// shared X-Rango-State token, since other tabs still in the old app are
|
|
114
|
+
// unaffected by this tab's transition.
|
|
115
|
+
store.clearHistoryCacheLocal();
|
|
116
|
+
}
|
|
117
|
+
|
|
74
118
|
// Create shared partial updater
|
|
75
119
|
const fetchPartialUpdate = createPartialUpdater({
|
|
76
120
|
store,
|
|
@@ -78,6 +122,7 @@ export function createNavigationBridge(
|
|
|
78
122
|
onUpdate,
|
|
79
123
|
renderSegments,
|
|
80
124
|
getVersion: () => version,
|
|
125
|
+
applyAppShell,
|
|
81
126
|
});
|
|
82
127
|
|
|
83
128
|
return {
|
|
@@ -159,11 +204,7 @@ export function createNavigationBridge(
|
|
|
159
204
|
},
|
|
160
205
|
{},
|
|
161
206
|
);
|
|
162
|
-
|
|
163
|
-
window.history.replaceState(historyState, "", url);
|
|
164
|
-
} else {
|
|
165
|
-
window.history.pushState(historyState, "", url);
|
|
166
|
-
}
|
|
207
|
+
pushHistoryWithIdx(historyState, url, options?.replace ?? false);
|
|
167
208
|
|
|
168
209
|
// Ensure new history entry has a scroll restoration key
|
|
169
210
|
ensureHistoryKey();
|
|
@@ -496,7 +537,14 @@ export function createNavigationBridge(
|
|
|
496
537
|
},
|
|
497
538
|
scroll: { restore: true, isStreaming },
|
|
498
539
|
};
|
|
499
|
-
|
|
540
|
+
// Intercept-driven popstate (entering OR leaving an intercept) only
|
|
541
|
+
// mutates the parallel slot; the main outlet shows the same content.
|
|
542
|
+
// Skip startViewTransition in those cases — same rationale as the
|
|
543
|
+
// intercept guard in partial-update.ts's hasTransition computation.
|
|
544
|
+
const hasTransition =
|
|
545
|
+
!isIntercept &&
|
|
546
|
+
!isLeavingIntercept &&
|
|
547
|
+
cachedSegments.some((s) => s.transition);
|
|
500
548
|
if (hasTransition) {
|
|
501
549
|
startTransition(() => {
|
|
502
550
|
if (addTransitionType) {
|
|
@@ -659,11 +707,19 @@ export function createNavigationBridge(
|
|
|
659
707
|
};
|
|
660
708
|
},
|
|
661
709
|
|
|
710
|
+
getVersion(): string | undefined {
|
|
711
|
+
return version;
|
|
712
|
+
},
|
|
713
|
+
|
|
662
714
|
updateVersion(newVersion: string): void {
|
|
663
715
|
version = newVersion;
|
|
664
716
|
setAppVersion(newVersion);
|
|
665
717
|
store.clearHistoryCache();
|
|
666
718
|
},
|
|
719
|
+
|
|
720
|
+
updateAppShell(next: AppShell): void {
|
|
721
|
+
applyAppShell(next);
|
|
722
|
+
},
|
|
667
723
|
};
|
|
668
724
|
}
|
|
669
725
|
|
|
@@ -15,10 +15,12 @@ import { getRangoState } from "./rango-state.js";
|
|
|
15
15
|
import {
|
|
16
16
|
extractRscHeaderUrl,
|
|
17
17
|
emptyResponse,
|
|
18
|
+
handleReloadHeader,
|
|
18
19
|
teeWithCompletion,
|
|
19
20
|
} from "./response-adapter.js";
|
|
20
21
|
import {
|
|
21
22
|
buildPrefetchKey,
|
|
23
|
+
buildSourceKey,
|
|
22
24
|
consumeInflightPrefetch,
|
|
23
25
|
consumePrefetch,
|
|
24
26
|
} from "./prefetch/cache.js";
|
|
@@ -30,8 +32,10 @@ import {
|
|
|
30
32
|
* deserializing the response using the RSC runtime.
|
|
31
33
|
*
|
|
32
34
|
* Checks the in-memory prefetch cache before making a network request.
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
+
* Tries the source-scoped key first (populated when the server tagged
|
|
36
|
+
* the response as source-sensitive via `X-RSC-Prefetch-Scope: source`)
|
|
37
|
+
* and falls back to the Rango-state-keyed wildcard slot used for the
|
|
38
|
+
* common source-agnostic case.
|
|
35
39
|
*
|
|
36
40
|
* @param deps - RSC browser dependencies (createFromFetch)
|
|
37
41
|
* @returns NavigationClient instance
|
|
@@ -93,17 +97,19 @@ export function createNavigationClient(
|
|
|
93
97
|
fetchUrl.searchParams.set("_rsc_rid", routerId);
|
|
94
98
|
}
|
|
95
99
|
|
|
96
|
-
// Check completed in-memory prefetch cache before making a network
|
|
97
|
-
//
|
|
98
|
-
//
|
|
100
|
+
// Check completed in-memory prefetch cache before making a network
|
|
101
|
+
// request. Try the source-scoped key first (populated when the server
|
|
102
|
+
// tagged the prefetch response as source-sensitive, e.g. intercepts,
|
|
103
|
+
// or when a Link opted in with `prefetchKey=":source"`), then fall
|
|
104
|
+
// back to the wildcard slot shared across source pages.
|
|
105
|
+
// Both keys embed the Rango state, so state rotation (deploy or
|
|
106
|
+
// server-action invalidation) auto-invalidates both scopes.
|
|
99
107
|
// Skip cache for stale revalidation (needs fresh data), HMR (needs
|
|
100
108
|
// fresh modules), and intercept contexts (source-dependent responses).
|
|
101
|
-
//
|
|
102
109
|
const canUsePrefetch = !staleRevalidation && !hmr && !interceptSourceUrl;
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const wildcardKey = "*\0" + fetchUrl.pathname + fetchUrl.search;
|
|
110
|
+
const rangoState = getRangoState();
|
|
111
|
+
const wildcardKey = buildPrefetchKey(rangoState, fetchUrl);
|
|
112
|
+
const cacheKey = buildSourceKey(rangoState, previousUrl, fetchUrl);
|
|
107
113
|
|
|
108
114
|
let cachedResponse: Response | null = null;
|
|
109
115
|
let hitKey: string | null = null;
|
|
@@ -143,21 +149,17 @@ export function createNavigationClient(
|
|
|
143
149
|
source: string,
|
|
144
150
|
): Response | Promise<Response> => {
|
|
145
151
|
// Version mismatch — server wants a full page reload
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
window.location.href = reload.url;
|
|
158
|
-
// Block further processing — page is reloading
|
|
159
|
-
return new Promise<Response>(() => {});
|
|
160
|
-
}
|
|
152
|
+
const reloadResult = handleReloadHeader(response, {
|
|
153
|
+
onBlocked: resolveStreamComplete,
|
|
154
|
+
onReload: (url) => {
|
|
155
|
+
if (tx) {
|
|
156
|
+
browserDebugLog(tx, `version mismatch, reloading (${source})`, {
|
|
157
|
+
reloadUrl: url,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
if (reloadResult) return reloadResult;
|
|
161
163
|
|
|
162
164
|
// Server-side redirect without state: the server returned 204 with
|
|
163
165
|
// X-RSC-Redirect instead of a 3xx (which fetch would auto-follow
|
|
@@ -244,6 +246,7 @@ export function createNavigationClient(
|
|
|
244
246
|
wildcard: hitKey === wildcardKey,
|
|
245
247
|
});
|
|
246
248
|
}
|
|
249
|
+
const adoptedViaWildcard = hitKey === wildcardKey;
|
|
247
250
|
responsePromise = inflightResponsePromise.then(async (response) => {
|
|
248
251
|
if (!response) {
|
|
249
252
|
if (tx) {
|
|
@@ -252,6 +255,23 @@ export function createNavigationClient(
|
|
|
252
255
|
return doFreshFetch();
|
|
253
256
|
}
|
|
254
257
|
|
|
258
|
+
// Cross-source safety: an inflight promise adopted via the
|
|
259
|
+
// wildcard key may turn out to be source-scoped (server emitted
|
|
260
|
+
// `X-RSC-Prefetch-Scope: source`), which means it was built for
|
|
261
|
+
// a different source page. Discard and refetch.
|
|
262
|
+
if (
|
|
263
|
+
adoptedViaWildcard &&
|
|
264
|
+
response.headers.get("x-rsc-prefetch-scope") === "source"
|
|
265
|
+
) {
|
|
266
|
+
if (tx) {
|
|
267
|
+
browserDebugLog(
|
|
268
|
+
tx,
|
|
269
|
+
"wildcard inflight turned out source-scoped, refetching",
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
return doFreshFetch();
|
|
273
|
+
}
|
|
274
|
+
|
|
255
275
|
const validated = validateRscHeaders(response, "inflight prefetch");
|
|
256
276
|
if (validated instanceof Promise) return validated;
|
|
257
277
|
|
|
@@ -12,7 +12,10 @@ import type {
|
|
|
12
12
|
ActionStateListener,
|
|
13
13
|
HandleData,
|
|
14
14
|
} from "./types.js";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
clearPrefetchCache,
|
|
17
|
+
clearPrefetchCacheLocal,
|
|
18
|
+
} from "./prefetch/cache.js";
|
|
16
19
|
|
|
17
20
|
/**
|
|
18
21
|
* Default action state (idle with no payload)
|
|
@@ -280,18 +283,17 @@ export function createNavigationStore(
|
|
|
280
283
|
/**
|
|
281
284
|
* Create a debounced function that batches rapid calls
|
|
282
285
|
*/
|
|
286
|
+
// A non-keyed notifier is the keyed one restricted to a single constant key;
|
|
287
|
+
// its own keyed instance means the "" key never collides with action keys.
|
|
283
288
|
function createDebouncedNotifier<T extends (...args: any[]) => void>(
|
|
284
289
|
fn: T,
|
|
285
290
|
ms: number = 20,
|
|
286
291
|
): T {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
fn(...args);
|
|
293
|
-
}, ms);
|
|
294
|
-
}) as T;
|
|
292
|
+
const keyed = createKeyedDebouncedNotifier(
|
|
293
|
+
(_key: string, ...args: any[]) => fn(...args),
|
|
294
|
+
ms,
|
|
295
|
+
);
|
|
296
|
+
return ((...args: Parameters<T>) => keyed("", ...args)) as T;
|
|
295
297
|
}
|
|
296
298
|
|
|
297
299
|
/**
|
|
@@ -335,6 +337,18 @@ export function createNavigationStore(
|
|
|
335
337
|
clearPrefetchCache();
|
|
336
338
|
}
|
|
337
339
|
|
|
340
|
+
/**
|
|
341
|
+
* Drop this tab's navigation + prefetch caches without broadcasting or
|
|
342
|
+
* rotating shared state. Used when the local session changes in a way that
|
|
343
|
+
* doesn't affect other tabs — e.g. this tab crosses into a different app
|
|
344
|
+
* via a cross-router navigation. Other tabs in the old app keep their
|
|
345
|
+
* caches and their X-Rango-State token.
|
|
346
|
+
*/
|
|
347
|
+
function clearCacheInternalLocal(): void {
|
|
348
|
+
historyCache.length = 0;
|
|
349
|
+
clearPrefetchCacheLocal();
|
|
350
|
+
}
|
|
351
|
+
|
|
338
352
|
/**
|
|
339
353
|
* Mark all cache entries as stale (internal - does not broadcast)
|
|
340
354
|
*/
|
|
@@ -668,6 +682,15 @@ export function createNavigationStore(
|
|
|
668
682
|
clearCacheAndBroadcast();
|
|
669
683
|
},
|
|
670
684
|
|
|
685
|
+
/**
|
|
686
|
+
* Drop this tab's navigation + prefetch caches locally without
|
|
687
|
+
* broadcasting or rotating shared state. Intended for cross-app
|
|
688
|
+
* transitions where the session state diverges for this tab only.
|
|
689
|
+
*/
|
|
690
|
+
clearHistoryCacheLocal(): void {
|
|
691
|
+
clearCacheInternalLocal();
|
|
692
|
+
},
|
|
693
|
+
|
|
671
694
|
/**
|
|
672
695
|
* Mark cache as stale and broadcast to other tabs
|
|
673
696
|
* Called after server actions - allows SWR pattern for popstate
|