@rangojs/router 0.0.0-experimental.8 → 0.0.0-experimental.8a4d0430
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/AGENTS.md +5 -0
- package/README.md +884 -4
- package/dist/bin/rango.js +1601 -0
- package/dist/vite/index.js +4474 -867
- package/package.json +60 -51
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +50 -21
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +12 -8
- package/skills/document-cache/SKILL.md +18 -16
- package/skills/fonts/SKILL.md +167 -0
- package/skills/hooks/SKILL.md +334 -72
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +131 -8
- package/skills/layout/SKILL.md +100 -3
- package/skills/links/SKILL.md +89 -30
- package/skills/loader/SKILL.md +388 -38
- package/skills/middleware/SKILL.md +171 -34
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +78 -1
- package/skills/prerender/SKILL.md +643 -0
- package/skills/rango/SKILL.md +85 -16
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +226 -14
- package/skills/router-setup/SKILL.md +123 -30
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +318 -89
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +102 -4
- package/src/bin/rango.ts +321 -0
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/event-controller.ts +87 -64
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +24 -4
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +285 -553
- package/src/browser/navigation-client.ts +124 -71
- package/src/browser/navigation-store.ts +33 -50
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +258 -308
- package/src/browser/prefetch/cache.ts +146 -0
- package/src/browser/prefetch/fetch.ts +135 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/prefetch/queue.ts +88 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +185 -73
- package/src/browser/react/NavigationProvider.tsx +51 -11
- package/src/browser/react/context.ts +6 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +12 -12
- package/src/browser/react/location-state-shared.ts +95 -53
- package/src/browser/react/location-state.ts +60 -15
- package/src/browser/react/mount-context.ts +6 -1
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +29 -51
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +32 -79
- package/src/browser/react/use-href.tsx +2 -2
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-navigation.ts +22 -63
- package/src/browser/react/use-params.ts +65 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +63 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +80 -97
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +107 -26
- package/src/browser/scroll-restoration.ts +92 -16
- package/src/browser/segment-reconciler.ts +216 -0
- package/src/browser/segment-structure-assert.ts +16 -0
- package/src/browser/server-action-bridge.ts +504 -599
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +109 -47
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +235 -24
- package/src/build/generate-route-types.ts +36 -0
- package/src/build/index.ts +13 -0
- package/src/build/route-trie.ts +265 -0
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +411 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +469 -0
- package/src/build/route-types/scan-filter.ts +78 -0
- package/src/build/runtime-discovery.ts +231 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +338 -0
- package/src/cache/cache-scope.ts +120 -303
- package/src/cache/cf/cf-cache-store.ts +119 -7
- package/src/cache/cf/index.ts +8 -2
- package/src/cache/document-cache.ts +101 -72
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +0 -15
- package/src/cache/memory-segment-store.ts +191 -13
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +98 -0
- package/src/cache/types.ts +72 -122
- package/src/client.rsc.tsx +3 -1
- package/src/client.tsx +106 -126
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +86 -0
- package/src/debug.ts +17 -7
- package/src/errors.ts +108 -2
- package/src/handle.ts +15 -29
- package/src/handles/MetaTags.tsx +73 -20
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/handles/meta.ts +30 -13
- package/src/host/cookie-handler.ts +21 -15
- package/src/host/errors.ts +8 -8
- package/src/host/index.ts +4 -7
- package/src/host/pattern-matcher.ts +27 -27
- package/src/host/router.ts +61 -39
- package/src/host/testing.ts +8 -8
- package/src/host/types.ts +15 -7
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +119 -29
- package/src/index.rsc.ts +153 -19
- package/src/index.ts +211 -30
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +26 -157
- package/src/loader.ts +27 -10
- package/src/network-error-thrower.tsx +3 -1
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +37 -0
- package/src/prerender/store.ts +185 -0
- package/src/prerender.ts +463 -0
- package/src/reverse.ts +330 -0
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +934 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +430 -0
- package/src/route-definition/index.ts +52 -0
- package/src/route-definition/redirect.ts +93 -0
- package/src/route-definition.ts +1 -1428
- package/src/route-map-builder.ts +211 -123
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +59 -8
- package/src/router/content-negotiation.ts +116 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +9 -9
- package/src/router/find-match.ts +158 -0
- package/src/router/handler-context.ts +374 -81
- package/src/router/intercept-resolution.ts +395 -0
- package/src/router/lazy-includes.ts +234 -0
- package/src/router/loader-resolution.ts +215 -122
- package/src/router/logging.ts +248 -0
- package/src/router/manifest.ts +148 -35
- package/src/router/match-api.ts +620 -0
- package/src/router/match-context.ts +5 -3
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +80 -93
- package/src/router/match-middleware/cache-lookup.ts +382 -9
- package/src/router/match-middleware/cache-store.ts +51 -22
- package/src/router/match-middleware/intercept-resolution.ts +55 -17
- package/src/router/match-middleware/segment-resolution.ts +24 -6
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +34 -28
- package/src/router/metrics.ts +235 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +324 -367
- package/src/router/pattern-matching.ts +211 -43
- package/src/router/prerender-match.ts +402 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +137 -38
- package/src/router/router-context.ts +36 -21
- package/src/router/router-interfaces.ts +452 -0
- package/src/router/router-options.ts +592 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +570 -0
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +198 -0
- package/src/router/segment-resolution/revalidation.ts +1241 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -0
- package/src/router/segment-wrappers.ts +289 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +239 -0
- package/src/router/types.ts +77 -3
- package/src/router.ts +692 -4257
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +764 -754
- package/src/rsc/helpers.ts +140 -6
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +209 -0
- package/src/rsc/manifest-init.ts +86 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +379 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +235 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +348 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +38 -11
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +25 -13
- package/src/server/context.ts +182 -51
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +94 -15
- package/src/server/loader-registry.ts +15 -56
- package/src/server/request-context.ts +430 -70
- package/src/server.ts +35 -130
- package/src/ssr/index.tsx +100 -31
- package/src/static-handler.ts +114 -0
- package/src/theme/ThemeProvider.tsx +21 -15
- package/src/theme/ThemeScript.tsx +5 -5
- package/src/theme/constants.ts +5 -2
- package/src/theme/index.ts +4 -14
- package/src/theme/theme-context.ts +4 -30
- package/src/theme/theme-script.ts +21 -18
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +687 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +183 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +102 -0
- package/src/types/segments.ts +148 -0
- package/src/types.ts +1 -1623
- package/src/urls/include-helper.ts +197 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +339 -0
- package/src/urls/path-helper.ts +329 -0
- package/src/urls/pattern-types.ts +95 -0
- package/src/urls/response-types.ts +106 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -802
- package/src/use-loader.tsx +85 -77
- package/src/vite/discovery/bundle-postprocess.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +344 -0
- package/src/vite/discovery/prerender-collection.ts +385 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +47 -0
- package/src/vite/discovery/state.ts +110 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +11 -1133
- package/src/vite/plugin-types.ts +131 -0
- package/src/vite/plugins/cjs-to-esm.ts +93 -0
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +105 -0
- package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -51
- package/src/vite/plugins/expose-id-utils.ts +287 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +569 -0
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +323 -0
- package/src/vite/plugins/version-injector.ts +83 -0
- package/src/vite/plugins/version-plugin.ts +254 -0
- package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +510 -0
- package/src/vite/router-discovery.ts +785 -0
- package/src/vite/utils/ast-handler-extract.ts +517 -0
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
- package/src/vite/utils/prerender-utils.ts +189 -0
- package/src/vite/utils/shared-utils.ts +169 -0
- package/CLAUDE.md +0 -43
- package/src/browser/lru-cache.ts +0 -69
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/href.ts +0 -255
- package/src/server/route-manifest-cache.ts +0 -173
- package/src/vite/expose-handle-id.ts +0 -209
- package/src/vite/expose-loader-id.ts +0 -426
- package/src/vite/expose-location-state-id.ts +0 -177
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
|
@@ -8,7 +8,9 @@ import type {
|
|
|
8
8
|
ResolvedSegment,
|
|
9
9
|
RscMetadata,
|
|
10
10
|
HandleData,
|
|
11
|
+
StreamingToken,
|
|
11
12
|
} from "./types.js";
|
|
13
|
+
import { filterSegmentOrder } from "./react/filter-segment-order.js";
|
|
12
14
|
|
|
13
15
|
// Polyfill Symbol.dispose for Safari and older browsers
|
|
14
16
|
if (typeof Symbol.dispose === "undefined") {
|
|
@@ -40,7 +42,7 @@ export interface NavigationEntry {
|
|
|
40
42
|
abort: AbortController;
|
|
41
43
|
phase: NavigationPhase;
|
|
42
44
|
startedAt: number;
|
|
43
|
-
options?: NavigateOptions;
|
|
45
|
+
options?: NavigateOptions & { skipLoadingState?: boolean };
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
/**
|
|
@@ -116,15 +118,6 @@ export interface HandleState {
|
|
|
116
118
|
segmentOrder: string[];
|
|
117
119
|
}
|
|
118
120
|
|
|
119
|
-
/**
|
|
120
|
-
* Token for tracking an active stream
|
|
121
|
-
* Call end() when the stream completes
|
|
122
|
-
*/
|
|
123
|
-
export interface StreamingToken {
|
|
124
|
-
/** End this streaming operation */
|
|
125
|
-
end(): void;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
121
|
/**
|
|
129
122
|
* Result from starting a navigation
|
|
130
123
|
* Implements Disposable for use with `using` keyword
|
|
@@ -165,8 +158,8 @@ export interface ActionHandle extends Disposable {
|
|
|
165
158
|
readonly settled: boolean;
|
|
166
159
|
/** Check if any concurrent actions were started */
|
|
167
160
|
hadConcurrentActions: boolean;
|
|
168
|
-
/** Get
|
|
169
|
-
|
|
161
|
+
/** Get raw set of segments revalidated by concurrent actions */
|
|
162
|
+
getRevalidatedSegments(): Set<string>;
|
|
170
163
|
/** Clear consolidation tracking */
|
|
171
164
|
clearConsolidation(): void;
|
|
172
165
|
}
|
|
@@ -176,7 +169,10 @@ export interface ActionHandle extends Disposable {
|
|
|
176
169
|
*/
|
|
177
170
|
export interface EventController {
|
|
178
171
|
// Navigation operations
|
|
179
|
-
startNavigation(
|
|
172
|
+
startNavigation(
|
|
173
|
+
url: string,
|
|
174
|
+
options?: NavigateOptions & { skipLoadingState?: boolean },
|
|
175
|
+
): NavigationHandle;
|
|
180
176
|
abortNavigation(): void;
|
|
181
177
|
|
|
182
178
|
// Action operations
|
|
@@ -186,6 +182,7 @@ export interface EventController {
|
|
|
186
182
|
// State access
|
|
187
183
|
getState(): DerivedNavigationState;
|
|
188
184
|
getActionState(actionId: string): TrackedActionState;
|
|
185
|
+
getLocation(): NavigationLocation;
|
|
189
186
|
|
|
190
187
|
// Location updates (for popstate where navigation doesn't go through startNavigation)
|
|
191
188
|
setLocation(location: NavigationLocation): void;
|
|
@@ -194,7 +191,7 @@ export interface EventController {
|
|
|
194
191
|
subscribe(listener: StateListener): () => void;
|
|
195
192
|
subscribeToAction(
|
|
196
193
|
actionId: string,
|
|
197
|
-
listener: ActionStateListener
|
|
194
|
+
listener: ActionStateListener,
|
|
198
195
|
): () => void;
|
|
199
196
|
subscribeToHandles(listener: HandleListener): () => void;
|
|
200
197
|
|
|
@@ -202,13 +199,19 @@ export interface EventController {
|
|
|
202
199
|
setHandleData(
|
|
203
200
|
data: HandleData,
|
|
204
201
|
matched?: string[],
|
|
205
|
-
isPartial?: boolean
|
|
202
|
+
isPartial?: boolean,
|
|
206
203
|
): void;
|
|
207
204
|
getHandleState(): HandleState;
|
|
208
205
|
|
|
206
|
+
// Params operations
|
|
207
|
+
setParams(params: Record<string, string>): void;
|
|
208
|
+
getParams(): Record<string, string>;
|
|
209
|
+
|
|
209
210
|
// Direct state access for advanced use
|
|
210
211
|
getCurrentNavigation(): NavigationEntry | null;
|
|
211
212
|
getInflightActions(): Map<string, ActionEntry>;
|
|
213
|
+
/** Whether any concurrent actions have occurred (shared across all handles) */
|
|
214
|
+
hadAnyConcurrentActions(): boolean;
|
|
212
215
|
}
|
|
213
216
|
|
|
214
217
|
// ============================================================================
|
|
@@ -230,7 +233,10 @@ const DEFAULT_ACTION_STATE: TrackedActionState = {
|
|
|
230
233
|
* When subscriptionId has no '#', it's just an action name and matches by suffix.
|
|
231
234
|
* This allows useAction("addToCart") to match "hash#addToCart" or "src/file.ts#addToCart".
|
|
232
235
|
*/
|
|
233
|
-
function matchesActionId(
|
|
236
|
+
function matchesActionId(
|
|
237
|
+
subscriptionId: string,
|
|
238
|
+
entryActionId: string,
|
|
239
|
+
): boolean {
|
|
234
240
|
if (subscriptionId.includes("#")) {
|
|
235
241
|
// Full ID: exact match
|
|
236
242
|
return subscriptionId === entryActionId;
|
|
@@ -261,7 +267,7 @@ export interface EventControllerConfig {
|
|
|
261
267
|
* Actions use mergeMap semantics (all run concurrently, consolidate at end).
|
|
262
268
|
*/
|
|
263
269
|
export function createEventController(
|
|
264
|
-
config?: EventControllerConfig
|
|
270
|
+
config?: EventControllerConfig,
|
|
265
271
|
): EventController {
|
|
266
272
|
// ========================================================================
|
|
267
273
|
// Source of Truth
|
|
@@ -293,6 +299,9 @@ export function createEventController(
|
|
|
293
299
|
let handleData: HandleData = {};
|
|
294
300
|
let handleSegmentOrder: string[] = [];
|
|
295
301
|
|
|
302
|
+
// Merged route params from current match
|
|
303
|
+
let routeParams: Record<string, string> = {};
|
|
304
|
+
|
|
296
305
|
// ========================================================================
|
|
297
306
|
// Listeners
|
|
298
307
|
// ========================================================================
|
|
@@ -334,7 +343,7 @@ export function createEventController(
|
|
|
334
343
|
listeners.forEach((listener) => listener(state));
|
|
335
344
|
}
|
|
336
345
|
}
|
|
337
|
-
}, 0)
|
|
346
|
+
}, 0),
|
|
338
347
|
);
|
|
339
348
|
}
|
|
340
349
|
|
|
@@ -367,9 +376,12 @@ export function createEventController(
|
|
|
367
376
|
}));
|
|
368
377
|
|
|
369
378
|
// State: loading if navigation OR actions are in progress
|
|
379
|
+
// Background revalidations (skipLoadingState) don't affect visible state
|
|
370
380
|
const hasActiveActions = inflightActionsList.length > 0;
|
|
371
|
-
const
|
|
372
|
-
currentNavigation !== null
|
|
381
|
+
const isVisibleNavigation =
|
|
382
|
+
currentNavigation !== null &&
|
|
383
|
+
!currentNavigation.options?.skipLoadingState;
|
|
384
|
+
const state = isVisibleNavigation || hasActiveActions ? "loading" : "idle";
|
|
373
385
|
|
|
374
386
|
// Streaming: true if any active streams (navigation or action) or loading
|
|
375
387
|
const isStreaming = activeStreamCount > 0 || state === "loading";
|
|
@@ -378,8 +390,13 @@ export function createEventController(
|
|
|
378
390
|
state,
|
|
379
391
|
isStreaming,
|
|
380
392
|
location,
|
|
381
|
-
// pendingUrl only during fetching phase - once streaming starts (URL changed), not pending
|
|
382
|
-
|
|
393
|
+
// pendingUrl only during fetching phase - once streaming starts (URL changed), not pending.
|
|
394
|
+
// Background revalidations (skipLoadingState) don't expose a pending URL.
|
|
395
|
+
pendingUrl:
|
|
396
|
+
currentNavigation?.phase === "fetching" &&
|
|
397
|
+
!currentNavigation.options?.skipLoadingState
|
|
398
|
+
? currentNavigation.url
|
|
399
|
+
: null,
|
|
383
400
|
inflightActions: inflightActionsList,
|
|
384
401
|
};
|
|
385
402
|
}
|
|
@@ -388,12 +405,16 @@ export function createEventController(
|
|
|
388
405
|
// Find the most recent action with this ID that's not settling
|
|
389
406
|
// Uses suffix matching when actionId is just a name (no #)
|
|
390
407
|
const activeEntry = [...inflightActions.values()]
|
|
391
|
-
.filter(
|
|
408
|
+
.filter(
|
|
409
|
+
(a) => matchesActionId(actionId, a.actionId) && a.phase !== "settling",
|
|
410
|
+
)
|
|
392
411
|
.sort((a, b) => b.startedAt - a.startedAt)[0];
|
|
393
412
|
|
|
394
413
|
// Also check for settling entries to get result/error
|
|
395
414
|
const settlingEntry = [...inflightActions.values()]
|
|
396
|
-
.filter(
|
|
415
|
+
.filter(
|
|
416
|
+
(a) => matchesActionId(actionId, a.actionId) && a.phase === "settling",
|
|
417
|
+
)
|
|
397
418
|
.sort((a, b) => b.startedAt - a.startedAt)[0];
|
|
398
419
|
|
|
399
420
|
const entry = activeEntry || settlingEntry;
|
|
@@ -431,7 +452,7 @@ export function createEventController(
|
|
|
431
452
|
|
|
432
453
|
function startNavigation(
|
|
433
454
|
url: string,
|
|
434
|
-
options?: NavigateOptions
|
|
455
|
+
options?: NavigateOptions & { skipLoadingState?: boolean },
|
|
435
456
|
): NavigationHandle {
|
|
436
457
|
// Cancel existing navigation (switchMap semantics)
|
|
437
458
|
if (currentNavigation) {
|
|
@@ -463,6 +484,7 @@ export function createEventController(
|
|
|
463
484
|
|
|
464
485
|
startStreaming(): StreamingToken {
|
|
465
486
|
let ended = false;
|
|
487
|
+
entry.phase = "streaming";
|
|
466
488
|
activeStreamCount++;
|
|
467
489
|
notify();
|
|
468
490
|
|
|
@@ -650,24 +672,8 @@ export function createEventController(
|
|
|
650
672
|
// If streaming is in progress, tryFinalize() will be called when streaming ends
|
|
651
673
|
},
|
|
652
674
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
// We don't need to wait for streaming to complete since we're refetching anyway
|
|
656
|
-
// Count actions that are still fetching (waiting for server response)
|
|
657
|
-
const stillFetchingCount = [...inflightActions.values()].filter(
|
|
658
|
-
(a) => a.phase === "fetching"
|
|
659
|
-
).length;
|
|
660
|
-
|
|
661
|
-
if (stillFetchingCount > 0) {
|
|
662
|
-
return null; // Some actions still waiting for server response
|
|
663
|
-
}
|
|
664
|
-
if (!hadAnyConcurrentActions) {
|
|
665
|
-
return null; // No concurrent actions occurred
|
|
666
|
-
}
|
|
667
|
-
if (concurrentRevalidatedSegments.size === 0) {
|
|
668
|
-
return null; // No segments to consolidate
|
|
669
|
-
}
|
|
670
|
-
return Array.from(concurrentRevalidatedSegments);
|
|
675
|
+
getRevalidatedSegments(): Set<string> {
|
|
676
|
+
return concurrentRevalidatedSegments;
|
|
671
677
|
},
|
|
672
678
|
|
|
673
679
|
clearConsolidation() {
|
|
@@ -702,16 +708,26 @@ export function createEventController(
|
|
|
702
708
|
}
|
|
703
709
|
|
|
704
710
|
function abortAllActions() {
|
|
705
|
-
for (const entry of inflightActions
|
|
711
|
+
for (const [id, entry] of inflightActions) {
|
|
712
|
+
// Preserve settling entries — they have already been handled by
|
|
713
|
+
// fail()/complete() and will self-cleanup via the settlement timeout.
|
|
714
|
+
// Clearing them here would prevent debounced notifications from
|
|
715
|
+
// delivering the error/result state to subscribers.
|
|
716
|
+
if (entry.phase === "settling") continue;
|
|
706
717
|
entry.abort.abort();
|
|
718
|
+
inflightActions.delete(id);
|
|
707
719
|
}
|
|
708
|
-
inflightActions.clear();
|
|
709
720
|
hadAnyConcurrentActions = false;
|
|
710
721
|
concurrentRevalidatedSegments.clear();
|
|
711
722
|
notify();
|
|
712
|
-
// Notify all action listeners
|
|
713
|
-
|
|
714
|
-
|
|
723
|
+
// Notify all action listeners directly by subscription ID.
|
|
724
|
+
// actionListeners keys are subscription IDs (possibly short names like
|
|
725
|
+
// "addToCart"), not full entry actionIds. Passing them to notifyAction
|
|
726
|
+
// would fail the suffix matcher — instead, notify each subscriber with
|
|
727
|
+
// its own state.
|
|
728
|
+
for (const [subscriptionId, listeners] of actionListeners) {
|
|
729
|
+
const state = getActionState(subscriptionId);
|
|
730
|
+
listeners.forEach((listener) => listener(state));
|
|
715
731
|
}
|
|
716
732
|
}
|
|
717
733
|
|
|
@@ -719,22 +735,10 @@ export function createEventController(
|
|
|
719
735
|
// Handle Operations
|
|
720
736
|
// ========================================================================
|
|
721
737
|
|
|
722
|
-
/**
|
|
723
|
-
* Filter segment IDs to only include routes and layouts.
|
|
724
|
-
* Excludes parallels (contain .@) and loaders (contain D followed by digit).
|
|
725
|
-
*/
|
|
726
|
-
function filterSegmentOrder(matched: string[]): string[] {
|
|
727
|
-
return matched.filter((id) => {
|
|
728
|
-
if (id.includes(".@")) return false;
|
|
729
|
-
if (/D\d+\./.test(id)) return false;
|
|
730
|
-
return true;
|
|
731
|
-
});
|
|
732
|
-
}
|
|
733
|
-
|
|
734
738
|
function setHandleData(
|
|
735
739
|
data: HandleData,
|
|
736
740
|
matched?: string[],
|
|
737
|
-
isPartial?: boolean
|
|
741
|
+
isPartial?: boolean,
|
|
738
742
|
): void {
|
|
739
743
|
const newSegmentOrder = filterSegmentOrder(matched ?? []);
|
|
740
744
|
|
|
@@ -783,7 +787,7 @@ export function createEventController(
|
|
|
783
787
|
|
|
784
788
|
function subscribeToAction(
|
|
785
789
|
actionId: string,
|
|
786
|
-
listener: ActionStateListener
|
|
790
|
+
listener: ActionStateListener,
|
|
787
791
|
): () => void {
|
|
788
792
|
let listeners = actionListeners.get(actionId);
|
|
789
793
|
if (!listeners) {
|
|
@@ -805,6 +809,19 @@ export function createEventController(
|
|
|
805
809
|
return () => handleListeners.delete(listener);
|
|
806
810
|
}
|
|
807
811
|
|
|
812
|
+
// ========================================================================
|
|
813
|
+
// Params Operations
|
|
814
|
+
// ========================================================================
|
|
815
|
+
|
|
816
|
+
function setParams(params: Record<string, string>): void {
|
|
817
|
+
routeParams = params;
|
|
818
|
+
notify();
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function getParams(): Record<string, string> {
|
|
822
|
+
return routeParams;
|
|
823
|
+
}
|
|
824
|
+
|
|
808
825
|
// ========================================================================
|
|
809
826
|
// Return Controller
|
|
810
827
|
// ========================================================================
|
|
@@ -821,12 +838,17 @@ export function createEventController(
|
|
|
821
838
|
// State
|
|
822
839
|
getState,
|
|
823
840
|
getActionState,
|
|
841
|
+
getLocation: () => location,
|
|
824
842
|
setLocation,
|
|
825
843
|
|
|
826
844
|
// Handles
|
|
827
845
|
setHandleData,
|
|
828
846
|
getHandleState,
|
|
829
847
|
|
|
848
|
+
// Params
|
|
849
|
+
setParams,
|
|
850
|
+
getParams,
|
|
851
|
+
|
|
830
852
|
// Subscriptions
|
|
831
853
|
subscribe,
|
|
832
854
|
subscribeToAction,
|
|
@@ -835,6 +857,7 @@ export function createEventController(
|
|
|
835
857
|
// Direct access
|
|
836
858
|
getCurrentNavigation: () => currentNavigation,
|
|
837
859
|
getInflightActions: () => inflightActions,
|
|
860
|
+
hadAnyConcurrentActions: () => hadAnyConcurrentActions,
|
|
838
861
|
};
|
|
839
862
|
}
|
|
840
863
|
|
|
@@ -848,7 +871,7 @@ let controllerInstance: EventController | null = null;
|
|
|
848
871
|
* Initialize the global event controller
|
|
849
872
|
*/
|
|
850
873
|
export function initEventController(
|
|
851
|
-
config?: EventControllerConfig
|
|
874
|
+
config?: EventControllerConfig,
|
|
852
875
|
): EventController {
|
|
853
876
|
if (!controllerInstance) {
|
|
854
877
|
controllerInstance = createEventController(config);
|
|
@@ -862,7 +885,7 @@ export function initEventController(
|
|
|
862
885
|
export function getEventController(): EventController {
|
|
863
886
|
if (!controllerInstance) {
|
|
864
887
|
throw new Error(
|
|
865
|
-
"Event controller not initialized. Call initEventController first."
|
|
888
|
+
"Event controller not initialized. Call initEventController first.",
|
|
866
889
|
);
|
|
867
890
|
}
|
|
868
891
|
return controllerInstance;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isLocationStateEntry,
|
|
3
|
+
resolveLocationStateEntries,
|
|
4
|
+
} from "./react/location-state-shared.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check if state is from typed LocationStateEntry[] (has __rsc_ls_ keys)
|
|
8
|
+
*/
|
|
9
|
+
function isTypedLocationState(
|
|
10
|
+
state: unknown,
|
|
11
|
+
): state is Record<string, unknown> {
|
|
12
|
+
if (state === null || typeof state !== "object") return false;
|
|
13
|
+
return Object.keys(state).some((key) => key.startsWith("__rsc_ls_"));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolve navigation state - handles both LocationStateEntry[] and plain formats
|
|
18
|
+
*/
|
|
19
|
+
export function resolveNavigationState(state: unknown): unknown {
|
|
20
|
+
if (
|
|
21
|
+
Array.isArray(state) &&
|
|
22
|
+
state.length > 0 &&
|
|
23
|
+
isLocationStateEntry(state[0])
|
|
24
|
+
) {
|
|
25
|
+
return resolveLocationStateEntries(state);
|
|
26
|
+
}
|
|
27
|
+
return state;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build history state object from user state
|
|
32
|
+
* - Typed state: spread directly into history.state
|
|
33
|
+
* - Plain state: store in history.state.state
|
|
34
|
+
*/
|
|
35
|
+
export function buildHistoryState(
|
|
36
|
+
userState: unknown,
|
|
37
|
+
routerState?: { intercept?: boolean; sourceUrl?: string },
|
|
38
|
+
serverState?: Record<string, unknown>,
|
|
39
|
+
): Record<string, unknown> | null {
|
|
40
|
+
const result: Record<string, unknown> = {};
|
|
41
|
+
|
|
42
|
+
if (routerState?.intercept) {
|
|
43
|
+
result.intercept = true;
|
|
44
|
+
if (routerState.sourceUrl) {
|
|
45
|
+
result.sourceUrl = routerState.sourceUrl;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (userState !== undefined) {
|
|
50
|
+
if (isTypedLocationState(userState)) {
|
|
51
|
+
Object.assign(result, userState);
|
|
52
|
+
} else {
|
|
53
|
+
result.state = userState;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (serverState) {
|
|
58
|
+
Object.assign(result, serverState);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Merge server-set location state into the current history entry.
|
|
66
|
+
* Replaces the current history state and dispatches notification event
|
|
67
|
+
* so useLocationState hooks re-read from history.state.
|
|
68
|
+
*/
|
|
69
|
+
export function mergeLocationState(
|
|
70
|
+
locationState: Record<string, unknown>,
|
|
71
|
+
): void {
|
|
72
|
+
const merged = {
|
|
73
|
+
...window.history.state,
|
|
74
|
+
...locationState,
|
|
75
|
+
};
|
|
76
|
+
window.history.replaceState(merged, "", window.location.href);
|
|
77
|
+
if (Object.keys(locationState).some((k) => k.startsWith("__rsc_ls_"))) {
|
|
78
|
+
window.dispatchEvent(new Event("__rsc_locationstate"));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
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:" — both the
|
|
7
|
+
* parallel container (@modal) and its content children receive this namespace
|
|
8
|
+
* from intercept-resolution.ts. Regular parallel segments like @sidebar do not.
|
|
9
|
+
*/
|
|
10
|
+
export function isInterceptSegment(s: ResolvedSegment): boolean {
|
|
11
|
+
return s.namespace?.startsWith("intercept:") === true;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Split an array of segments into main and intercept groups.
|
|
16
|
+
* Intercept segments are separated for explicit injection into the render tree
|
|
17
|
+
* via the interceptSegments render option.
|
|
18
|
+
*/
|
|
19
|
+
export function splitInterceptSegments(segments: ResolvedSegment[]): {
|
|
20
|
+
main: ResolvedSegment[];
|
|
21
|
+
intercept: ResolvedSegment[];
|
|
22
|
+
} {
|
|
23
|
+
const main: ResolvedSegment[] = [];
|
|
24
|
+
const intercept: ResolvedSegment[] = [];
|
|
25
|
+
for (const s of segments) {
|
|
26
|
+
if (isInterceptSegment(s)) {
|
|
27
|
+
intercept.push(s);
|
|
28
|
+
} else {
|
|
29
|
+
main.push(s);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { main, intercept };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if any slot is currently active (has content to render).
|
|
37
|
+
* Active slots indicate an intercept response where a parallel segment
|
|
38
|
+
* (e.g., @modal) has matched and should be rendered.
|
|
39
|
+
*/
|
|
40
|
+
export function hasActiveIntercept(slots?: Record<string, SlotState>): boolean {
|
|
41
|
+
if (!slots) return false;
|
|
42
|
+
return Object.values(slots).some((slot) => slot.active);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if cached segments contain any intercept segments.
|
|
47
|
+
* Intercept caches shouldn't be used for cached SWR rendering since
|
|
48
|
+
* whether interception happens depends on the current page context.
|
|
49
|
+
*/
|
|
50
|
+
export function isInterceptOnlyCache(segments: ResolvedSegment[]): boolean {
|
|
51
|
+
return segments.some(isInterceptSegment);
|
|
52
|
+
}
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import type { LinkInterceptorOptions, NavigateOptions } from "./types.js";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Check if an anchor points to the same page with only a hash change.
|
|
5
|
+
* Used by both Link component and link-interceptor to let the browser
|
|
6
|
+
* handle anchor scrolling natively.
|
|
7
|
+
*/
|
|
8
|
+
export function isHashOnlyNavigation(anchor: HTMLAnchorElement): boolean {
|
|
9
|
+
return (
|
|
10
|
+
anchor.pathname === window.location.pathname &&
|
|
11
|
+
anchor.search === window.location.search &&
|
|
12
|
+
!!anchor.hash
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
3
16
|
/**
|
|
4
17
|
* Default link interception predicate
|
|
5
18
|
*
|
|
@@ -44,6 +57,12 @@ export function defaultShouldIntercept(link: HTMLAnchorElement): boolean {
|
|
|
44
57
|
return false;
|
|
45
58
|
}
|
|
46
59
|
|
|
60
|
+
// Don't intercept hash-only navigation (same path, only fragment changes).
|
|
61
|
+
// Let the browser handle anchor scrolling natively.
|
|
62
|
+
if (isHashOnlyNavigation(link)) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
47
66
|
return true;
|
|
48
67
|
}
|
|
49
68
|
|
|
@@ -70,7 +89,7 @@ export function defaultShouldIntercept(link: HTMLAnchorElement): boolean {
|
|
|
70
89
|
*/
|
|
71
90
|
export function setupLinkInterception(
|
|
72
91
|
onNavigate: (url: string, options?: NavigateOptions) => void,
|
|
73
|
-
options?: LinkInterceptorOptions
|
|
92
|
+
options?: LinkInterceptorOptions,
|
|
74
93
|
): () => void {
|
|
75
94
|
const shouldIntercept = options?.shouldIntercept ?? defaultShouldIntercept;
|
|
76
95
|
|
|
@@ -98,6 +117,7 @@ export function setupLinkInterception(
|
|
|
98
117
|
// Read navigation options from data attributes (set by Link component)
|
|
99
118
|
const scrollAttr = link.getAttribute("data-scroll");
|
|
100
119
|
const replaceAttr = link.getAttribute("data-replace");
|
|
120
|
+
const revalidateAttr = link.getAttribute("data-revalidate");
|
|
101
121
|
|
|
102
122
|
const navigateOptions: NavigateOptions = {};
|
|
103
123
|
if (scrollAttr === "false") {
|
|
@@ -106,16 +126,16 @@ export function setupLinkInterception(
|
|
|
106
126
|
if (replaceAttr === "true") {
|
|
107
127
|
navigateOptions.replace = true;
|
|
108
128
|
}
|
|
129
|
+
if (revalidateAttr === "false") {
|
|
130
|
+
navigateOptions.revalidate = false;
|
|
131
|
+
}
|
|
109
132
|
|
|
110
133
|
onNavigate(href, navigateOptions);
|
|
111
134
|
};
|
|
112
135
|
|
|
113
136
|
document.addEventListener("click", handleClick);
|
|
114
137
|
|
|
115
|
-
console.log("[Browser] Link interception enabled");
|
|
116
|
-
|
|
117
138
|
return () => {
|
|
118
139
|
document.removeEventListener("click", handleClick);
|
|
119
|
-
console.log("[Browser] Link interception disabled");
|
|
120
140
|
};
|
|
121
141
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
|
|
2
|
+
|
|
3
|
+
interface BrowserLogContext {
|
|
4
|
+
requestId: string;
|
|
5
|
+
txId: string;
|
|
6
|
+
operation: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let txCounter = 0;
|
|
10
|
+
let requestCounter = 0;
|
|
11
|
+
|
|
12
|
+
export function isBrowserDebugEnabled(): boolean {
|
|
13
|
+
return INTERNAL_RANGO_DEBUG;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function nextId(prefix: string, counter: number): string {
|
|
17
|
+
return `${prefix}${counter.toString(36)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function startBrowserTransaction(operation: string): BrowserLogContext {
|
|
21
|
+
txCounter += 1;
|
|
22
|
+
requestCounter += 1;
|
|
23
|
+
return {
|
|
24
|
+
operation,
|
|
25
|
+
txId: nextId("ctx-", txCounter),
|
|
26
|
+
requestId: nextId("creq-", requestCounter),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function browserDebugLog(
|
|
31
|
+
ctx: BrowserLogContext,
|
|
32
|
+
message: string,
|
|
33
|
+
details?: Record<string, unknown>,
|
|
34
|
+
): void {
|
|
35
|
+
if (!INTERNAL_RANGO_DEBUG) return;
|
|
36
|
+
|
|
37
|
+
const prefix = `[Browser][req:${ctx.requestId}][tx:${ctx.operation}-${ctx.txId}]`;
|
|
38
|
+
if (details) {
|
|
39
|
+
console.log(`${prefix} ${message}`, details);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log(`${prefix} ${message}`);
|
|
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
|
+
}
|
|
@@ -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.
|
|
@@ -13,13 +14,13 @@ import type { ResolvedSegment } from "./types.js";
|
|
|
13
14
|
*/
|
|
14
15
|
export function mergeSegmentLoaders(
|
|
15
16
|
fromServer: ResolvedSegment,
|
|
16
|
-
fromCache: ResolvedSegment
|
|
17
|
+
fromCache: ResolvedSegment,
|
|
17
18
|
): ResolvedSegment {
|
|
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 {
|
|
@@ -54,7 +55,7 @@ export function mergeSegmentLoaders(
|
|
|
54
55
|
*/
|
|
55
56
|
export function needsLoaderMerge(
|
|
56
57
|
fromServer: ResolvedSegment,
|
|
57
|
-
fromCache: ResolvedSegment | undefined
|
|
58
|
+
fromCache: ResolvedSegment | undefined,
|
|
58
59
|
): fromCache is ResolvedSegment {
|
|
59
60
|
return !!(
|
|
60
61
|
fromCache &&
|
|
@@ -86,10 +87,15 @@ export function insertMissingDiffSegments(
|
|
|
86
87
|
allSegments: ResolvedSegment[],
|
|
87
88
|
diff: string[] | undefined,
|
|
88
89
|
matchedIdSet: Set<string>,
|
|
89
|
-
newSegmentMap: Map<string, ResolvedSegment
|
|
90
|
+
newSegmentMap: Map<string, ResolvedSegment>,
|
|
90
91
|
): void {
|
|
91
92
|
if (!diff || diff.length === 0) return;
|
|
92
93
|
|
|
94
|
+
// Track how many siblings have been inserted per parent so each new
|
|
95
|
+
// sibling goes after the last one rather than always at parentIndex + 1
|
|
96
|
+
// (which would reverse the server order).
|
|
97
|
+
const insertedPerParent = new Map<string, number>();
|
|
98
|
+
|
|
93
99
|
diff.forEach((diffId: string) => {
|
|
94
100
|
if (!matchedIdSet.has(diffId)) {
|
|
95
101
|
const fromServer = newSegmentMap.get(diffId);
|
|
@@ -100,25 +106,27 @@ export function insertMissingDiffSegments(
|
|
|
100
106
|
if (loaderMatch) {
|
|
101
107
|
const parentLayoutId = loaderMatch[1];
|
|
102
108
|
const parentIndex = allSegments.findIndex(
|
|
103
|
-
(s) => s.id === parentLayoutId
|
|
109
|
+
(s) => s.id === parentLayoutId,
|
|
104
110
|
);
|
|
105
111
|
if (parentIndex !== -1) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
112
|
+
const alreadyInserted = insertedPerParent.get(parentLayoutId) ?? 0;
|
|
113
|
+
const insertAt = parentIndex + 1 + alreadyInserted;
|
|
114
|
+
allSegments.splice(insertAt, 0, fromServer);
|
|
115
|
+
insertedPerParent.set(parentLayoutId, alreadyInserted + 1);
|
|
116
|
+
debugLog(
|
|
117
|
+
`[Browser] Inserted diff segment ${diffId} after ${parentLayoutId}`,
|
|
110
118
|
);
|
|
111
119
|
} else {
|
|
112
120
|
// Fallback: append to end if parent not found
|
|
113
121
|
allSegments.push(fromServer);
|
|
114
122
|
console.warn(
|
|
115
|
-
`[Browser] Appended diff segment ${diffId} (parent ${parentLayoutId} not found)
|
|
123
|
+
`[Browser] Appended diff segment ${diffId} (parent ${parentLayoutId} not found)`,
|
|
116
124
|
);
|
|
117
125
|
}
|
|
118
126
|
} else {
|
|
119
127
|
// Non-loader diff segment not in matched - append to end
|
|
120
128
|
allSegments.push(fromServer);
|
|
121
|
-
|
|
129
|
+
debugLog(`[Browser] Appended diff segment ${diffId}`);
|
|
122
130
|
}
|
|
123
131
|
}
|
|
124
132
|
}
|