@rangojs/router 0.0.0-experimental.15 → 0.0.0-experimental.16
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/vite/index.js +74 -3
- package/package.json +14 -15
- package/src/browser/action-response-classifier.ts +104 -0
- package/src/browser/event-controller.ts +14 -5
- package/src/browser/intercept-utils.ts +56 -0
- package/src/browser/logging.ts +11 -0
- package/src/browser/lru-cache.ts +1 -9
- package/src/browser/merge-segment-loaders.ts +6 -5
- package/src/browser/navigation-bridge.ts +44 -142
- package/src/browser/navigation-client.ts +4 -1
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +77 -169
- package/src/browser/react/use-navigation.ts +9 -2
- package/src/browser/scroll-restoration.ts +71 -5
- package/src/browser/segment-reconciler.ts +216 -0
- package/src/browser/segment-structure-assert.ts +16 -0
- package/src/browser/server-action-bridge.ts +212 -454
- package/src/build/route-trie.ts +14 -7
- package/src/cache/cache-scope.ts +15 -4
- package/src/cache/memory-segment-store.ts +49 -7
- package/src/errors.ts +8 -0
- package/src/prerender/param-hash.ts +4 -2
- package/src/prerender.ts +5 -7
- package/src/router/handler-context.ts +1 -1
- package/src/router/loader-resolution.ts +137 -107
- package/src/router/match-result.ts +16 -2
- package/src/router/pattern-matching.ts +44 -7
- package/src/router/segment-resolution.ts +35 -5
- package/src/router/trie-matching.ts +29 -13
- package/src/rsc/handler.ts +2 -2
- package/src/segment-system.tsx +1 -1
- package/src/server/context.ts +8 -1
- package/src/server/loader-registry.ts +4 -10
- package/src/server/request-context.ts +7 -1
- package/src/vite/index.ts +115 -3
- package/dist/vite/index.named-routes.gen.ts +0 -103
package/dist/vite/index.js
CHANGED
|
@@ -1778,7 +1778,7 @@ import { resolve as resolve2 } from "node:path";
|
|
|
1778
1778
|
// package.json
|
|
1779
1779
|
var package_default = {
|
|
1780
1780
|
name: "@rangojs/router",
|
|
1781
|
-
version: "0.0.0-experimental.
|
|
1781
|
+
version: "0.0.0-experimental.16",
|
|
1782
1782
|
type: "module",
|
|
1783
1783
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
1784
1784
|
author: "Ivo Todorov",
|
|
@@ -2061,7 +2061,7 @@ function createVirtualEntriesPlugin(entries, routerPath) {
|
|
|
2061
2061
|
};
|
|
2062
2062
|
}
|
|
2063
2063
|
function onwarn(warning, defaultHandler) {
|
|
2064
|
-
if (warning.code === "MODULE_LEVEL_DIRECTIVE" || warning.code === "SOURCEMAP_ERROR") {
|
|
2064
|
+
if (warning.code === "MODULE_LEVEL_DIRECTIVE" || warning.code === "SOURCEMAP_ERROR" || warning.code === "EMPTY_BUNDLE") {
|
|
2065
2065
|
return;
|
|
2066
2066
|
}
|
|
2067
2067
|
if (warning.message?.includes("Sourcemap is likely to be incorrect")) {
|
|
@@ -2586,6 +2586,13 @@ ${err.stack}`
|
|
|
2586
2586
|
setTimeout(() => discover().then(resolve4, resolve4), 0);
|
|
2587
2587
|
});
|
|
2588
2588
|
let prerenderTempServer = null;
|
|
2589
|
+
server.httpServer?.on("close", () => {
|
|
2590
|
+
if (prerenderTempServer) {
|
|
2591
|
+
prerenderTempServer.close().catch(() => {
|
|
2592
|
+
});
|
|
2593
|
+
prerenderTempServer = null;
|
|
2594
|
+
}
|
|
2595
|
+
});
|
|
2589
2596
|
let prerenderNodeRegistry = null;
|
|
2590
2597
|
let mainRegistry = null;
|
|
2591
2598
|
server.middlewares.use("/__rsc_prerender", async (req, res) => {
|
|
@@ -2998,6 +3005,53 @@ globalThis.__PRERENDER_MANIFEST = __pm;
|
|
|
2998
3005
|
}
|
|
2999
3006
|
};
|
|
3000
3007
|
}
|
|
3008
|
+
function stripJsonComments(input) {
|
|
3009
|
+
let result = "";
|
|
3010
|
+
let i = 0;
|
|
3011
|
+
const len = input.length;
|
|
3012
|
+
while (i < len) {
|
|
3013
|
+
const ch = input[i];
|
|
3014
|
+
if (ch === '"') {
|
|
3015
|
+
result += ch;
|
|
3016
|
+
i++;
|
|
3017
|
+
while (i < len) {
|
|
3018
|
+
const sc = input[i];
|
|
3019
|
+
result += sc;
|
|
3020
|
+
i++;
|
|
3021
|
+
if (sc === "\\") {
|
|
3022
|
+
if (i < len) {
|
|
3023
|
+
result += input[i];
|
|
3024
|
+
i++;
|
|
3025
|
+
}
|
|
3026
|
+
} else if (sc === '"') {
|
|
3027
|
+
break;
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
continue;
|
|
3031
|
+
}
|
|
3032
|
+
if (ch === "/" && i + 1 < len && input[i + 1] === "/") {
|
|
3033
|
+
i += 2;
|
|
3034
|
+
while (i < len && input[i] !== "\n") {
|
|
3035
|
+
i++;
|
|
3036
|
+
}
|
|
3037
|
+
continue;
|
|
3038
|
+
}
|
|
3039
|
+
if (ch === "/" && i + 1 < len && input[i + 1] === "*") {
|
|
3040
|
+
i += 2;
|
|
3041
|
+
while (i < len) {
|
|
3042
|
+
if (input[i] === "*" && i + 1 < len && input[i + 1] === "/") {
|
|
3043
|
+
i += 2;
|
|
3044
|
+
break;
|
|
3045
|
+
}
|
|
3046
|
+
i++;
|
|
3047
|
+
}
|
|
3048
|
+
continue;
|
|
3049
|
+
}
|
|
3050
|
+
result += ch;
|
|
3051
|
+
i++;
|
|
3052
|
+
}
|
|
3053
|
+
return result;
|
|
3054
|
+
}
|
|
3001
3055
|
var VIRTUAL_ROUTES_MANIFEST_ID = "virtual:rsc-router/routes-manifest";
|
|
3002
3056
|
function resolveDiscoveryEntryPath(options, routerPath) {
|
|
3003
3057
|
if (options.preset === "cloudflare") {
|
|
@@ -3006,7 +3060,7 @@ function resolveDiscoveryEntryPath(options, routerPath) {
|
|
|
3006
3060
|
if (existsSync3(filename)) {
|
|
3007
3061
|
try {
|
|
3008
3062
|
const raw = readFileSync2(filename, "utf-8");
|
|
3009
|
-
const cleaned = raw
|
|
3063
|
+
const cleaned = stripJsonComments(raw);
|
|
3010
3064
|
const config = JSON.parse(cleaned);
|
|
3011
3065
|
if (config.main) {
|
|
3012
3066
|
return config.main;
|
|
@@ -3353,6 +3407,23 @@ ${list}`
|
|
|
3353
3407
|
);
|
|
3354
3408
|
}
|
|
3355
3409
|
}
|
|
3410
|
+
plugins.push({
|
|
3411
|
+
name: "@rangojs/router:client-component-hmr",
|
|
3412
|
+
hotUpdate(ctx) {
|
|
3413
|
+
const envName = this.environment?.name;
|
|
3414
|
+
if (envName !== "rsc" && envName !== "ssr") return;
|
|
3415
|
+
const file = ctx.file;
|
|
3416
|
+
if (!file.endsWith(".tsx") && !file.endsWith(".ts") && !file.endsWith(".jsx") && !file.endsWith(".js")) return;
|
|
3417
|
+
try {
|
|
3418
|
+
const source = readFileSync2(file, "utf-8");
|
|
3419
|
+
const trimmed = source.trimStart();
|
|
3420
|
+
if (trimmed.startsWith('"use client"') || trimmed.startsWith("'use client'")) {
|
|
3421
|
+
return [];
|
|
3422
|
+
}
|
|
3423
|
+
} catch {
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
});
|
|
3356
3427
|
plugins.push(exposeActionId());
|
|
3357
3428
|
plugins.push(exposeInternalIds());
|
|
3358
3429
|
plugins.push(exposeRouterId());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rangojs/router",
|
|
3
|
-
"version": "0.0.0-experimental.
|
|
3
|
+
"version": "0.0.0-experimental.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Django-inspired RSC router with composable URL patterns",
|
|
6
6
|
"author": "Ivo Todorov",
|
|
@@ -127,15 +127,6 @@
|
|
|
127
127
|
"bin": {
|
|
128
128
|
"rango": "./dist/bin/rango.js"
|
|
129
129
|
},
|
|
130
|
-
"scripts": {
|
|
131
|
-
"build": "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && pnpm dlx esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node'",
|
|
132
|
-
"prepublishOnly": "pnpm build",
|
|
133
|
-
"typecheck": "tsc --noEmit",
|
|
134
|
-
"test": "playwright test",
|
|
135
|
-
"test:ui": "playwright test --ui",
|
|
136
|
-
"test:unit": "vitest run",
|
|
137
|
-
"test:unit:watch": "vitest"
|
|
138
|
-
},
|
|
139
130
|
"peerDependencies": {
|
|
140
131
|
"@cloudflare/vite-plugin": "^1.21.0",
|
|
141
132
|
"@vitejs/plugin-rsc": "^0.5.14",
|
|
@@ -159,14 +150,22 @@
|
|
|
159
150
|
"devDependencies": {
|
|
160
151
|
"@playwright/test": "^1.49.1",
|
|
161
152
|
"@types/node": "^24.10.1",
|
|
162
|
-
"@types/react": "
|
|
163
|
-
"@types/react-dom": "
|
|
153
|
+
"@types/react": "^19.2.7",
|
|
154
|
+
"@types/react-dom": "^19.2.3",
|
|
164
155
|
"esbuild": "^0.27.0",
|
|
165
156
|
"jiti": "^2.6.1",
|
|
166
|
-
"react": "
|
|
167
|
-
"react-dom": "
|
|
157
|
+
"react": "^19.2.4",
|
|
158
|
+
"react-dom": "^19.2.4",
|
|
168
159
|
"tinyexec": "^0.3.2",
|
|
169
160
|
"typescript": "^5.3.0",
|
|
170
161
|
"vitest": "^4.0.0"
|
|
162
|
+
},
|
|
163
|
+
"scripts": {
|
|
164
|
+
"build": "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && pnpm dlx esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node'",
|
|
165
|
+
"typecheck": "tsc --noEmit",
|
|
166
|
+
"test": "playwright test",
|
|
167
|
+
"test:ui": "playwright test --ui",
|
|
168
|
+
"test:unit": "vitest run",
|
|
169
|
+
"test:unit:watch": "vitest"
|
|
171
170
|
}
|
|
172
|
-
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discriminated union of post-reconciliation action response scenarios.
|
|
3
|
+
*
|
|
4
|
+
* Error and full-update-unsupported are handled inline in the bridge
|
|
5
|
+
* before reconciliation. This classifier only runs for partial responses
|
|
6
|
+
* that have been successfully reconciled.
|
|
7
|
+
*/
|
|
8
|
+
export type ActionScenario =
|
|
9
|
+
| {
|
|
10
|
+
type: "navigated-away";
|
|
11
|
+
historyKeyChanged: boolean;
|
|
12
|
+
onInterceptRoute: boolean;
|
|
13
|
+
}
|
|
14
|
+
| { type: "hmr-missing" }
|
|
15
|
+
| { type: "consolidation-needed"; segmentIds: string[] }
|
|
16
|
+
| { type: "concurrent-skip"; otherFetchingCount: number }
|
|
17
|
+
| { type: "normal" };
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Pure data inputs for classifying a partial action response.
|
|
21
|
+
* All values come from the bridge but no browser APIs or side effects.
|
|
22
|
+
*/
|
|
23
|
+
export interface ClassifierInput {
|
|
24
|
+
/** window.location.pathname captured at action start */
|
|
25
|
+
actionStartPathname: string;
|
|
26
|
+
/** window.location.pathname at classification time */
|
|
27
|
+
currentPathname: string;
|
|
28
|
+
/** window.history.state?.key captured at action start */
|
|
29
|
+
actionStartLocationKey: string | undefined;
|
|
30
|
+
/** window.history.state?.key at classification time */
|
|
31
|
+
currentLocationKey: string | undefined;
|
|
32
|
+
/** Number of segments after reconciliation */
|
|
33
|
+
reconciledSegmentCount: number;
|
|
34
|
+
/** Number of matched segment IDs from server */
|
|
35
|
+
matchedCount: number;
|
|
36
|
+
/** Segment IDs needing consolidation (from concurrent action tracking) */
|
|
37
|
+
consolidationSegments: string[] | null;
|
|
38
|
+
/** Number of other actions still in "fetching" phase */
|
|
39
|
+
otherFetchingActionCount: number;
|
|
40
|
+
/** Current intercept source URL (null when not on intercept route) */
|
|
41
|
+
currentInterceptSource: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Classify a partial action response into one of 5 post-reconciliation
|
|
46
|
+
* scenarios.
|
|
47
|
+
*
|
|
48
|
+
* Called after error and full-update cases are handled inline by the bridge.
|
|
49
|
+
* The classification order matches the priority chain:
|
|
50
|
+
* 1. User navigated away during action
|
|
51
|
+
* 2. HMR missing segments (fewer reconciled than matched)
|
|
52
|
+
* 3. Consolidation needed (concurrent actions finished)
|
|
53
|
+
* 4. Concurrent skip (other actions still fetching)
|
|
54
|
+
* 5. Normal (single action, no issues)
|
|
55
|
+
*
|
|
56
|
+
* This is a pure function with no side effects - the bridge handles
|
|
57
|
+
* all UI updates, store mutations, and network requests based on the
|
|
58
|
+
* returned scenario.
|
|
59
|
+
*/
|
|
60
|
+
export function classifyActionResponse(
|
|
61
|
+
input: ClassifierInput,
|
|
62
|
+
): ActionScenario {
|
|
63
|
+
// Check if user navigated away during the action
|
|
64
|
+
const userNavigatedAway =
|
|
65
|
+
input.currentPathname !== input.actionStartPathname ||
|
|
66
|
+
input.currentLocationKey !== input.actionStartLocationKey;
|
|
67
|
+
|
|
68
|
+
if (userNavigatedAway) {
|
|
69
|
+
const historyKeyChanged =
|
|
70
|
+
input.currentLocationKey !== input.actionStartLocationKey;
|
|
71
|
+
return {
|
|
72
|
+
type: "navigated-away",
|
|
73
|
+
historyKeyChanged,
|
|
74
|
+
onInterceptRoute: input.currentInterceptSource !== null,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// HMR resilience: segments missing after reconciliation
|
|
79
|
+
if (input.reconciledSegmentCount < input.matchedCount) {
|
|
80
|
+
return { type: "hmr-missing" };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Consolidation needed for concurrent actions
|
|
84
|
+
if (
|
|
85
|
+
input.consolidationSegments &&
|
|
86
|
+
input.consolidationSegments.length > 0
|
|
87
|
+
) {
|
|
88
|
+
return {
|
|
89
|
+
type: "consolidation-needed",
|
|
90
|
+
segmentIds: input.consolidationSegments,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Other actions still fetching - skip UI update
|
|
95
|
+
if (input.otherFetchingActionCount > 0) {
|
|
96
|
+
return {
|
|
97
|
+
type: "concurrent-skip",
|
|
98
|
+
otherFetchingCount: input.otherFetchingActionCount,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Normal single-action completion
|
|
103
|
+
return { type: "normal" };
|
|
104
|
+
}
|
|
@@ -40,7 +40,7 @@ export interface NavigationEntry {
|
|
|
40
40
|
abort: AbortController;
|
|
41
41
|
phase: NavigationPhase;
|
|
42
42
|
startedAt: number;
|
|
43
|
-
options?: NavigateOptions;
|
|
43
|
+
options?: NavigateOptions & { skipLoadingState?: boolean };
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
@@ -176,7 +176,7 @@ export interface ActionHandle extends Disposable {
|
|
|
176
176
|
*/
|
|
177
177
|
export interface EventController {
|
|
178
178
|
// Navigation operations
|
|
179
|
-
startNavigation(url: string, options?: NavigateOptions): NavigationHandle;
|
|
179
|
+
startNavigation(url: string, options?: NavigateOptions & { skipLoadingState?: boolean }): NavigationHandle;
|
|
180
180
|
abortNavigation(): void;
|
|
181
181
|
|
|
182
182
|
// Action operations
|
|
@@ -367,9 +367,13 @@ export function createEventController(
|
|
|
367
367
|
}));
|
|
368
368
|
|
|
369
369
|
// State: loading if navigation OR actions are in progress
|
|
370
|
+
// Background revalidations (skipLoadingState) don't affect visible state
|
|
370
371
|
const hasActiveActions = inflightActionsList.length > 0;
|
|
372
|
+
const isVisibleNavigation =
|
|
373
|
+
currentNavigation !== null &&
|
|
374
|
+
!currentNavigation.options?.skipLoadingState;
|
|
371
375
|
const state =
|
|
372
|
-
|
|
376
|
+
isVisibleNavigation || hasActiveActions ? "loading" : "idle";
|
|
373
377
|
|
|
374
378
|
// Streaming: true if any active streams (navigation or action) or loading
|
|
375
379
|
const isStreaming = activeStreamCount > 0 || state === "loading";
|
|
@@ -379,7 +383,12 @@ export function createEventController(
|
|
|
379
383
|
isStreaming,
|
|
380
384
|
location,
|
|
381
385
|
// pendingUrl only during fetching phase - once streaming starts (URL changed), not pending
|
|
382
|
-
|
|
386
|
+
// Background revalidations don't expose a pending URL
|
|
387
|
+
pendingUrl:
|
|
388
|
+
currentNavigation?.phase === "fetching" &&
|
|
389
|
+
!currentNavigation.options?.skipLoadingState
|
|
390
|
+
? currentNavigation.url
|
|
391
|
+
: null,
|
|
383
392
|
inflightActions: inflightActionsList,
|
|
384
393
|
};
|
|
385
394
|
}
|
|
@@ -431,7 +440,7 @@ export function createEventController(
|
|
|
431
440
|
|
|
432
441
|
function startNavigation(
|
|
433
442
|
url: string,
|
|
434
|
-
options?: NavigateOptions
|
|
443
|
+
options?: NavigateOptions & { skipLoadingState?: boolean }
|
|
435
444
|
): NavigationHandle {
|
|
436
445
|
// Cancel existing navigation (switchMap semantics)
|
|
437
446
|
if (currentNavigation) {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ResolvedSegment } from "./types.js";
|
|
2
|
+
import type { SlotState } from "../types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Check if a segment is an intercept segment.
|
|
6
|
+
* Intercept segments have namespace starting with "intercept:" or are parallel
|
|
7
|
+
* segments with ".@" in their ID (e.g., "L0.@modal").
|
|
8
|
+
*/
|
|
9
|
+
export function isInterceptSegment(s: ResolvedSegment): boolean {
|
|
10
|
+
return (
|
|
11
|
+
s.namespace?.startsWith("intercept:") ||
|
|
12
|
+
(s.type === "parallel" && s.id.includes(".@"))
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Split an array of segments into main and intercept groups.
|
|
18
|
+
* Intercept segments are separated for explicit injection into the render tree
|
|
19
|
+
* via the interceptSegments render option.
|
|
20
|
+
*/
|
|
21
|
+
export function splitInterceptSegments(segments: ResolvedSegment[]): {
|
|
22
|
+
main: ResolvedSegment[];
|
|
23
|
+
intercept: ResolvedSegment[];
|
|
24
|
+
} {
|
|
25
|
+
const main: ResolvedSegment[] = [];
|
|
26
|
+
const intercept: ResolvedSegment[] = [];
|
|
27
|
+
for (const s of segments) {
|
|
28
|
+
if (isInterceptSegment(s)) {
|
|
29
|
+
intercept.push(s);
|
|
30
|
+
} else {
|
|
31
|
+
main.push(s);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { main, intercept };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if any slot is currently active (has content to render).
|
|
39
|
+
* Active slots indicate an intercept response where a parallel segment
|
|
40
|
+
* (e.g., @modal) has matched and should be rendered.
|
|
41
|
+
*/
|
|
42
|
+
export function hasActiveIntercept(
|
|
43
|
+
slots?: Record<string, SlotState>,
|
|
44
|
+
): boolean {
|
|
45
|
+
if (!slots) return false;
|
|
46
|
+
return Object.values(slots).some((slot) => slot.active);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check if cached segments contain any intercept segments.
|
|
51
|
+
* Intercept caches shouldn't be used for optimistic rendering since
|
|
52
|
+
* whether interception happens depends on the current page context.
|
|
53
|
+
*/
|
|
54
|
+
export function isInterceptOnlyCache(segments: ResolvedSegment[]): boolean {
|
|
55
|
+
return segments.some(isInterceptSegment);
|
|
56
|
+
}
|
package/src/browser/logging.ts
CHANGED
|
@@ -42,3 +42,14 @@ export function browserDebugLog(
|
|
|
42
42
|
|
|
43
43
|
console.log(`${prefix} ${message}`);
|
|
44
44
|
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Simple gated console.log for browser-side debug output.
|
|
48
|
+
* Unlike browserDebugLog, this doesn't require a transaction context -
|
|
49
|
+
* use it for standalone debug messages in partial-update, navigation-bridge, etc.
|
|
50
|
+
*/
|
|
51
|
+
export function debugLog(msg: string, ...args: unknown[]): void {
|
|
52
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
53
|
+
console.log(msg, ...args);
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/browser/lru-cache.ts
CHANGED
|
@@ -40,15 +40,7 @@ export class LRUCache<K, V> {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
has(key: K): boolean {
|
|
43
|
-
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Move to end (most recently used) - same as get()
|
|
48
|
-
const value = this.cache.get(key)!;
|
|
49
|
-
this.cache.delete(key);
|
|
50
|
-
this.cache.set(key, value);
|
|
51
|
-
return true;
|
|
43
|
+
return this.cache.has(key);
|
|
52
44
|
}
|
|
53
45
|
|
|
54
46
|
delete(key: K): boolean {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ResolvedSegment } from "./types.js";
|
|
2
|
+
import { debugLog } from "./logging.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Merge partial loader data from server with cached loader data.
|
|
@@ -18,8 +19,8 @@ export function mergeSegmentLoaders(
|
|
|
18
19
|
const serverLoaderIds = fromServer.loaderIds || [];
|
|
19
20
|
const cachedLoaderIds = fromCache.loaderIds || [];
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
`[Browser] Merging partial loaders: server has ${serverLoaderIds.join(", ")}, cache has ${cachedLoaderIds.join(", ")}
|
|
22
|
+
debugLog(
|
|
23
|
+
`[Browser] Merging partial loaders: server has ${serverLoaderIds.join(", ")}, cache has ${cachedLoaderIds.join(", ")}`,
|
|
23
24
|
);
|
|
24
25
|
|
|
25
26
|
return {
|
|
@@ -105,8 +106,8 @@ export function insertMissingDiffSegments(
|
|
|
105
106
|
if (parentIndex !== -1) {
|
|
106
107
|
// Insert loader segment right after its parent layout
|
|
107
108
|
allSegments.splice(parentIndex + 1, 0, fromServer);
|
|
108
|
-
|
|
109
|
-
`[Browser] Inserted diff segment ${diffId} after ${parentLayoutId}
|
|
109
|
+
debugLog(
|
|
110
|
+
`[Browser] Inserted diff segment ${diffId} after ${parentLayoutId}`,
|
|
110
111
|
);
|
|
111
112
|
} else {
|
|
112
113
|
// Fallback: append to end if parent not found
|
|
@@ -118,7 +119,7 @@ export function insertMissingDiffSegments(
|
|
|
118
119
|
} else {
|
|
119
120
|
// Non-loader diff segment not in matched - append to end
|
|
120
121
|
allSegments.push(fromServer);
|
|
121
|
-
|
|
122
|
+
debugLog(`[Browser] Appended diff segment ${diffId}`);
|
|
122
123
|
}
|
|
123
124
|
}
|
|
124
125
|
}
|