@rangojs/router 0.0.0-experimental.122 → 0.0.0-experimental.125
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 +10 -6
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +55 -48
- package/package.json +61 -21
- package/skills/caching/SKILL.md +2 -1
- package/skills/hooks/SKILL.md +40 -29
- package/skills/host-router/SKILL.md +16 -2
- package/skills/intercept/SKILL.md +4 -2
- package/skills/layout/SKILL.md +11 -6
- package/skills/loader/SKILL.md +6 -2
- package/skills/middleware/SKILL.md +4 -2
- package/skills/migrate-nextjs/SKILL.md +3 -1
- package/skills/parallel/SKILL.md +9 -4
- package/skills/rango/SKILL.md +12 -0
- package/skills/route/SKILL.md +10 -2
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +98 -0
- package/skills/testing/client-components.md +122 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +89 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +118 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +120 -0
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +1 -1
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +1 -83
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/navigation-bridge.ts +14 -1
- package/src/browser/navigation-client.ts +14 -1
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +26 -51
- package/src/browser/navigation-transaction.ts +0 -32
- package/src/browser/partial-update.ts +1 -83
- package/src/browser/prefetch/cache.ts +6 -45
- package/src/browser/prefetch/fetch.ts +7 -0
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +157 -99
- package/src/browser/react/Link.tsx +0 -2
- package/src/browser/react/NavigationProvider.tsx +2 -1
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +0 -2
- package/src/browser/react/index.ts +0 -51
- package/src/browser/react/location-state-shared.ts +0 -13
- package/src/browser/react/location-state.ts +0 -1
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +0 -5
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +0 -3
- package/src/browser/react/use-params.ts +0 -2
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +0 -13
- package/src/browser/rsc-router.tsx +12 -4
- package/src/browser/server-action-bridge.ts +77 -15
- package/src/browser/types.ts +7 -2
- package/src/browser/validate-redirect-origin.ts +4 -5
- package/src/build/route-trie.ts +3 -0
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/router-processing.ts +0 -8
- package/src/cache/cache-policy.ts +0 -54
- package/src/cache/cache-runtime.ts +27 -24
- package/src/cache/cache-scope.ts +0 -27
- package/src/cache/cache-tag.ts +0 -37
- package/src/cache/cf/cf-cache-store.ts +94 -46
- package/src/cache/cf/index.ts +0 -24
- package/src/cache/document-cache.ts +11 -36
- package/src/cache/handle-snapshot.ts +0 -40
- package/src/cache/index.ts +0 -27
- package/src/cache/memory-segment-store.ts +2 -48
- package/src/cache/profile-registry.ts +7 -3
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/types.ts +0 -98
- package/src/client.rsc.tsx +1 -22
- package/src/client.tsx +14 -38
- package/src/component-utils.ts +19 -0
- package/src/deps/ssr.ts +0 -1
- package/src/handle.ts +28 -18
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +6 -0
- package/src/host/pattern-matcher.ts +7 -50
- package/src/host/router.ts +1 -65
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +6 -2
- package/src/href-client.ts +0 -4
- package/src/index.rsc.ts +42 -3
- package/src/index.ts +31 -1
- package/src/internal-debug.ts +2 -4
- package/src/loader.rsc.ts +19 -9
- package/src/loader.ts +12 -4
- package/src/network-error-thrower.tsx +1 -6
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +23 -30
- package/src/prerender.ts +58 -3
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +1 -44
- package/src/route-definition/dsl-helpers.ts +7 -19
- package/src/route-definition/helpers-types.ts +3 -3
- package/src/route-definition/redirect.ts +11 -1
- package/src/route-map-builder.ts +0 -16
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +0 -13
- package/src/router/error-handling.ts +12 -16
- package/src/router/find-match.ts +4 -30
- package/src/router/intercept-resolution.ts +10 -1
- package/src/router/lazy-includes.ts +1 -57
- package/src/router/loader-resolution.ts +3 -2
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +1 -25
- package/src/router/match-api.ts +0 -20
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +57 -58
- package/src/router/match-middleware/background-revalidation.ts +0 -7
- package/src/router/match-middleware/cache-lookup.ts +1 -54
- package/src/router/match-middleware/cache-store.ts +0 -31
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -21
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +1 -52
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-cookies.ts +0 -13
- package/src/router/middleware-types.ts +0 -115
- package/src/router/middleware.ts +7 -30
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +1 -33
- package/src/router/prerender-match.ts +33 -45
- package/src/router/request-classification.ts +1 -38
- package/src/router/revalidation.ts +5 -58
- package/src/router/router-context.ts +0 -26
- package/src/router/router-interfaces.ts +7 -0
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/fresh.ts +25 -57
- package/src/router/segment-resolution/helpers.ts +34 -0
- package/src/router/segment-resolution/loader-cache.ts +10 -13
- package/src/router/segment-resolution/revalidation.ts +5 -42
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +63 -40
- package/src/router/types.ts +1 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +40 -9
- package/src/rsc/handler.ts +14 -2
- package/src/rsc/helpers.ts +34 -0
- package/src/rsc/origin-guard.ts +0 -12
- package/src/rsc/progressive-enhancement.ts +4 -1
- package/src/rsc/rsc-rendering.ts +4 -7
- package/src/rsc/runtime-warnings.ts +14 -0
- package/src/rsc/server-action.ts +30 -28
- package/src/rsc/types.ts +2 -1
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +0 -16
- package/src/segment-loader-promise.ts +14 -2
- package/src/segment-system.tsx +79 -88
- package/src/server/cookie-store.ts +52 -1
- package/src/server/handle-store.ts +7 -24
- package/src/server/loader-registry.ts +5 -24
- package/src/server/request-context.ts +74 -77
- package/src/ssr/index.tsx +14 -14
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +119 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +581 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +127 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +186 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +98 -0
- package/src/testing/internal/context.ts +348 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +311 -0
- package/src/testing/render-route.tsx +504 -0
- package/src/testing/run-loader.ts +378 -0
- package/src/testing/run-middleware.ts +205 -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 +305 -0
- package/src/theme/ThemeProvider.tsx +0 -52
- package/src/theme/ThemeScript.tsx +0 -6
- package/src/theme/constants.ts +0 -12
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +0 -14
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/error-types.ts +25 -89
- package/src/types/global-namespace.ts +15 -15
- package/src/types/handler-context.ts +16 -13
- package/src/types/index.ts +0 -10
- package/src/types/request-scope.ts +0 -19
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +0 -13
- package/src/urls/include-helper.ts +0 -4
- package/src/urls/index.ts +0 -6
- package/src/urls/path-helper-types.ts +2 -2
- package/src/urls/path-helper.ts +0 -54
- package/src/urls/urls-function.ts +0 -13
- package/src/use-loader.tsx +0 -186
- package/src/vite/discovery/bundle-postprocess.ts +2 -1
- package/src/vite/discovery/discover-routers.ts +6 -7
- package/src/vite/discovery/virtual-module-codegen.ts +1 -11
- package/src/vite/plugin-types.ts +3 -1
- package/src/vite/plugins/cjs-to-esm.ts +0 -11
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +0 -10
- package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
- package/src/vite/plugins/expose-action-id.ts +2 -73
- package/src/vite/plugins/expose-id-utils.ts +0 -55
- package/src/vite/plugins/expose-ids/export-analysis.ts +0 -38
- package/src/vite/plugins/expose-ids/handler-transform.ts +0 -15
- package/src/vite/plugins/expose-ids/loader-transform.ts +0 -15
- package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
- package/src/vite/plugins/expose-internal-ids.ts +10 -0
- package/src/vite/plugins/performance-tracks.ts +0 -3
- package/src/vite/plugins/use-cache-transform.ts +0 -36
- package/src/vite/plugins/version-injector.ts +0 -20
- package/src/vite/plugins/version-plugin.ts +1 -49
- package/src/vite/plugins/virtual-entries.ts +0 -15
- package/src/vite/rango.ts +1 -108
- package/src/vite/router-discovery.ts +2 -1
- package/src/vite/utils/ast-handler-extract.ts +0 -16
- package/src/vite/utils/bundle-analysis.ts +6 -13
- package/src/vite/utils/client-chunks.ts +0 -6
- package/src/vite/utils/forward-user-plugins.ts +0 -22
- package/src/vite/utils/manifest-utils.ts +0 -4
- package/src/vite/utils/package-resolution.ts +1 -73
- package/src/vite/utils/prerender-utils.ts +0 -35
- package/src/vite/utils/shared-utils.ts +3 -35
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
|
@@ -237,10 +237,6 @@ export interface EventController {
|
|
|
237
237
|
hadAnyConcurrentActions(): boolean;
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
// ============================================================================
|
|
241
|
-
// Default States
|
|
242
|
-
// ============================================================================
|
|
243
|
-
|
|
244
240
|
const DEFAULT_ACTION_STATE: TrackedActionState = {
|
|
245
241
|
state: "idle",
|
|
246
242
|
actionId: null,
|
|
@@ -261,16 +257,12 @@ function matchesActionId(
|
|
|
261
257
|
entryActionId: string,
|
|
262
258
|
): boolean {
|
|
263
259
|
if (subscriptionId.includes("#")) {
|
|
264
|
-
// Full ID: exact match
|
|
265
260
|
return subscriptionId === entryActionId;
|
|
266
261
|
}
|
|
267
|
-
// Action name only: suffix match (matches "anything#actionName")
|
|
268
262
|
return entryActionId.endsWith(`#${subscriptionId}`);
|
|
269
263
|
}
|
|
270
264
|
|
|
271
|
-
//
|
|
272
|
-
// setTimeout(0) batching prevents render storms. Each notifier owns its timer
|
|
273
|
-
// so listener kinds coalesce independently.
|
|
265
|
+
// Batch rapid notifications into one microtask to prevent render storms
|
|
274
266
|
function makeDebouncedNotifier(listeners: Set<() => void>): () => void {
|
|
275
267
|
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
276
268
|
return () => {
|
|
@@ -282,13 +274,6 @@ function makeDebouncedNotifier(listeners: Set<() => void>): () => void {
|
|
|
282
274
|
};
|
|
283
275
|
}
|
|
284
276
|
|
|
285
|
-
// ============================================================================
|
|
286
|
-
// Implementation
|
|
287
|
-
// ============================================================================
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Configuration for creating an event controller
|
|
291
|
-
*/
|
|
292
277
|
export interface EventControllerConfig {
|
|
293
278
|
initialLocation?: NavigationLocation;
|
|
294
279
|
}
|
|
@@ -306,51 +291,34 @@ export interface EventControllerConfig {
|
|
|
306
291
|
export function createEventController(
|
|
307
292
|
config?: EventControllerConfig,
|
|
308
293
|
): EventController {
|
|
309
|
-
// ========================================================================
|
|
310
|
-
// Source of Truth
|
|
311
|
-
// ========================================================================
|
|
312
|
-
|
|
313
|
-
// Current navigation in progress (null = idle)
|
|
314
294
|
let currentNavigation: NavigationEntry | null = null;
|
|
315
295
|
|
|
316
|
-
// All in-flight actions (keyed by unique instance ID)
|
|
317
296
|
const inflightActions = new Map<string, ActionEntry>();
|
|
318
297
|
|
|
319
|
-
// Committed location (updated when navigation completes)
|
|
320
298
|
let location: NavigationLocation =
|
|
321
299
|
config?.initialLocation ??
|
|
322
300
|
(typeof window !== "undefined"
|
|
323
301
|
? new URL(window.location.href)
|
|
324
302
|
: new URL("/", "http://localhost"));
|
|
325
303
|
|
|
326
|
-
// Track if any concurrent actions occurred (for consolidation)
|
|
327
304
|
let hadAnyConcurrentActions = false;
|
|
328
305
|
|
|
329
|
-
// Track segments revalidated by concurrent actions
|
|
330
306
|
const concurrentRevalidatedSegments = new Set<string>();
|
|
331
307
|
|
|
332
|
-
// Active streaming count (independent of navigation/action lifecycle)
|
|
333
308
|
let activeStreamCount = 0;
|
|
334
309
|
|
|
335
|
-
// Handle data from RSC payload
|
|
336
310
|
let handleData: HandleData = {};
|
|
337
311
|
let handleSegmentOrder: string[] = [];
|
|
338
312
|
let routeSegmentIds: string[] = [];
|
|
339
313
|
|
|
340
|
-
// Merged route params from current match
|
|
341
314
|
let routeParams: Record<string, string> = {};
|
|
342
315
|
|
|
343
|
-
// ========================================================================
|
|
344
|
-
// Listeners
|
|
345
|
-
// ========================================================================
|
|
346
|
-
|
|
347
316
|
const stateListeners = new Set<StateListener>();
|
|
348
317
|
const actionListeners = new Map<string, Set<ActionStateListener>>();
|
|
349
318
|
const handleListeners = new Set<HandleListener>();
|
|
350
319
|
|
|
351
320
|
const notify = makeDebouncedNotifier(stateListeners);
|
|
352
321
|
|
|
353
|
-
// Debounce per-action notifications
|
|
354
322
|
const actionNotifyTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
|
355
323
|
|
|
356
324
|
function notifyAction(actionId: string) {
|
|
@@ -362,8 +330,6 @@ export function createEventController(
|
|
|
362
330
|
actionId,
|
|
363
331
|
setTimeout(() => {
|
|
364
332
|
actionNotifyTimeouts.delete(actionId);
|
|
365
|
-
// Notify all listeners whose subscription ID matches this action
|
|
366
|
-
// This includes exact matches and suffix matches (e.g., "addToCart" matches "hash#addToCart")
|
|
367
333
|
for (const [subscriptionId, listeners] of actionListeners) {
|
|
368
334
|
if (matchesActionId(subscriptionId, actionId)) {
|
|
369
335
|
const state = getActionState(subscriptionId);
|
|
@@ -376,12 +342,7 @@ export function createEventController(
|
|
|
376
342
|
|
|
377
343
|
const notifyHandles = makeDebouncedNotifier(handleListeners);
|
|
378
344
|
|
|
379
|
-
// ========================================================================
|
|
380
|
-
// Derived State
|
|
381
|
-
// ========================================================================
|
|
382
|
-
|
|
383
345
|
function getState(): DerivedNavigationState {
|
|
384
|
-
// Build inflight actions list (for compatibility with existing API)
|
|
385
346
|
const inflightActionsList: InflightAction[] = [...inflightActions.values()]
|
|
386
347
|
.filter((a) => a.phase !== "settling")
|
|
387
348
|
.map((a) => ({
|
|
@@ -391,15 +352,12 @@ export function createEventController(
|
|
|
391
352
|
startedAt: a.startedAt,
|
|
392
353
|
}));
|
|
393
354
|
|
|
394
|
-
// State: loading if navigation OR actions are in progress
|
|
395
|
-
// Background revalidations (skipLoadingState) don't affect visible state
|
|
396
355
|
const hasActiveActions = inflightActionsList.length > 0;
|
|
397
356
|
const isVisibleNavigation =
|
|
398
357
|
currentNavigation !== null &&
|
|
399
358
|
!currentNavigation.options?.skipLoadingState;
|
|
400
359
|
const state = isVisibleNavigation || hasActiveActions ? "loading" : "idle";
|
|
401
360
|
|
|
402
|
-
// Streaming: true if any active streams (navigation or action) or loading
|
|
403
361
|
const isStreaming = activeStreamCount > 0 || state === "loading";
|
|
404
362
|
|
|
405
363
|
return {
|
|
@@ -421,8 +379,6 @@ export function createEventController(
|
|
|
421
379
|
}
|
|
422
380
|
|
|
423
381
|
function getActionState(actionId: string): TrackedActionState {
|
|
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
382
|
const entry = [...inflightActions.values()]
|
|
427
383
|
.filter((a) => matchesActionId(actionId, a.actionId))
|
|
428
384
|
.reduce<ActionEntry | undefined>((best, a) => {
|
|
@@ -437,7 +393,6 @@ export function createEventController(
|
|
|
437
393
|
return { ...DEFAULT_ACTION_STATE };
|
|
438
394
|
}
|
|
439
395
|
|
|
440
|
-
// Derive state from phase
|
|
441
396
|
let state: ActionLifecycleState;
|
|
442
397
|
switch (entry.phase) {
|
|
443
398
|
case "fetching":
|
|
@@ -881,40 +836,3 @@ export function createEventController(
|
|
|
881
836
|
hadAnyConcurrentActions: () => hadAnyConcurrentActions,
|
|
882
837
|
};
|
|
883
838
|
}
|
|
884
|
-
|
|
885
|
-
// ============================================================================
|
|
886
|
-
// Singleton
|
|
887
|
-
// ============================================================================
|
|
888
|
-
|
|
889
|
-
let controllerInstance: EventController | null = null;
|
|
890
|
-
|
|
891
|
-
/**
|
|
892
|
-
* Initialize the global event controller
|
|
893
|
-
*/
|
|
894
|
-
export function initEventController(
|
|
895
|
-
config?: EventControllerConfig,
|
|
896
|
-
): EventController {
|
|
897
|
-
if (!controllerInstance) {
|
|
898
|
-
controllerInstance = createEventController(config);
|
|
899
|
-
}
|
|
900
|
-
return controllerInstance;
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
/**
|
|
904
|
-
* Get the global event controller
|
|
905
|
-
*/
|
|
906
|
-
export function getEventController(): EventController {
|
|
907
|
-
if (!controllerInstance) {
|
|
908
|
-
throw new Error(
|
|
909
|
-
"Event controller not initialized. Call initEventController first.",
|
|
910
|
-
);
|
|
911
|
-
}
|
|
912
|
-
return controllerInstance;
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
/**
|
|
916
|
-
* Reset the controller instance (for testing)
|
|
917
|
-
*/
|
|
918
|
-
export function resetEventController(): void {
|
|
919
|
-
controllerInstance = null;
|
|
920
|
-
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client seat of `invalidateClientCache()` (the `default` export condition).
|
|
3
|
+
*
|
|
4
|
+
* Makes the current client behave as if a server action had just completed:
|
|
5
|
+
* the history cache is marked stale (SWR), the prefetch map is flushed, the
|
|
6
|
+
* state rotates, and sibling tabs are broadcast to — the same
|
|
7
|
+
* `markCacheAsStaleAndBroadcast()` path the server-action bridge uses. This is
|
|
8
|
+
* the gentler mark-stale (not hard-clear) behavior, so Back renders the cached
|
|
9
|
+
* entry instantly and revalidates.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { getRegisteredStore } from "./navigation-store-handle.js";
|
|
13
|
+
import { clearPrefetchCache } from "./prefetch/cache.js";
|
|
14
|
+
|
|
15
|
+
export function invalidateClientCache(): void {
|
|
16
|
+
if (typeof document === "undefined") {
|
|
17
|
+
// SSR pass of a client component also resolves the default condition. A
|
|
18
|
+
// render-time call must not take down the page; no-op with a dev warning.
|
|
19
|
+
if (process.env.NODE_ENV !== "production") {
|
|
20
|
+
console.warn(
|
|
21
|
+
"[rango] invalidateClientCache() was called during a server render; " +
|
|
22
|
+
"it is a no-op outside the browser.",
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const store = getRegisteredStore();
|
|
29
|
+
if (store) {
|
|
30
|
+
store.markCacheAsStaleAndBroadcast();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Pre-boot: no store registered yet. clearPrefetchCache() (which rotates the
|
|
35
|
+
// state) is complete at this point — there is no history cache to mark and no
|
|
36
|
+
// sibling state worth broadcasting.
|
|
37
|
+
clearPrefetchCache();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Client no-op for `keepClientCache()`. It is a server action directive (the
|
|
42
|
+
* `react-server` condition sets a response header the action bridge reads);
|
|
43
|
+
* there is nothing to suppress from the client side.
|
|
44
|
+
*/
|
|
45
|
+
export function keepClientCache(): void {
|
|
46
|
+
if (process.env.NODE_ENV !== "production") {
|
|
47
|
+
console.warn(
|
|
48
|
+
"[rango] keepClientCache() has no effect on the client; it is a server " +
|
|
49
|
+
"action directive. Call it from inside a server action.",
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -5,6 +5,8 @@ import type {
|
|
|
5
5
|
ResolvedSegment,
|
|
6
6
|
} from "./types.js";
|
|
7
7
|
import { setAppVersion } from "./app-version.js";
|
|
8
|
+
import { isActionFenceActive } from "./action-fence.js";
|
|
9
|
+
import { getRangoState } from "./rango-state.js";
|
|
8
10
|
import * as React from "react";
|
|
9
11
|
import { startTransition } from "react";
|
|
10
12
|
import {
|
|
@@ -446,11 +448,22 @@ export function createNavigationBridge(
|
|
|
446
448
|
// Helper to check if streaming is in progress
|
|
447
449
|
const isStreaming = () => eventController.getState().isStreaming;
|
|
448
450
|
|
|
451
|
+
// Surface any external rotation of the rango state cookie (a server
|
|
452
|
+
// Set-Cookie, a sibling tab, a cookie clear) BEFORE reading the stale bit.
|
|
453
|
+
// The divergence observer only runs inside getRangoState() — fetch-time —
|
|
454
|
+
// so a popstate-first interaction would otherwise serve a pre-mutation
|
|
455
|
+
// page as fresh and never fetch to trigger the observer. Reading here lets
|
|
456
|
+
// the observer mark the history cache stale so getCachedSegments sees it.
|
|
457
|
+
getRangoState();
|
|
458
|
+
|
|
449
459
|
// Check if we can restore from history cache
|
|
450
460
|
const cached = store.getCachedSegments(historyKey);
|
|
451
461
|
const cachedSegments = cached?.segments;
|
|
452
462
|
const cachedHandleData = cached?.handleData;
|
|
453
|
-
|
|
463
|
+
// While an action is in flight the fence persists no stale flag, so OR it
|
|
464
|
+
// in here: a popstate during the flight serves the cached entry AND
|
|
465
|
+
// revalidates (SWR) instead of serving it as fresh.
|
|
466
|
+
const isStale = (cached?.stale ?? false) || isActionFenceActive();
|
|
454
467
|
|
|
455
468
|
if (cachedSegments && cachedSegments.length > 0) {
|
|
456
469
|
// Update store to point to this history entry
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
startBrowserTransaction,
|
|
13
13
|
} from "./logging.js";
|
|
14
14
|
import { getRangoState } from "./rango-state.js";
|
|
15
|
+
import { isActionFenceActive } from "./action-fence.js";
|
|
15
16
|
import {
|
|
16
17
|
extractRscHeaderUrl,
|
|
17
18
|
emptyResponse,
|
|
@@ -108,7 +109,14 @@ export function createNavigationClient(
|
|
|
108
109
|
// server-action invalidation) auto-invalidates both scopes.
|
|
109
110
|
// Skip cache for stale revalidation (needs fresh data), HMR (needs
|
|
110
111
|
// fresh modules), and intercept contexts (source-dependent responses).
|
|
111
|
-
|
|
112
|
+
// Suspend prefetch consumption while an action is in flight: a queued
|
|
113
|
+
// prefetch holds pre-mutation data and must not be served until the
|
|
114
|
+
// action's response decides whether anything changed.
|
|
115
|
+
const canUsePrefetch =
|
|
116
|
+
!staleRevalidation &&
|
|
117
|
+
!hmr &&
|
|
118
|
+
!interceptSourceUrl &&
|
|
119
|
+
!isActionFenceActive();
|
|
112
120
|
const rangoState = getRangoState();
|
|
113
121
|
const wildcardKey = buildPrefetchKey(rangoState, fetchUrl);
|
|
114
122
|
const cacheKey = buildSourceKey(rangoState, previousUrl, fetchUrl);
|
|
@@ -207,6 +215,11 @@ export function createNavigationClient(
|
|
|
207
215
|
}
|
|
208
216
|
|
|
209
217
|
return fetch(fetchUrl, {
|
|
218
|
+
// During an action's flight the state is not rotated, so the old
|
|
219
|
+
// X-Rango-State still matches the Vary-keyed HTTP-cache entry; bypass
|
|
220
|
+
// it so a genuine mid-action navigation fetches fresh instead of being
|
|
221
|
+
// served the stale prefetched bytes.
|
|
222
|
+
...(isActionFenceActive() && { cache: "no-store" as RequestCache }),
|
|
210
223
|
headers: {
|
|
211
224
|
"X-RSC-Router-Client-Path": previousUrl,
|
|
212
225
|
"X-Rango-State": getRangoState(),
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A module-level handle to the active navigation store.
|
|
3
|
+
*
|
|
4
|
+
* The boot path (`rsc-router.tsx`) calls `createNavigationStore()` directly;
|
|
5
|
+
* there is no global store singleton. This handle is the live reference for
|
|
6
|
+
* code that needs the store but does not
|
|
7
|
+
* receive it by argument: the jar-divergence observer (below) and the client
|
|
8
|
+
* seat of `invalidateClientCache()` (added later).
|
|
9
|
+
*
|
|
10
|
+
* Dependency-light on purpose: it imports only `setRangoStateObserver` and the
|
|
11
|
+
* store type, so pulling it into the default root entry does not drag the
|
|
12
|
+
* navigation store into bundles that previously lacked it.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { setRangoStateObserver } from "./rango-state.js";
|
|
16
|
+
import type { NavigationStore } from "./types.js";
|
|
17
|
+
|
|
18
|
+
let registeredStore: NavigationStore | null = null;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Register the active navigation store at boot, and wire the jar-divergence
|
|
22
|
+
* observer: when a per-request cookie read detects an EXTERNAL rotation (a
|
|
23
|
+
* sibling tab, a server `Set-Cookie`, or a cookie clear), mark this tab's
|
|
24
|
+
* history cache stale. The history cache is not state-keyed, so the value
|
|
25
|
+
* rotation alone does not reach it. No broadcast, no prefetch clear, no
|
|
26
|
+
* re-rotation — the value already changed externally.
|
|
27
|
+
*/
|
|
28
|
+
export function registerNavigationStore(store: NavigationStore): void {
|
|
29
|
+
registeredStore = store;
|
|
30
|
+
setRangoStateObserver(() => {
|
|
31
|
+
registeredStore?.markHistoryCacheStale();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The active navigation store, or null before boot has registered it. */
|
|
36
|
+
export function getRegisteredStore(): NavigationStore | null {
|
|
37
|
+
return registeredStore;
|
|
38
|
+
}
|
|
@@ -130,14 +130,14 @@ export interface NavigationStoreConfig {
|
|
|
130
130
|
|
|
131
131
|
/**
|
|
132
132
|
* Enable cross-tab cache invalidation via BroadcastChannel (default: true)
|
|
133
|
-
* When cache is cleared (via server actions or
|
|
133
|
+
* When cache is cleared (via server actions or invalidateClientCache()),
|
|
134
134
|
* other tabs will also clear their cache
|
|
135
135
|
*/
|
|
136
136
|
crossTabSync?: boolean;
|
|
137
137
|
|
|
138
138
|
/**
|
|
139
139
|
* Auto-refresh when another tab mutates data on the same path (default: true)
|
|
140
|
-
* Triggered when cache is cleared via server actions or
|
|
140
|
+
* Triggered when cache is cleared via server actions or invalidateClientCache()
|
|
141
141
|
* Requires crossTabSync to be enabled
|
|
142
142
|
*/
|
|
143
143
|
crossTabAutoRefresh?: boolean;
|
|
@@ -335,12 +335,24 @@ export function createNavigationStore(
|
|
|
335
335
|
}
|
|
336
336
|
|
|
337
337
|
/**
|
|
338
|
-
* Mark
|
|
338
|
+
* Mark every history entry stale WITHOUT touching the prefetch caches or the
|
|
339
|
+
* rango state. Used by the jar-divergence observer: an external rotation has
|
|
340
|
+
* already changed the state value (so prefetch/HTTP entries strand under the
|
|
341
|
+
* retired key), and this tab must NOT re-rotate — only the history cache,
|
|
342
|
+
* which is not state-keyed, needs marking.
|
|
339
343
|
*/
|
|
340
|
-
function
|
|
344
|
+
function markHistoryStale(): void {
|
|
341
345
|
for (let i = 0; i < historyCache.length; i++) {
|
|
342
346
|
historyCache[i][2] = true;
|
|
343
347
|
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Mark all cache entries as stale (internal - does not broadcast). Also
|
|
352
|
+
* clears the prefetch caches, which rotates the rango state.
|
|
353
|
+
*/
|
|
354
|
+
function markCacheAsStaleInternal(): void {
|
|
355
|
+
markHistoryStale();
|
|
344
356
|
clearPrefetchCache();
|
|
345
357
|
}
|
|
346
358
|
|
|
@@ -659,6 +671,16 @@ export function createNavigationStore(
|
|
|
659
671
|
markCacheAsStaleInternal();
|
|
660
672
|
},
|
|
661
673
|
|
|
674
|
+
/**
|
|
675
|
+
* Mark every history entry stale WITHOUT clearing the prefetch caches or
|
|
676
|
+
* rotating the rango state. The jar-divergence observer calls this after an
|
|
677
|
+
* external rotation has already changed the state value, so re-rotating
|
|
678
|
+
* here would ping-pong with the tab that rotated.
|
|
679
|
+
*/
|
|
680
|
+
markHistoryCacheStale(): void {
|
|
681
|
+
markHistoryStale();
|
|
682
|
+
},
|
|
683
|
+
|
|
662
684
|
/**
|
|
663
685
|
* Clear the history cache and broadcast to other tabs
|
|
664
686
|
* Use this for hard invalidation when data is definitely stale
|
|
@@ -675,14 +697,6 @@ export function createNavigationStore(
|
|
|
675
697
|
markStaleAndBroadcast();
|
|
676
698
|
},
|
|
677
699
|
|
|
678
|
-
/**
|
|
679
|
-
* Broadcast cache invalidation to other tabs without clearing local cache
|
|
680
|
-
* Used after consolidation fetch where local cache has fresh data
|
|
681
|
-
*/
|
|
682
|
-
broadcastCacheInvalidation(): void {
|
|
683
|
-
broadcastInvalidation();
|
|
684
|
-
},
|
|
685
|
-
|
|
686
700
|
/**
|
|
687
701
|
* Set the callback to invoke when cross-tab refresh is triggered
|
|
688
702
|
* Called by navigation bridge during initialization
|
|
@@ -799,42 +813,3 @@ export function createNavigationStore(
|
|
|
799
813
|
},
|
|
800
814
|
};
|
|
801
815
|
}
|
|
802
|
-
|
|
803
|
-
// Singleton store instance
|
|
804
|
-
let storeInstance: NavigationStore | null = null;
|
|
805
|
-
|
|
806
|
-
/**
|
|
807
|
-
* Initialize the global navigation store
|
|
808
|
-
*
|
|
809
|
-
* Should be called once during app initialization.
|
|
810
|
-
* Subsequent calls return the existing instance.
|
|
811
|
-
*/
|
|
812
|
-
export function initNavigationStore(
|
|
813
|
-
config?: NavigationStoreConfig,
|
|
814
|
-
): NavigationStore {
|
|
815
|
-
if (!storeInstance) {
|
|
816
|
-
storeInstance = createNavigationStore(config);
|
|
817
|
-
}
|
|
818
|
-
return storeInstance;
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
/**
|
|
822
|
-
* Get the global navigation store
|
|
823
|
-
*
|
|
824
|
-
* Throws if store hasn't been initialized.
|
|
825
|
-
*/
|
|
826
|
-
export function getNavigationStore(): NavigationStore {
|
|
827
|
-
if (!storeInstance) {
|
|
828
|
-
throw new Error(
|
|
829
|
-
"Navigation store not initialized. Call initNavigationStore first.",
|
|
830
|
-
);
|
|
831
|
-
}
|
|
832
|
-
return storeInstance;
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
/**
|
|
836
|
-
* Reset the store instance (for testing)
|
|
837
|
-
*/
|
|
838
|
-
export function resetNavigationStore(): void {
|
|
839
|
-
storeInstance = null;
|
|
840
|
-
}
|
|
@@ -13,7 +13,6 @@ import type { EventController, NavigationHandle } from "./event-controller.js";
|
|
|
13
13
|
import { debugLog } from "./logging.js";
|
|
14
14
|
import { buildHistoryState, pushHistoryWithIdx } from "./history-state.js";
|
|
15
15
|
|
|
16
|
-
// Re-export for consumers that import from navigation-transaction
|
|
17
16
|
export { resolveNavigationState } from "./history-state.js";
|
|
18
17
|
|
|
19
18
|
/** Check if a history state object contains location state keys. */
|
|
@@ -25,7 +24,6 @@ function hasLocationState(state: unknown): boolean {
|
|
|
25
24
|
);
|
|
26
25
|
}
|
|
27
26
|
|
|
28
|
-
// Polyfill Symbol.dispose for Safari and older browsers
|
|
29
27
|
if (typeof Symbol.dispose === "undefined") {
|
|
30
28
|
(Symbol as any).dispose = Symbol("Symbol.dispose");
|
|
31
29
|
}
|
|
@@ -114,7 +112,6 @@ export function createNavigationTransaction(
|
|
|
114
112
|
let committed = false;
|
|
115
113
|
const currentUrl = window.location.href;
|
|
116
114
|
|
|
117
|
-
// Start navigation in event controller (this sets loading state)
|
|
118
115
|
const handle = eventController.startNavigation(url, options);
|
|
119
116
|
|
|
120
117
|
/**
|
|
@@ -138,72 +135,50 @@ export function createNavigationTransaction(
|
|
|
138
135
|
|
|
139
136
|
const parsedUrl = new URL(url, window.location.origin);
|
|
140
137
|
|
|
141
|
-
// Generate history key from URL (with intercept suffix for separate caching)
|
|
142
138
|
const historyKey = generateHistoryKey(url, { intercept });
|
|
143
139
|
|
|
144
|
-
// For cache-only commits (stale revalidation), only update cache and return
|
|
145
|
-
// Don't touch store state or history - user may have navigated elsewhere
|
|
146
140
|
if (cacheOnly) {
|
|
147
141
|
const currentHandleData = eventController.getHandleState().data;
|
|
148
142
|
store.cacheSegmentsForHistory(historyKey, segments, currentHandleData);
|
|
149
|
-
// Complete the navigation handle so currentNavigation is cleared.
|
|
150
|
-
// Without this, the entry lingers and weakens state-machine invariants.
|
|
151
143
|
handle.complete(parsedUrl);
|
|
152
144
|
debugLog("[Browser] Cache-only commit, historyKey:", historyKey);
|
|
153
145
|
return { scroll: false };
|
|
154
146
|
}
|
|
155
147
|
|
|
156
|
-
// Save current scroll position before navigating
|
|
157
148
|
handleNavigationStart();
|
|
158
149
|
|
|
159
|
-
// Update segment state atomically
|
|
160
150
|
store.setSegmentIds(segmentIds);
|
|
161
151
|
store.setCurrentUrl(url);
|
|
162
152
|
store.setPath(parsedUrl.pathname);
|
|
163
153
|
|
|
164
154
|
store.setHistoryKey(historyKey);
|
|
165
155
|
|
|
166
|
-
// Cache segments with current handleData for this history entry
|
|
167
156
|
const currentHandleData = eventController.getHandleState().data;
|
|
168
157
|
store.cacheSegmentsForHistory(historyKey, segments, currentHandleData);
|
|
169
158
|
|
|
170
|
-
// For server actions, skip URL/history updates but still complete navigation
|
|
171
159
|
if (storeOnly) {
|
|
172
160
|
debugLog("[Browser] Store updated (action)");
|
|
173
|
-
// Complete navigation to clear loading state
|
|
174
161
|
handle.complete(parsedUrl);
|
|
175
162
|
return { scroll: false };
|
|
176
163
|
}
|
|
177
164
|
|
|
178
|
-
// Build history state - include user state, intercept info, and server-set state
|
|
179
165
|
const historyState = buildHistoryState(
|
|
180
166
|
opts.state,
|
|
181
167
|
{ intercept, sourceUrl: interceptSourceUrl },
|
|
182
168
|
serverState,
|
|
183
169
|
);
|
|
184
170
|
|
|
185
|
-
// Snapshot old state before pushState/replaceState overwrites it.
|
|
186
|
-
// Used to detect when location state is being cleared.
|
|
187
171
|
const oldState = window.history.state;
|
|
188
172
|
|
|
189
|
-
// Update browser URL (stamps history.state.idx for back() first-entry detection)
|
|
190
173
|
pushHistoryWithIdx(historyState, url, replace ?? false);
|
|
191
|
-
// Ensure new history entry has a scroll restoration key
|
|
192
174
|
ensureHistoryKey();
|
|
193
175
|
|
|
194
|
-
// Notify location state hooks when either old or new state carries
|
|
195
|
-
// location state. This covers both "set new state" and "clear old state"
|
|
196
|
-
// for same-page navigations where components don't remount.
|
|
197
176
|
if (hasLocationState(oldState) || hasLocationState(historyState)) {
|
|
198
177
|
window.dispatchEvent(new Event("__rsc_locationstate"));
|
|
199
178
|
}
|
|
200
179
|
|
|
201
|
-
// Complete the navigation in event controller (sets idle state, updates location)
|
|
202
180
|
handle.complete(parsedUrl);
|
|
203
181
|
|
|
204
|
-
// NOTE: Scroll is NOT handled here. The caller (partial-update.ts) handles
|
|
205
|
-
// scroll AFTER onUpdate() so React has the new content before we scroll.
|
|
206
|
-
|
|
207
182
|
debugLog(
|
|
208
183
|
"[Browser] Navigation committed, historyKey:",
|
|
209
184
|
historyKey,
|
|
@@ -217,10 +192,6 @@ export function createNavigationTransaction(
|
|
|
217
192
|
handle,
|
|
218
193
|
commit,
|
|
219
194
|
|
|
220
|
-
/**
|
|
221
|
-
* Create a bound transaction with pre-configured URL options
|
|
222
|
-
* segmentIds and segments provided at commit time (after they're resolved)
|
|
223
|
-
*/
|
|
224
195
|
with(
|
|
225
196
|
opts: Omit<CommitOptions, "segmentIds" | "segments">,
|
|
226
197
|
): BoundTransaction {
|
|
@@ -264,13 +235,10 @@ export function createNavigationTransaction(
|
|
|
264
235
|
},
|
|
265
236
|
|
|
266
237
|
[Symbol.dispose]() {
|
|
267
|
-
// Superseded: another navigation took over.
|
|
268
238
|
if (handle.signal.aborted) {
|
|
269
239
|
return;
|
|
270
240
|
}
|
|
271
241
|
|
|
272
|
-
// Failed (not committed): keep the target URL -- the error UI owns it.
|
|
273
|
-
// Just reset the event controller to idle.
|
|
274
242
|
if (!committed) {
|
|
275
243
|
handle[Symbol.dispose]();
|
|
276
244
|
}
|