Spaces:
Running
Running
File size: 25,665 Bytes
c592d77 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 | /**
* Optimistic Routing (Known Routes)
*
* This module enables the client to predict route structure for URLs that
* haven't been prefetched yet, based on previously learned route patterns.
* When successful, this allows skipping the route tree prefetch request
* entirely.
*
* The core idea is that many URLs map to the same route structure. For example,
* /blog/post-1 and /blog/post-2 both resolve to /blog/[slug]. Once we've
* prefetched one, we can predict the structure of the other.
*
* However, we can't always make this prediction. Static siblings (like
* /blog/featured alongside /blog/[slug]) have different route structures.
* When we learn a dynamic route, we also learn its static siblings so we
* know when NOT to apply the prediction.
*
* Main entry points:
*
* 1. discoverKnownRoute: Called after receiving a route tree from the server.
* Traverses the route tree, compares URL parts to segments, and populates
* the known route tree if they match. Routes are always inserted into the
* cache.
*
* 2. matchKnownRoute: Called when looking up a route with no cache entry.
* Matches the candidate URL against learned patterns. Returns a synthetic
* cache entry if successful, or null to fall back to server resolution.
*
* Rewrite detection happens during traversal: if a URL path part doesn't match
* the corresponding route segment, we stop populating the known route tree
* (since the mapping is incorrect) but still insert the route into the cache.
*
* The known route tree is append-only with no eviction. Route patterns are
* derived from the filesystem, so they don't become stale within a session.
* Cache invalidation on deploy clears everything anyway.
*
* Current limitations (deopt to server resolution):
* - Rewrites: Detected during traversal (tree not populated, but route cached)
* - Intercepted routes: The route tree varies by referrer (Next-Url header),
* so we can't predict the correct structure from the URL alone. Patterns are
* still stored during discovery (so the trie stays populated for non-
* intercepted siblings), but matching bails out when the pattern is marked
* as interceptable.
*/ "use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
0 && (module.exports = {
discoverKnownRoute: null,
matchKnownRoute: null,
resetKnownRoutes: null
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: all[name]
});
}
_export(exports, {
discoverKnownRoute: function() {
return discoverKnownRoute;
},
matchKnownRoute: function() {
return matchKnownRoute;
},
resetKnownRoutes: function() {
return resetKnownRoutes;
}
});
const _cache = require("./cache");
const _routeparams = require("../../route-params");
const _varypath = require("./vary-path");
function createEmptyPart() {
return {
staticChildren: null,
dynamicChild: null,
dynamicChildParamName: null,
dynamicChildParamType: null,
pattern: null
};
}
// The root of the known route tree.
let knownRouteTreeRoot = createEmptyPart();
function discoverKnownRoute(now, pathname, nextUrl, pendingEntry, routeTree, metadataVaryPath, couldBeIntercepted, canonicalUrl, supportsPerSegmentPrefetching, hasDynamicRewrite) {
const tree = routeTree;
const pathnameParts = pathname.split('/').filter((p)=>p !== '');
const firstPart = pathnameParts.length > 0 ? pathnameParts[0] : null;
const remainingParts = pathnameParts.length > 0 ? pathnameParts.slice(1) : [];
if (pendingEntry !== null) {
// Fulfill the pending entry first
const fulfilledEntry = (0, _cache.fulfillRouteCacheEntry)(now, pendingEntry, tree, metadataVaryPath, couldBeIntercepted, canonicalUrl, supportsPerSegmentPrefetching);
if (hasDynamicRewrite) {
fulfilledEntry.hasDynamicRewrite = true;
}
// Populate the known route tree (handles rewrite detection internally).
// The entry is already in the cache; this just stores it as a pattern
// if the URL matches the route structure.
discoverKnownRoutePart(knownRouteTreeRoot, tree, firstPart, remainingParts, fulfilledEntry, now, pathname, nextUrl, tree, metadataVaryPath, couldBeIntercepted, canonicalUrl, supportsPerSegmentPrefetching, hasDynamicRewrite);
return fulfilledEntry;
}
// No pending entry - discoverKnownRoutePart will create one and insert it
// into the cache, or return an existing pattern if one exists.
return discoverKnownRoutePart(knownRouteTreeRoot, tree, firstPart, remainingParts, null, now, pathname, nextUrl, tree, metadataVaryPath, couldBeIntercepted, canonicalUrl, supportsPerSegmentPrefetching, hasDynamicRewrite);
}
/**
* Gets or creates the dynamic child node for a KnownRoutePart.
* A node can have at most one dynamic child (you can't have both [slug] and
* [id] at the same route level), so we either return existing or create new.
*/ function discoverDynamicChild(part, paramName, paramType) {
if (part.dynamicChild !== null) {
return part.dynamicChild;
}
const newChild = createEmptyPart();
// Type assertion needed because we're converting from "without" to "with"
// dynamic child variant.
const mutablePart = part;
mutablePart.dynamicChild = newChild;
mutablePart.dynamicChildParamName = paramName;
mutablePart.dynamicChildParamType = paramType;
return newChild;
}
/**
* Recursive workhorse for discoverKnownRoute.
*
* Walks the route tree and URL parts in parallel, building out the known
* route tree as it goes. At each step:
* 1. Determines if the current segment appears in the URL (dynamic/static)
* 2. Validates URL matches route structure (detects rewrites)
* 3. Creates/updates the corresponding KnownRoutePart node
* 4. Records static siblings for future matching
* 5. Recurses into child slots (parallel routes)
*
* If a URL/route mismatch is detected (rewrite), we stop building the known
* route tree but still cache the route entry for direct lookup.
*/ function discoverKnownRoutePart(parentKnownRoutePart, routeTree, urlPart, remainingParts, existingEntry, // These are passed through unchanged for entry creation at the leaf
now, pathname, nextUrl, fullTree, metadataVaryPath, couldBeIntercepted, canonicalUrl, supportsPerSegmentPrefetching, hasDynamicRewrite) {
const segment = routeTree.segment;
let segmentAppearsInURL;
let paramName = null;
let paramType = null;
let staticSiblings = null;
if (typeof segment === 'string') {
segmentAppearsInURL = (0, _routeparams.doesStaticSegmentAppearInURL)(segment);
} else {
// Dynamic segment tuple: [paramName, paramCacheKey, paramType, staticSiblings]
paramName = segment[0];
paramType = segment[2];
staticSiblings = segment[3];
segmentAppearsInURL = true;
}
let knownRoutePart = parentKnownRoutePart;
let nextUrlPart = urlPart;
let nextRemainingParts = remainingParts;
if (segmentAppearsInURL) {
// Check for mismatch: if this is a static segment, the URL part must match
if (paramName === null && urlPart !== segment) {
// URL doesn't match route structure (likely a rewrite).
// Don't populate the known route tree, just write the route into the
// cache and return immediately.
if (existingEntry !== null) {
return existingEntry;
}
return (0, _cache.writeRouteIntoCache)(now, pathname, nextUrl, fullTree, metadataVaryPath, couldBeIntercepted, canonicalUrl, supportsPerSegmentPrefetching);
}
// URL matches route structure. Build the known route tree.
if (paramName !== null && paramType !== null) {
// Dynamic segment
knownRoutePart = discoverDynamicChild(parentKnownRoutePart, paramName, paramType);
// Record static siblings as placeholder parts.
// IMPORTANT: We use the null vs Map distinction to track whether
// siblings are known at this level:
// - staticChildren: null = siblings unknown (can't safely match dynamic)
// - staticChildren: Map = siblings known (even if empty)
// This matters in dev mode where webpack may not know all siblings yet.
if (staticSiblings !== null) {
// Siblings are known - ensure we have a Map (even if empty)
if (parentKnownRoutePart.staticChildren === null) {
parentKnownRoutePart.staticChildren = new Map();
}
for (const sibling of staticSiblings){
if (!parentKnownRoutePart.staticChildren.has(sibling)) {
parentKnownRoutePart.staticChildren.set(sibling, createEmptyPart());
}
}
}
} else {
// Static segment
if (parentKnownRoutePart.staticChildren === null) {
parentKnownRoutePart.staticChildren = new Map();
}
let existingChild = parentKnownRoutePart.staticChildren.get(urlPart);
if (existingChild === undefined) {
existingChild = createEmptyPart();
parentKnownRoutePart.staticChildren.set(urlPart, existingChild);
}
knownRoutePart = existingChild;
}
// Advance to next URL part
nextUrlPart = remainingParts.length > 0 ? remainingParts[0] : null;
nextRemainingParts = remainingParts.length > 0 ? remainingParts.slice(1) : [];
}
// else: Transparent segment (route group, __PAGE__, etc.)
// Stay at the same known route part, don't advance URL parts
// Recurse into child routes. A route tree can have multiple parallel routes
// (e.g., @modal alongside children). Each parallel route is a separate
// branch, but they all share the same URL - we just need to traverse all
// branches to build out the known route tree.
const slots = routeTree.slots;
let resultFromChildren = null;
if (slots !== null) {
for(const parallelRouteKey in slots){
const childRouteTree = slots[parallelRouteKey];
// Skip branches with refreshState set - these were reused from a
// different route (e.g., a "default" parallel slot) and don't represent
// the actual route structure for this URL.
if (childRouteTree.refreshState !== null) {
continue;
}
const result = discoverKnownRoutePart(knownRoutePart, childRouteTree, nextUrlPart, nextRemainingParts, existingEntry, now, pathname, nextUrl, fullTree, metadataVaryPath, couldBeIntercepted, canonicalUrl, supportsPerSegmentPrefetching, hasDynamicRewrite);
// All parallel route branches share the same URL, so they should all
// reach compatible leaf nodes. We capture any result.
resultFromChildren = result;
}
if (resultFromChildren !== null) {
return resultFromChildren;
}
// Defensive fallback: no children returned a result. This shouldn't happen
// for valid route trees, but handle it gracefully.
if (existingEntry !== null) {
return existingEntry;
}
return (0, _cache.writeRouteIntoCache)(now, pathname, nextUrl, fullTree, metadataVaryPath, couldBeIntercepted, canonicalUrl, supportsPerSegmentPrefetching);
}
// Reached a page node. Create/get the route cache entry and store as a
// pattern. First, check if there's already a pattern for this route.
if (knownRoutePart.pattern !== null) {
// If this route has a dynamic rewrite, mark the existing pattern.
if (hasDynamicRewrite) {
knownRoutePart.pattern.hasDynamicRewrite = true;
}
return knownRoutePart.pattern;
}
// Get or create the entry
let entry;
if (existingEntry !== null) {
// Already have a fulfilled entry, use it directly. It's already in the
// route cache map.
entry = existingEntry;
} else {
// Create the entry and insert it into the route cache map.
entry = (0, _cache.writeRouteIntoCache)(now, pathname, nextUrl, fullTree, metadataVaryPath, couldBeIntercepted, canonicalUrl, supportsPerSegmentPrefetching);
}
if (hasDynamicRewrite) {
entry.hasDynamicRewrite = true;
}
// Store as pattern
knownRoutePart.pattern = entry;
return entry;
}
function matchKnownRoute(pathname, search) {
const pathnameParts = pathname.split('/').filter((p)=>p !== '');
const resolvedParams = new Map();
const match = matchKnownRoutePart(knownRouteTreeRoot, pathnameParts, 0, resolvedParams);
if (match === null) {
return null;
}
const matchedPart = match.part;
const pattern = match.pattern;
// If the pattern could be intercepted, we can't safely use it for prediction.
// Interception routes resolve to different route trees depending on the
// referrer (the Next-Url header), which means the same URL can map to
// different page components depending on where the navigation originated.
// Since the known route tree only stores a single pattern per URL shape, we
// can't distinguish between the intercepted and non-intercepted cases, so we
// bail out to server resolution.
//
// TODO: We could store interception behavior in the known route tree itself
// (e.g., which segments use interception markers and what they resolve to).
// With enough information embedded in the trie, we could match interception
// routes entirely on the client without a server round-trip.
if (pattern.couldBeIntercepted) {
return null;
}
// "Reify" the pattern: clone the template tree with concrete param values.
// This substitutes resolved params (e.g., slug: "hello") into dynamic
// segments and recomputes vary paths for correct segment cache keying.
const acc = {
metadataVaryPath: null
};
const reifiedTree = reifyRouteTree(pattern.tree, resolvedParams, search, null, acc);
// The metadata tree is a flat page node without the intermediate layout
// structure. Clone it with the updated metadata vary path collected during
// the main tree traversal.
const metadataVaryPath = acc.metadataVaryPath;
if (metadataVaryPath === null) {
// This shouldn't be reachable for a valid route tree.
return null;
}
const reifiedMetadata = (0, _cache.createMetadataRouteTree)(metadataVaryPath);
// Create a synthetic (predicted) entry and store it as the new pattern.
//
// Why replace the pattern? We intentionally update the pattern with this
// synthetic entry so that if our prediction was wrong (server returns a
// different pathname due to dynamic rewrite), the entry gets marked with
// hasDynamicRewrite. Future predictions for this route will see the flag
// and bail out to server resolution instead of making the same mistake.
const syntheticEntry = {
canonicalUrl: pathname + search,
status: _cache.EntryStatus.Fulfilled,
blockedTasks: null,
tree: reifiedTree,
metadata: reifiedMetadata,
couldBeIntercepted: pattern.couldBeIntercepted,
supportsPerSegmentPrefetching: pattern.supportsPerSegmentPrefetching,
hasDynamicRewrite: false,
renderedSearch: search,
ref: null,
size: pattern.size,
staleAt: pattern.staleAt,
version: pattern.version
};
matchedPart.pattern = syntheticEntry;
return syntheticEntry;
}
/**
* Recursively matches a URL against the known route tree.
*
* Matching priority (most specific first):
* 1. Static children - exact path segment match
* 2. Dynamic child - [param], [...param], [[...param]]
* 3. Direct pattern - when no more URL parts remain
*
* Collects resolved param values in resolvedParams as it traverses.
* Returns null if no match found (caller should fall back to server).
*/ function matchKnownRoutePart(part, pathnameParts, partIndex, resolvedParams) {
const urlPart = partIndex < pathnameParts.length ? pathnameParts[partIndex] : null;
// If staticChildren is null, we don't know what static routes exist at this
// level. This happens in webpack dev mode where routes are compiled
// on-demand. We can't safely match a dynamicChild because the URL part might
// be a static sibling we haven't discovered yet. Example: We know
// /blog/[slug] exists, but haven't compiled /blog/featured. A request for
// /blog/featured would incorrectly match /blog/[slug].
if (part.staticChildren === null) {
// The only safe match is a direct pattern when no URL parts remain.
if (urlPart === null) {
const pattern = part.pattern;
if (pattern !== null && !pattern.hasDynamicRewrite) {
return {
part,
pattern
};
}
}
return null;
}
// Static children take priority over dynamic. This ensures /blog/featured
// matches its own route rather than /blog/[slug].
if (urlPart !== null) {
const staticChild = part.staticChildren.get(urlPart);
if (staticChild !== undefined) {
// Check if this is an "unknown" placeholder part. These are created when
// we learn about static siblings (from the route tree's staticSiblings
// field) but haven't prefetched them yet. We know the path exists but
// don't know its structure, so we can't predict it.
if (staticChild.pattern === null && staticChild.dynamicChild === null && staticChild.staticChildren === null) {
// Bail out - server must resolve this route.
return null;
}
const match = matchKnownRoutePart(staticChild, pathnameParts, partIndex + 1, resolvedParams);
if (match !== null) {
return match;
}
// Static child is a real node (not a placeholder) but its subtree
// didn't match the remaining URL parts. This means the route exists
// in the static subtree but hasn't been fully discovered yet. Do not
// fall through to try the dynamic child — the static match is
// authoritative. Bail out to server resolution.
return null;
}
}
// Try dynamic child
if (part.dynamicChild !== null) {
const dynamicPart = part.dynamicChild;
const paramName = part.dynamicChildParamName;
const paramType = part.dynamicChildParamType;
const dynamicPattern = dynamicPart.pattern;
switch(paramType){
case 'c':
// Required catch-all [...param]: consumes 1+ URL parts
if (dynamicPattern !== null && !dynamicPattern.hasDynamicRewrite && urlPart !== null) {
resolvedParams.set(paramName, pathnameParts.slice(partIndex));
return {
part: dynamicPart,
pattern: dynamicPattern
};
}
break;
case 'oc':
// Optional catch-all [[...param]]: consumes 0+ URL parts
if (dynamicPattern !== null && !dynamicPattern.hasDynamicRewrite) {
if (urlPart !== null) {
resolvedParams.set(paramName, pathnameParts.slice(partIndex));
return {
part: dynamicPart,
pattern: dynamicPattern
};
}
// urlPart is null - can match with zero parts, but a direct pattern
// (e.g., page.tsx alongside [[...param]]) takes precedence.
if (part.pattern === null || part.pattern.hasDynamicRewrite) {
resolvedParams.set(paramName, []);
return {
part: dynamicPart,
pattern: dynamicPattern
};
}
}
break;
case 'd':
// Regular dynamic [param]: consumes exactly 1 URL part.
// Unlike catch-all which terminates here, regular dynamic must
// continue recursing to find the leaf pattern.
if (urlPart !== null) {
resolvedParams.set(paramName, urlPart);
return matchKnownRoutePart(dynamicPart, pathnameParts, partIndex + 1, resolvedParams);
}
break;
// Intercepted routes use relative path markers like (.), (..), (...)
// Their behavior depends on navigation context (soft vs hard nav),
// so we can't predict them client-side. Defer to server.
case 'ci(..)(..)':
case 'ci(.)':
case 'ci(..)':
case 'ci(...)':
case 'di(..)(..)':
case 'di(.)':
case 'di(..)':
case 'di(...)':
return null;
default:
paramType;
}
}
// No children matched. If we've consumed all URL parts, check for a direct
// pattern at this node (the route terminates here).
if (urlPart === null) {
const pattern = part.pattern;
if (pattern !== null && !pattern.hasDynamicRewrite) {
return {
part,
pattern
};
}
}
return null;
}
/**
* "Reify" means to make concrete - we take an abstract pattern (the template
* route tree) and produce a concrete instance with actual param values.
*
* This function clones a RouteTree, substituting dynamic segment values from
* resolvedParams and computing new vary paths. The vary path encodes param
* values so segment cache entries can be correctly keyed.
*
* Example: Pattern for /blog/[slug] with resolvedParams { slug: "hello" }
* produces a tree where segment [slug] has cacheKey "hello".
*/ function reifyRouteTree(pattern, resolvedParams, search, parentPartialVaryPath, acc) {
const originalSegment = pattern.segment;
let newSegment = originalSegment;
let partialVaryPath;
if (typeof originalSegment !== 'string') {
// Dynamic segment: compute new cache key and append to partial vary path
const paramName = originalSegment[0];
const paramType = originalSegment[2];
const staticSiblings = originalSegment[3];
const newValue = resolvedParams.get(paramName);
if (newValue !== undefined) {
const newCacheKey = Array.isArray(newValue) ? newValue.join('/') : newValue;
newSegment = [
paramName,
newCacheKey,
paramType,
staticSiblings
];
partialVaryPath = (0, _varypath.appendLayoutVaryPath)(parentPartialVaryPath, newCacheKey, paramName);
} else {
// Param not found in resolvedParams - keep original and inherit partial
// TODO: This should never happen. Bail out with null.
partialVaryPath = parentPartialVaryPath;
}
} else {
// Static segment: inherit partial vary path from parent
partialVaryPath = parentPartialVaryPath;
}
// Recurse into children with the (possibly updated) partial vary path
let newSlots = null;
if (pattern.slots !== null) {
newSlots = {};
for(const key in pattern.slots){
newSlots[key] = reifyRouteTree(pattern.slots[key], resolvedParams, search, partialVaryPath, acc);
}
}
if (pattern.isPage) {
// Page segment: finalize with search params
const newVaryPath = (0, _varypath.finalizePageVaryPath)(pattern.requestKey, search, partialVaryPath);
// Collect metadata vary path (first page wins, same as original algorithm)
if (acc.metadataVaryPath === null) {
acc.metadataVaryPath = (0, _varypath.finalizeMetadataVaryPath)(pattern.requestKey, search, partialVaryPath);
}
return {
requestKey: pattern.requestKey,
segment: newSegment,
refreshState: pattern.refreshState,
slots: newSlots,
prefetchHints: pattern.prefetchHints,
isPage: true,
varyPath: newVaryPath
};
} else {
// Layout segment: finalize without search params
const newVaryPath = (0, _varypath.finalizeLayoutVaryPath)(pattern.requestKey, partialVaryPath);
return {
requestKey: pattern.requestKey,
segment: newSegment,
refreshState: pattern.refreshState,
slots: newSlots,
prefetchHints: pattern.prefetchHints,
isPage: false,
varyPath: newVaryPath
};
}
}
function resetKnownRoutes() {
knownRouteTreeRoot = createEmptyPart();
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=optimistic-routes.js.map |