vorma 0.84.0 → 0.85.0-pre.0
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/internal/framework/_typescript/client/src/client.ts +646 -422
- package/internal/framework/_typescript/client/src/links.ts +55 -21
- package/internal/framework/_typescript/create/package.json +3 -3
- package/internal/framework/_typescript/create/pnpm-lock.yaml +14 -14
- package/internal/framework/_typescript/vite/vite.ts +99 -3
- package/npm_dist/internal/framework/_typescript/client/index.js +496 -314
- package/npm_dist/internal/framework/_typescript/client/index.js.map +3 -3
- package/npm_dist/internal/framework/_typescript/client/src/client.d.ts +22 -13
- package/npm_dist/internal/framework/_typescript/client/src/client.d.ts.map +1 -1
- package/npm_dist/internal/framework/_typescript/client/src/links.d.ts.map +1 -1
- package/npm_dist/internal/framework/_typescript/vite/vite.d.ts +1 -0
- package/npm_dist/internal/framework/_typescript/vite/vite.d.ts.map +1 -1
- package/npm_dist/internal/framework/_typescript/vite/vite.js +60 -2
- package/npm_dist/internal/framework/_typescript/vite/vite.js.map +2 -2
- package/package.json +13 -13
|
@@ -59,23 +59,22 @@ export type NavigateProps = {
|
|
|
59
59
|
scrollToTop?: boolean;
|
|
60
60
|
};
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
// Discriminated union for navigation outcomes -- provides exhaustiveness checking
|
|
63
|
+
export type NavigationOutcome =
|
|
64
|
+
| { type: "aborted" }
|
|
65
|
+
| { type: "redirect"; redirectData: RedirectData; props: NavigateProps }
|
|
66
|
+
| {
|
|
67
|
+
type: "success";
|
|
64
68
|
response: Response;
|
|
69
|
+
json: GetRouteDataOutput;
|
|
70
|
+
cssBundlePromises: Array<Promise<any>>;
|
|
71
|
+
waitFnPromise: Promise<ClientLoadersResult> | undefined;
|
|
65
72
|
props: NavigateProps;
|
|
66
|
-
}
|
|
67
|
-
| {
|
|
68
|
-
json: GetRouteDataOutput;
|
|
69
|
-
cssBundlePromises: Array<Promise<any>>;
|
|
70
|
-
waitFnPromise: Promise<ClientLoadersResult> | undefined;
|
|
71
|
-
}
|
|
72
|
-
| { redirectData: RedirectData }
|
|
73
|
-
))
|
|
74
|
-
| undefined;
|
|
73
|
+
};
|
|
75
74
|
|
|
76
75
|
export type NavigationControl = {
|
|
77
76
|
abortController: AbortController | undefined;
|
|
78
|
-
promise: Promise<
|
|
77
|
+
promise: Promise<NavigationOutcome>;
|
|
79
78
|
};
|
|
80
79
|
|
|
81
80
|
/////////////////////////////////////////////////////////////////////
|
|
@@ -109,14 +108,183 @@ interface NavigationEntry {
|
|
|
109
108
|
}
|
|
110
109
|
|
|
111
110
|
interface SubmissionEntry {
|
|
112
|
-
control:
|
|
111
|
+
control: {
|
|
112
|
+
abortController: AbortController | undefined;
|
|
113
|
+
promise: Promise<any>;
|
|
114
|
+
};
|
|
113
115
|
startTime: number;
|
|
114
116
|
skipGlobalLoadingIndicator?: boolean;
|
|
115
117
|
}
|
|
116
118
|
|
|
119
|
+
/////////////////////////////////////////////////////////////////////
|
|
120
|
+
// canSkipServerFetch HELPERS - extracted for readability
|
|
121
|
+
/////////////////////////////////////////////////////////////////////
|
|
122
|
+
|
|
123
|
+
type SkipCheckContext = {
|
|
124
|
+
routeManifest: Record<string, number>;
|
|
125
|
+
patternRegistry: any;
|
|
126
|
+
patternToWaitFnMap: Record<string, any>;
|
|
127
|
+
clientModuleMap: Record<
|
|
128
|
+
string,
|
|
129
|
+
{ importURL: string; exportKey: string; errorExportKey: string }
|
|
130
|
+
>;
|
|
131
|
+
currentMatchedPatterns: string[];
|
|
132
|
+
currentParams: Record<string, string>;
|
|
133
|
+
currentSplatValues: string[];
|
|
134
|
+
currentLoadersData: any[];
|
|
135
|
+
url: URL;
|
|
136
|
+
matchResult: any;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
type SkipCheckResult =
|
|
140
|
+
| { canSkip: false }
|
|
141
|
+
| {
|
|
142
|
+
canSkip: true;
|
|
143
|
+
matchResult: any;
|
|
144
|
+
importURLs: string[];
|
|
145
|
+
exportKeys: string[];
|
|
146
|
+
loadersData: any[];
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
function hasServerLoaderRemoval(ctx: SkipCheckContext): boolean {
|
|
150
|
+
for (const pattern of ctx.currentMatchedPatterns) {
|
|
151
|
+
const hasServerLoader = ctx.routeManifest[pattern] === 1;
|
|
152
|
+
if (hasServerLoader) {
|
|
153
|
+
const stillMatched = ctx.matchResult.matches.some(
|
|
154
|
+
(m: Match) => m.registeredPattern.originalPattern === pattern,
|
|
155
|
+
);
|
|
156
|
+
if (!stillMatched) {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function hasNewClientLoader(ctx: SkipCheckContext): boolean {
|
|
165
|
+
for (const m of ctx.matchResult.matches) {
|
|
166
|
+
const pattern = m.registeredPattern.originalPattern;
|
|
167
|
+
const hasClientLoader = !!ctx.patternToWaitFnMap[pattern];
|
|
168
|
+
const wasAlreadyMatched = ctx.currentMatchedPatterns.includes(pattern);
|
|
169
|
+
if (hasClientLoader && !wasAlreadyMatched) {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function findOutermostLoaderIndex(ctx: SkipCheckContext): number {
|
|
177
|
+
for (let i = ctx.matchResult.matches.length - 1; i >= 0; i--) {
|
|
178
|
+
const match: Match | undefined = ctx.matchResult.matches[i];
|
|
179
|
+
if (!match) continue;
|
|
180
|
+
|
|
181
|
+
const pattern = match.registeredPattern.originalPattern;
|
|
182
|
+
const hasServerLoader = ctx.routeManifest[pattern] === 1;
|
|
183
|
+
const hasClientLoader = !!ctx.patternToWaitFnMap[pattern];
|
|
184
|
+
|
|
185
|
+
if (hasServerLoader || hasClientLoader) {
|
|
186
|
+
return i;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return -1;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function didSearchParamsChange(ctx: SkipCheckContext): boolean {
|
|
193
|
+
const currentUrlObj = new URL(window.location.href);
|
|
194
|
+
const currentParamsSorted = Array.from(
|
|
195
|
+
currentUrlObj.searchParams.entries(),
|
|
196
|
+
).sort();
|
|
197
|
+
const targetParamsSorted = Array.from(
|
|
198
|
+
ctx.url.searchParams.entries(),
|
|
199
|
+
).sort();
|
|
200
|
+
return !jsonDeepEquals(currentParamsSorted, targetParamsSorted);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function didOutermostParamsChange(
|
|
204
|
+
ctx: SkipCheckContext,
|
|
205
|
+
outermostLoaderIndex: number,
|
|
206
|
+
): boolean {
|
|
207
|
+
const outermostMatch = ctx.matchResult.matches[outermostLoaderIndex];
|
|
208
|
+
if (!outermostMatch) return false;
|
|
209
|
+
|
|
210
|
+
for (const seg of outermostMatch.registeredPattern.normalizedSegments) {
|
|
211
|
+
if (seg.segType === "dynamic") {
|
|
212
|
+
const paramName = seg.normalizedVal.substring(1);
|
|
213
|
+
if (
|
|
214
|
+
ctx.matchResult.params[paramName] !==
|
|
215
|
+
ctx.currentParams[paramName]
|
|
216
|
+
) {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const hasSplat = outermostMatch.registeredPattern.lastSegType === "splat";
|
|
223
|
+
if (hasSplat) {
|
|
224
|
+
if (
|
|
225
|
+
!jsonDeepEquals(ctx.matchResult.splatValues, ctx.currentSplatValues)
|
|
226
|
+
) {
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function buildSkipResult(ctx: SkipCheckContext): SkipCheckResult {
|
|
235
|
+
const importURLs: string[] = [];
|
|
236
|
+
const exportKeys: string[] = [];
|
|
237
|
+
const loadersData: any[] = [];
|
|
238
|
+
|
|
239
|
+
for (let i = 0; i < ctx.matchResult.matches.length; i++) {
|
|
240
|
+
const match: Match | undefined = ctx.matchResult.matches[i];
|
|
241
|
+
if (!match) continue;
|
|
242
|
+
|
|
243
|
+
const pattern = match.registeredPattern.originalPattern;
|
|
244
|
+
const moduleInfo = ctx.clientModuleMap[pattern];
|
|
245
|
+
if (!moduleInfo) {
|
|
246
|
+
return { canSkip: false };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
importURLs.push(moduleInfo.importURL);
|
|
250
|
+
exportKeys.push(moduleInfo.exportKey);
|
|
251
|
+
|
|
252
|
+
const hasServerLoader = ctx.routeManifest[pattern] === 1;
|
|
253
|
+
if (!hasServerLoader) {
|
|
254
|
+
loadersData.push(undefined);
|
|
255
|
+
} else {
|
|
256
|
+
const currentPatternIndex =
|
|
257
|
+
ctx.currentMatchedPatterns.indexOf(pattern);
|
|
258
|
+
if (currentPatternIndex === -1) {
|
|
259
|
+
return { canSkip: false };
|
|
260
|
+
}
|
|
261
|
+
loadersData.push(ctx.currentLoadersData[currentPatternIndex]);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
canSkip: true,
|
|
267
|
+
matchResult: ctx.matchResult,
|
|
268
|
+
importURLs,
|
|
269
|
+
exportKeys,
|
|
270
|
+
loadersData,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/////////////////////////////////////////////////////////////////////
|
|
275
|
+
// NAVIGATION STATE MANAGER CLASS
|
|
276
|
+
/////////////////////////////////////////////////////////////////////
|
|
277
|
+
|
|
117
278
|
class NavigationStateManager {
|
|
118
|
-
|
|
279
|
+
// Single slot for active user/browser/redirect navigation
|
|
280
|
+
private _activeNavigation: NavigationEntry | null = null;
|
|
281
|
+
// Separate cache for prefetches (can have multiple to different URLs)
|
|
282
|
+
private _prefetchCache = new Map<string, NavigationEntry>();
|
|
283
|
+
// Single slot for pending revalidation (at most one, coalesced)
|
|
284
|
+
private _pendingRevalidation: NavigationEntry | null = null;
|
|
285
|
+
// Submissions tracked separately
|
|
119
286
|
private _submissions = new Map<string | symbol, SubmissionEntry>();
|
|
287
|
+
|
|
120
288
|
private lastDispatchedStatus: StatusEventDetail | null = null;
|
|
121
289
|
private dispatchStatusEventDebounced: () => void;
|
|
122
290
|
private readonly REVALIDATION_COALESCE_MS = 8;
|
|
@@ -131,30 +299,67 @@ class NavigationStateManager {
|
|
|
131
299
|
const control = this.beginNavigation(props);
|
|
132
300
|
|
|
133
301
|
try {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
302
|
+
const outcome = await control.promise;
|
|
303
|
+
|
|
304
|
+
// Handle based on outcome type (discriminated union)
|
|
305
|
+
switch (outcome.type) {
|
|
306
|
+
case "aborted":
|
|
307
|
+
return { didNavigate: false };
|
|
308
|
+
|
|
309
|
+
case "redirect": {
|
|
310
|
+
const targetUrl = new URL(props.href, window.location.href)
|
|
311
|
+
.href;
|
|
312
|
+
const entry = this.findNavigationEntry(targetUrl);
|
|
313
|
+
if (!entry) {
|
|
314
|
+
return { didNavigate: false };
|
|
315
|
+
}
|
|
138
316
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
317
|
+
// Skip redirect effectuation for pure prefetches
|
|
318
|
+
if (entry.type === "prefetch" && entry.intent === "none") {
|
|
319
|
+
this.deleteNavigation(targetUrl);
|
|
320
|
+
return { didNavigate: false };
|
|
321
|
+
}
|
|
145
322
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
323
|
+
this.deleteNavigation(targetUrl);
|
|
324
|
+
await effectuateRedirectDataResult(
|
|
325
|
+
outcome.redirectData,
|
|
326
|
+
props.redirectCount || 0,
|
|
327
|
+
props,
|
|
328
|
+
);
|
|
329
|
+
return { didNavigate: false };
|
|
330
|
+
}
|
|
150
331
|
|
|
151
|
-
|
|
152
|
-
|
|
332
|
+
case "success": {
|
|
333
|
+
const targetUrl = new URL(props.href, window.location.href)
|
|
334
|
+
.href;
|
|
335
|
+
const entry = this.findNavigationEntry(targetUrl);
|
|
336
|
+
if (!entry) {
|
|
337
|
+
return { didNavigate: false };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (
|
|
341
|
+
entry.intent === "navigate" ||
|
|
342
|
+
entry.intent === "revalidate"
|
|
343
|
+
) {
|
|
344
|
+
lastTriggeredNavOrRevalidateTimestampMS = Date.now();
|
|
345
|
+
}
|
|
153
346
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
347
|
+
await this.processSuccessfulNavigation(outcome, entry);
|
|
348
|
+
|
|
349
|
+
if (entry.intent === "none" && entry.type === "prefetch") {
|
|
350
|
+
return { didNavigate: false };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return { didNavigate: true };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
default: {
|
|
357
|
+
// Exhaustiveness check - TypeScript will error if a case is missing
|
|
358
|
+
const _exhaustive: never = outcome;
|
|
359
|
+
throw new Error(
|
|
360
|
+
`Unexpected navigation outcome type: ${(_exhaustive as any).type}`,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
158
363
|
}
|
|
159
364
|
} catch (error) {
|
|
160
365
|
const targetUrl = new URL(props.href, window.location.href).href;
|
|
@@ -164,65 +369,107 @@ class NavigationStateManager {
|
|
|
164
369
|
}
|
|
165
370
|
return { didNavigate: false };
|
|
166
371
|
}
|
|
167
|
-
return { didNavigate: true };
|
|
168
372
|
}
|
|
169
373
|
|
|
170
374
|
beginNavigation(props: NavigateProps): NavigationControl {
|
|
171
|
-
const
|
|
172
|
-
new URL(props.href, window.location.href).href,
|
|
173
|
-
);
|
|
375
|
+
const targetUrl = new URL(props.href, window.location.href).href;
|
|
174
376
|
|
|
175
377
|
switch (props.navigationType) {
|
|
176
378
|
case "userNavigation":
|
|
177
|
-
return this.beginUserNavigation(props,
|
|
379
|
+
return this.beginUserNavigation(props, targetUrl);
|
|
178
380
|
case "prefetch":
|
|
179
|
-
return this.beginPrefetch(props,
|
|
381
|
+
return this.beginPrefetch(props, targetUrl);
|
|
180
382
|
case "revalidation":
|
|
181
383
|
return this.beginRevalidation(props);
|
|
182
384
|
case "browserHistory":
|
|
183
385
|
case "redirect":
|
|
184
386
|
default:
|
|
185
|
-
return this.
|
|
387
|
+
return this.createActiveNavigation(props, "navigate");
|
|
186
388
|
}
|
|
187
389
|
}
|
|
188
390
|
|
|
189
391
|
private beginUserNavigation(
|
|
190
392
|
props: NavigateProps,
|
|
191
|
-
|
|
393
|
+
targetUrl: string,
|
|
192
394
|
): NavigationControl {
|
|
193
|
-
|
|
395
|
+
// Abort active navigation if it's to a different URL
|
|
396
|
+
if (
|
|
397
|
+
this._activeNavigation &&
|
|
398
|
+
this._activeNavigation.targetUrl !== targetUrl
|
|
399
|
+
) {
|
|
400
|
+
this._activeNavigation.control.abortController?.abort();
|
|
401
|
+
this._activeNavigation = null;
|
|
402
|
+
}
|
|
194
403
|
|
|
195
|
-
// Abort all
|
|
196
|
-
this.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
// Upgrade prefetch to user navigation
|
|
201
|
-
this.upgradeNavigation(targetUrl, {
|
|
202
|
-
type: "userNavigation",
|
|
203
|
-
intent: "navigate",
|
|
204
|
-
scrollToTop: props.scrollToTop,
|
|
205
|
-
replace: props.replace,
|
|
206
|
-
state: props.state,
|
|
207
|
-
});
|
|
208
|
-
return existing.control;
|
|
404
|
+
// Abort all prefetches except the one we might upgrade
|
|
405
|
+
for (const [url, prefetch] of this._prefetchCache.entries()) {
|
|
406
|
+
if (url !== targetUrl) {
|
|
407
|
+
prefetch.control.abortController?.abort();
|
|
408
|
+
this._prefetchCache.delete(url);
|
|
209
409
|
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Abort pending revalidation only if it's to a different URL
|
|
413
|
+
if (
|
|
414
|
+
this._pendingRevalidation &&
|
|
415
|
+
this._pendingRevalidation.targetUrl !== targetUrl
|
|
416
|
+
) {
|
|
417
|
+
this._pendingRevalidation.control.abortController?.abort();
|
|
418
|
+
this._pendingRevalidation = null;
|
|
419
|
+
}
|
|
210
420
|
|
|
211
|
-
|
|
212
|
-
|
|
421
|
+
// Check if there's already an active navigation to this URL
|
|
422
|
+
if (this._activeNavigation?.targetUrl === targetUrl) {
|
|
423
|
+
return this._activeNavigation.control;
|
|
213
424
|
}
|
|
214
425
|
|
|
215
|
-
|
|
426
|
+
// Check if there's a prefetch to upgrade
|
|
427
|
+
const existingPrefetch = this._prefetchCache.get(targetUrl);
|
|
428
|
+
if (existingPrefetch) {
|
|
429
|
+
// Upgrade prefetch: move from cache to active slot, change intent
|
|
430
|
+
this._prefetchCache.delete(targetUrl);
|
|
431
|
+
existingPrefetch.type = "userNavigation";
|
|
432
|
+
existingPrefetch.intent = "navigate";
|
|
433
|
+
existingPrefetch.scrollToTop = props.scrollToTop;
|
|
434
|
+
existingPrefetch.replace = props.replace;
|
|
435
|
+
existingPrefetch.state = props.state;
|
|
436
|
+
this._activeNavigation = existingPrefetch;
|
|
437
|
+
this.scheduleStatusUpdate();
|
|
438
|
+
return existingPrefetch.control;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Check if there's a pending revalidation to the same URL - upgrade it
|
|
442
|
+
if (this._pendingRevalidation?.targetUrl === targetUrl) {
|
|
443
|
+
// Upgrade revalidation: change intent so user gets proper link semantics
|
|
444
|
+
this._pendingRevalidation.type = "userNavigation";
|
|
445
|
+
this._pendingRevalidation.intent = "navigate";
|
|
446
|
+
this._pendingRevalidation.scrollToTop = props.scrollToTop;
|
|
447
|
+
this._pendingRevalidation.replace = props.replace;
|
|
448
|
+
this._pendingRevalidation.state = props.state;
|
|
449
|
+
return this._pendingRevalidation.control;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return this.createActiveNavigation(props, "navigate");
|
|
216
453
|
}
|
|
217
454
|
|
|
218
455
|
private beginPrefetch(
|
|
219
456
|
props: NavigateProps,
|
|
220
|
-
|
|
457
|
+
targetUrl: string,
|
|
221
458
|
): NavigationControl {
|
|
222
|
-
|
|
459
|
+
// If there's already an active navigation to this URL, return its control
|
|
460
|
+
if (this._activeNavigation?.targetUrl === targetUrl) {
|
|
461
|
+
return this._activeNavigation.control;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// If there's already a prefetch to this URL, return its control
|
|
465
|
+
const existingPrefetch = this._prefetchCache.get(targetUrl);
|
|
466
|
+
if (existingPrefetch) {
|
|
467
|
+
return existingPrefetch.control;
|
|
468
|
+
}
|
|
223
469
|
|
|
224
|
-
|
|
225
|
-
|
|
470
|
+
// If there's a pending revalidation to this URL, return its control
|
|
471
|
+
if (this._pendingRevalidation?.targetUrl === targetUrl) {
|
|
472
|
+
return this._pendingRevalidation.control;
|
|
226
473
|
}
|
|
227
474
|
|
|
228
475
|
// Don't prefetch current page
|
|
@@ -231,45 +478,37 @@ class NavigationStateManager {
|
|
|
231
478
|
currentUrl.hash = "";
|
|
232
479
|
targetUrlObj.hash = "";
|
|
233
480
|
if (currentUrl.href === targetUrlObj.href) {
|
|
234
|
-
// Return a no-op control
|
|
235
481
|
return {
|
|
236
482
|
abortController: new AbortController(),
|
|
237
|
-
promise: Promise.resolve(
|
|
483
|
+
promise: Promise.resolve({ type: "aborted" as const }),
|
|
238
484
|
};
|
|
239
485
|
}
|
|
240
486
|
|
|
241
|
-
return this.
|
|
487
|
+
return this.createPrefetch(props, targetUrl);
|
|
242
488
|
}
|
|
243
489
|
|
|
244
490
|
private beginRevalidation(props: NavigateProps): NavigationControl {
|
|
245
|
-
// Store current URL to validate against later
|
|
246
491
|
const currentUrl = window.location.href;
|
|
247
492
|
|
|
248
|
-
//
|
|
249
|
-
const existing = this._navigations.get(currentUrl);
|
|
493
|
+
// Coalesce recent revalidations
|
|
250
494
|
if (
|
|
251
|
-
|
|
252
|
-
Date.now() -
|
|
495
|
+
this._pendingRevalidation &&
|
|
496
|
+
Date.now() - this._pendingRevalidation.startTime <
|
|
497
|
+
this.REVALIDATION_COALESCE_MS
|
|
253
498
|
) {
|
|
254
|
-
return
|
|
499
|
+
return this._pendingRevalidation.control;
|
|
255
500
|
}
|
|
256
501
|
|
|
257
|
-
// Abort
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
this.deleteNavigation(key);
|
|
262
|
-
}
|
|
502
|
+
// Abort existing revalidation
|
|
503
|
+
if (this._pendingRevalidation) {
|
|
504
|
+
this._pendingRevalidation.control.abortController?.abort();
|
|
505
|
+
this._pendingRevalidation = null;
|
|
263
506
|
}
|
|
264
507
|
|
|
265
|
-
|
|
266
|
-
return this.createNavigation(
|
|
267
|
-
{ ...props, href: currentUrl },
|
|
268
|
-
"revalidate",
|
|
269
|
-
);
|
|
508
|
+
return this.createRevalidation({ ...props, href: currentUrl });
|
|
270
509
|
}
|
|
271
510
|
|
|
272
|
-
private
|
|
511
|
+
private createActiveNavigation(
|
|
273
512
|
props: NavigateProps,
|
|
274
513
|
intent: NavigationIntent,
|
|
275
514
|
): NavigationControl {
|
|
@@ -297,206 +536,168 @@ class NavigationStateManager {
|
|
|
297
536
|
state: props.state,
|
|
298
537
|
};
|
|
299
538
|
|
|
300
|
-
this.
|
|
539
|
+
this._activeNavigation = entry;
|
|
540
|
+
this.scheduleStatusUpdate();
|
|
301
541
|
return entry.control;
|
|
302
542
|
}
|
|
303
543
|
|
|
304
|
-
private
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
544
|
+
private createPrefetch(
|
|
545
|
+
props: NavigateProps,
|
|
546
|
+
targetUrl: string,
|
|
547
|
+
): NavigationControl {
|
|
548
|
+
const controller = new AbortController();
|
|
549
|
+
|
|
550
|
+
const entry: NavigationEntry = {
|
|
551
|
+
control: {
|
|
552
|
+
abortController: controller,
|
|
553
|
+
promise: this.fetchRouteData(controller, props).catch(
|
|
554
|
+
(error) => {
|
|
555
|
+
this._prefetchCache.delete(targetUrl);
|
|
556
|
+
throw error;
|
|
557
|
+
},
|
|
558
|
+
),
|
|
559
|
+
},
|
|
560
|
+
type: "prefetch",
|
|
561
|
+
intent: "none",
|
|
562
|
+
phase: "fetching",
|
|
563
|
+
startTime: Date.now(),
|
|
564
|
+
targetUrl,
|
|
565
|
+
originUrl: window.location.href,
|
|
566
|
+
scrollToTop: props.scrollToTop,
|
|
567
|
+
replace: props.replace,
|
|
568
|
+
state: props.state,
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
this._prefetchCache.set(targetUrl, entry);
|
|
572
|
+
// No status update needed - prefetches don't affect status
|
|
573
|
+
return entry.control;
|
|
320
574
|
}
|
|
321
575
|
|
|
322
|
-
private
|
|
323
|
-
const
|
|
324
|
-
|
|
576
|
+
private createRevalidation(props: NavigateProps): NavigationControl {
|
|
577
|
+
const controller = new AbortController();
|
|
578
|
+
const targetUrl = new URL(props.href, window.location.href).href;
|
|
325
579
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
580
|
+
const entry: NavigationEntry = {
|
|
581
|
+
control: {
|
|
582
|
+
abortController: controller,
|
|
583
|
+
promise: this.fetchRouteData(controller, props).catch(
|
|
584
|
+
(error) => {
|
|
585
|
+
if (
|
|
586
|
+
this._pendingRevalidation?.targetUrl === targetUrl
|
|
587
|
+
) {
|
|
588
|
+
this._pendingRevalidation = null;
|
|
589
|
+
this.scheduleStatusUpdate();
|
|
590
|
+
}
|
|
591
|
+
throw error;
|
|
592
|
+
},
|
|
593
|
+
),
|
|
594
|
+
},
|
|
595
|
+
type: "revalidation",
|
|
596
|
+
intent: "revalidate",
|
|
597
|
+
phase: "fetching",
|
|
598
|
+
startTime: Date.now(),
|
|
599
|
+
targetUrl,
|
|
600
|
+
originUrl: window.location.href,
|
|
601
|
+
scrollToTop: props.scrollToTop,
|
|
602
|
+
replace: props.replace,
|
|
603
|
+
state: props.state,
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
this._pendingRevalidation = entry;
|
|
607
|
+
this.scheduleStatusUpdate();
|
|
608
|
+
return entry.control;
|
|
330
609
|
}
|
|
331
610
|
|
|
332
|
-
private
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
611
|
+
private transitionPhase(targetUrl: string, phase: NavigationPhase): void {
|
|
612
|
+
if (this._activeNavigation?.targetUrl === targetUrl) {
|
|
613
|
+
this._activeNavigation.phase = phase;
|
|
614
|
+
this.scheduleStatusUpdate();
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const prefetch = this._prefetchCache.get(targetUrl);
|
|
619
|
+
if (prefetch) {
|
|
620
|
+
prefetch.phase = phase;
|
|
621
|
+
// No status update for prefetches
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (this._pendingRevalidation?.targetUrl === targetUrl) {
|
|
626
|
+
this._pendingRevalidation.phase = phase;
|
|
627
|
+
this.scheduleStatusUpdate();
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
private canSkipServerFetch(targetUrl: string): SkipCheckResult {
|
|
632
|
+
// Early return: no route manifest
|
|
339
633
|
const routeManifest = __vormaClientGlobal.get("routeManifest");
|
|
340
634
|
if (!routeManifest) {
|
|
341
635
|
return { canSkip: false };
|
|
342
636
|
}
|
|
343
637
|
|
|
638
|
+
// Early return: no pattern registry
|
|
344
639
|
const patternRegistry = __vormaClientGlobal.get("patternRegistry");
|
|
345
640
|
if (!patternRegistry) {
|
|
346
641
|
return { canSkip: false };
|
|
347
642
|
}
|
|
348
643
|
|
|
349
|
-
|
|
350
|
-
__vormaClientGlobal.get("patternToWaitFnMap") || {};
|
|
351
|
-
|
|
644
|
+
// Early return: no match
|
|
352
645
|
const url = new URL(targetUrl);
|
|
353
646
|
const matchResult = findNestedMatches(patternRegistry, url.pathname);
|
|
354
647
|
if (!matchResult) {
|
|
355
648
|
return { canSkip: false };
|
|
356
649
|
}
|
|
357
650
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
if (!stillMatched) {
|
|
374
|
-
// A server loader is being removed - must fetch from server
|
|
375
|
-
return { canSkip: false };
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// Block skip if the target introduces a new client loader
|
|
381
|
-
for (const m of matchResult.matches) {
|
|
382
|
-
const pattern = m.registeredPattern.originalPattern;
|
|
383
|
-
const hasClientLoader = !!patternToWaitFnMap[pattern];
|
|
384
|
-
const wasAlreadyMatched = currentMatchedPatterns.includes(pattern);
|
|
385
|
-
if (hasClientLoader && !wasAlreadyMatched) {
|
|
386
|
-
return { canSkip: false };
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
let outermostLoaderIndex = -1;
|
|
391
|
-
for (let i = matchResult.matches.length - 1; i >= 0; i--) {
|
|
392
|
-
const match: Match | undefined = matchResult.matches[i];
|
|
393
|
-
if (!match) continue;
|
|
394
|
-
|
|
395
|
-
const pattern = match.registeredPattern.originalPattern;
|
|
396
|
-
const hasServerLoader = routeManifest[pattern] === 1;
|
|
397
|
-
const hasClientLoader = !!patternToWaitFnMap[pattern];
|
|
651
|
+
// Build context for helper functions
|
|
652
|
+
const ctx: SkipCheckContext = {
|
|
653
|
+
routeManifest,
|
|
654
|
+
patternRegistry,
|
|
655
|
+
patternToWaitFnMap:
|
|
656
|
+
__vormaClientGlobal.get("patternToWaitFnMap") || {},
|
|
657
|
+
clientModuleMap: __vormaClientGlobal.get("clientModuleMap") || {},
|
|
658
|
+
currentMatchedPatterns:
|
|
659
|
+
__vormaClientGlobal.get("matchedPatterns") || [],
|
|
660
|
+
currentParams: __vormaClientGlobal.get("params") || {},
|
|
661
|
+
currentSplatValues: __vormaClientGlobal.get("splatValues") || [],
|
|
662
|
+
currentLoadersData: __vormaClientGlobal.get("loadersData") || [],
|
|
663
|
+
url,
|
|
664
|
+
matchResult,
|
|
665
|
+
};
|
|
398
666
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
}
|
|
667
|
+
// Early return: server loader being removed
|
|
668
|
+
if (hasServerLoaderRemoval(ctx)) {
|
|
669
|
+
return { canSkip: false };
|
|
403
670
|
}
|
|
404
671
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
currentUrlObj.searchParams.entries(),
|
|
408
|
-
).sort();
|
|
409
|
-
const targetParamsSorted = Array.from(
|
|
410
|
-
url.searchParams.entries(),
|
|
411
|
-
).sort();
|
|
412
|
-
const searchChanged = !jsonDeepEquals(
|
|
413
|
-
currentParamsSorted,
|
|
414
|
-
targetParamsSorted,
|
|
415
|
-
);
|
|
416
|
-
|
|
417
|
-
if (searchChanged && outermostLoaderIndex !== -1) {
|
|
672
|
+
// Early return: new client loader introduced
|
|
673
|
+
if (hasNewClientLoader(ctx)) {
|
|
418
674
|
return { canSkip: false };
|
|
419
675
|
}
|
|
420
676
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
if (outermostMatch) {
|
|
424
|
-
for (const seg of outermostMatch.registeredPattern
|
|
425
|
-
.normalizedSegments) {
|
|
426
|
-
if (seg.segType === "dynamic") {
|
|
427
|
-
const paramName = seg.normalizedVal.substring(1);
|
|
428
|
-
if (
|
|
429
|
-
matchResult.params[paramName] !==
|
|
430
|
-
currentParams[paramName]
|
|
431
|
-
) {
|
|
432
|
-
return { canSkip: false };
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
const hasSplat =
|
|
438
|
-
outermostMatch.registeredPattern.lastSegType === "splat";
|
|
677
|
+
// Find outermost loader index for param/search change checks
|
|
678
|
+
const outermostLoaderIndex = findOutermostLoaderIndex(ctx);
|
|
439
679
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
matchResult.splatValues,
|
|
444
|
-
currentSplatValues,
|
|
445
|
-
)
|
|
446
|
-
) {
|
|
447
|
-
return { canSkip: false };
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
}
|
|
680
|
+
// Early return: search params changed with loaders present
|
|
681
|
+
if (outermostLoaderIndex !== -1 && didSearchParamsChange(ctx)) {
|
|
682
|
+
return { canSkip: false };
|
|
451
683
|
}
|
|
452
684
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
if (!match) continue;
|
|
460
|
-
|
|
461
|
-
const pattern = match.registeredPattern.originalPattern;
|
|
462
|
-
|
|
463
|
-
const moduleInfo = clientModuleMap[pattern];
|
|
464
|
-
if (!moduleInfo) {
|
|
465
|
-
return { canSkip: false };
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
importURLs.push(moduleInfo.importURL);
|
|
469
|
-
exportKeys.push(moduleInfo.exportKey);
|
|
470
|
-
|
|
471
|
-
const hasServerLoader = routeManifest[pattern] === 1;
|
|
472
|
-
|
|
473
|
-
if (!hasServerLoader) {
|
|
474
|
-
loadersData.push(undefined);
|
|
475
|
-
} else {
|
|
476
|
-
const currentPatternIndex =
|
|
477
|
-
currentMatchedPatterns.indexOf(pattern);
|
|
478
|
-
|
|
479
|
-
if (currentPatternIndex === -1) {
|
|
480
|
-
// New server loader that we don't have data for
|
|
481
|
-
return { canSkip: false };
|
|
482
|
-
}
|
|
483
|
-
loadersData.push(currentLoadersData[currentPatternIndex]);
|
|
484
|
-
}
|
|
685
|
+
// Early return: outermost loader params changed
|
|
686
|
+
if (
|
|
687
|
+
outermostLoaderIndex !== -1 &&
|
|
688
|
+
didOutermostParamsChange(ctx, outermostLoaderIndex)
|
|
689
|
+
) {
|
|
690
|
+
return { canSkip: false };
|
|
485
691
|
}
|
|
486
692
|
|
|
487
|
-
return
|
|
488
|
-
|
|
489
|
-
matchResult,
|
|
490
|
-
importURLs,
|
|
491
|
-
exportKeys,
|
|
492
|
-
loadersData,
|
|
493
|
-
};
|
|
693
|
+
// Build and return skip result
|
|
694
|
+
return buildSkipResult(ctx);
|
|
494
695
|
}
|
|
495
696
|
|
|
496
697
|
private async fetchRouteData(
|
|
497
698
|
controller: AbortController,
|
|
498
699
|
props: NavigateProps,
|
|
499
|
-
): Promise<
|
|
700
|
+
): Promise<NavigationOutcome> {
|
|
500
701
|
try {
|
|
501
702
|
const url = new URL(props.href, window.location.href);
|
|
502
703
|
|
|
@@ -507,91 +708,12 @@ class NavigationStateManager {
|
|
|
507
708
|
) {
|
|
508
709
|
const skipCheck = this.canSkipServerFetch(url.href);
|
|
509
710
|
|
|
510
|
-
if (skipCheck.canSkip
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
// Build the response as if it came from the server
|
|
515
|
-
const json: GetRouteDataOutput = {
|
|
516
|
-
matchedPatterns: skipCheck.matchResult.matches.map(
|
|
517
|
-
(m: any) => m.registeredPattern.originalPattern,
|
|
518
|
-
),
|
|
519
|
-
loadersData: loadersData!,
|
|
520
|
-
importURLs: importURLs!,
|
|
521
|
-
exportKeys: exportKeys!,
|
|
522
|
-
hasRootData: __vormaClientGlobal.get("hasRootData"),
|
|
523
|
-
params: skipCheck.matchResult.params,
|
|
524
|
-
splatValues: skipCheck.matchResult.splatValues,
|
|
525
|
-
deps: [],
|
|
526
|
-
cssBundles: [],
|
|
527
|
-
outermostServerError: undefined,
|
|
528
|
-
outermostServerErrorIdx: undefined,
|
|
529
|
-
errorExportKeys: [],
|
|
530
|
-
title: undefined,
|
|
531
|
-
metaHeadEls: undefined,
|
|
532
|
-
restHeadEls: undefined,
|
|
533
|
-
activeComponents: undefined as unknown as [],
|
|
534
|
-
};
|
|
535
|
-
|
|
536
|
-
// Create a response object
|
|
537
|
-
const response = new Response(JSON.stringify(json), {
|
|
538
|
-
status: 200,
|
|
539
|
-
headers: {
|
|
540
|
-
"Content-Type": "application/json",
|
|
541
|
-
"X-Vorma-Build-Id":
|
|
542
|
-
__vormaClientGlobal.get("buildID") || "1",
|
|
543
|
-
},
|
|
544
|
-
});
|
|
545
|
-
|
|
546
|
-
const currentClientLoadersData =
|
|
547
|
-
__vormaClientGlobal.get("clientLoadersData") || [];
|
|
548
|
-
const patternToWaitFnMap =
|
|
549
|
-
__vormaClientGlobal.get("patternToWaitFnMap") || {};
|
|
550
|
-
const runningLoaders = new Map<string, Promise<any>>();
|
|
551
|
-
|
|
552
|
-
for (let i = 0; i < json.matchedPatterns.length; i++) {
|
|
553
|
-
const pattern = json.matchedPatterns[i];
|
|
554
|
-
if (!pattern) continue;
|
|
555
|
-
|
|
556
|
-
if (patternToWaitFnMap[pattern]) {
|
|
557
|
-
const currentMatchedPatterns =
|
|
558
|
-
__vormaClientGlobal.get("matchedPatterns") ||
|
|
559
|
-
[];
|
|
560
|
-
const currentPatternIndex =
|
|
561
|
-
currentMatchedPatterns.indexOf(pattern);
|
|
562
|
-
|
|
563
|
-
if (
|
|
564
|
-
currentPatternIndex !== -1 &&
|
|
565
|
-
currentClientLoadersData[
|
|
566
|
-
currentPatternIndex
|
|
567
|
-
] !== undefined
|
|
568
|
-
) {
|
|
569
|
-
runningLoaders.set(
|
|
570
|
-
pattern,
|
|
571
|
-
Promise.resolve(
|
|
572
|
-
currentClientLoadersData[
|
|
573
|
-
currentPatternIndex
|
|
574
|
-
],
|
|
575
|
-
),
|
|
576
|
-
);
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
const waitFnPromise = completeClientLoaders(
|
|
582
|
-
json,
|
|
583
|
-
__vormaClientGlobal.get("buildID") || "1",
|
|
584
|
-
runningLoaders,
|
|
585
|
-
controller.signal,
|
|
586
|
-
);
|
|
587
|
-
|
|
588
|
-
return {
|
|
589
|
-
response,
|
|
711
|
+
if (skipCheck.canSkip) {
|
|
712
|
+
return this.buildClientOnlyOutcome(
|
|
713
|
+
skipCheck,
|
|
590
714
|
props,
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
waitFnPromise,
|
|
594
|
-
};
|
|
715
|
+
controller,
|
|
716
|
+
);
|
|
595
717
|
}
|
|
596
718
|
}
|
|
597
719
|
|
|
@@ -614,7 +736,6 @@ class NavigationStateManager {
|
|
|
614
736
|
isPrefetch: props.navigationType === "prefetch",
|
|
615
737
|
redirectCount: props.redirectCount,
|
|
616
738
|
}).then(async (result) => {
|
|
617
|
-
// Read the response body once and return both the original result and parsed JSON
|
|
618
739
|
if (
|
|
619
740
|
result.response &&
|
|
620
741
|
result.response.ok &&
|
|
@@ -645,7 +766,6 @@ class NavigationStateManager {
|
|
|
645
766
|
const loaderFn = patternToWaitFnMap[pattern];
|
|
646
767
|
|
|
647
768
|
if (loaderFn) {
|
|
648
|
-
// Create a promise for this pattern's server data
|
|
649
769
|
const serverDataPromise = serverPromise
|
|
650
770
|
.then(
|
|
651
771
|
({
|
|
@@ -707,22 +827,18 @@ class NavigationStateManager {
|
|
|
707
827
|
const responseNotOK = !response?.ok && response?.status !== 304;
|
|
708
828
|
|
|
709
829
|
if (redirected || !response) {
|
|
710
|
-
// This is a valid end to a navigation attempt (e.g., a redirect occurred
|
|
711
|
-
// or the request was aborted). It's not an error.
|
|
712
830
|
controller.abort();
|
|
713
|
-
return
|
|
831
|
+
return { type: "aborted" };
|
|
714
832
|
}
|
|
715
833
|
|
|
716
834
|
if (responseNotOK) {
|
|
717
|
-
// This is a server error. Throwing an exception allows our .catch()
|
|
718
|
-
// blocks to handle cleanup and reset the loading state.
|
|
719
835
|
controller.abort();
|
|
720
836
|
throw new Error(`Fetch failed with status ${response.status}`);
|
|
721
837
|
}
|
|
722
838
|
|
|
723
839
|
if (redirectData?.status === "should") {
|
|
724
840
|
controller.abort();
|
|
725
|
-
return {
|
|
841
|
+
return { type: "redirect", redirectData, props };
|
|
726
842
|
}
|
|
727
843
|
|
|
728
844
|
if (!json) {
|
|
@@ -731,9 +847,6 @@ class NavigationStateManager {
|
|
|
731
847
|
}
|
|
732
848
|
|
|
733
849
|
// deps are only present in prod because they stem from the rollup metafile
|
|
734
|
-
// (same for CSS bundles -- vite handles them in dev)
|
|
735
|
-
// so in dev, to get similar behavior, we use the importURLs
|
|
736
|
-
// (which is a subset of what the deps would be in prod)
|
|
737
850
|
const depsToPreload = import.meta.env.DEV
|
|
738
851
|
? [...new Set(json.importURLs)]
|
|
739
852
|
: json.deps;
|
|
@@ -756,7 +869,14 @@ class NavigationStateManager {
|
|
|
756
869
|
cssBundlePromises.push(AssetManager.preloadCSS(bundle));
|
|
757
870
|
}
|
|
758
871
|
|
|
759
|
-
return {
|
|
872
|
+
return {
|
|
873
|
+
type: "success",
|
|
874
|
+
response,
|
|
875
|
+
json,
|
|
876
|
+
props,
|
|
877
|
+
cssBundlePromises,
|
|
878
|
+
waitFnPromise,
|
|
879
|
+
};
|
|
760
880
|
} catch (error) {
|
|
761
881
|
if (!isAbortError(error)) {
|
|
762
882
|
logError("Navigation failed", error);
|
|
@@ -765,49 +885,109 @@ class NavigationStateManager {
|
|
|
765
885
|
}
|
|
766
886
|
}
|
|
767
887
|
|
|
768
|
-
private
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
888
|
+
private buildClientOnlyOutcome(
|
|
889
|
+
skipCheck: Extract<SkipCheckResult, { canSkip: true }>,
|
|
890
|
+
props: NavigateProps,
|
|
891
|
+
controller: AbortController,
|
|
892
|
+
): NavigationOutcome {
|
|
893
|
+
const { matchResult, importURLs, exportKeys, loadersData } = skipCheck;
|
|
894
|
+
|
|
895
|
+
const json: GetRouteDataOutput = {
|
|
896
|
+
matchedPatterns: matchResult.matches.map(
|
|
897
|
+
(m: Match) => m.registeredPattern.originalPattern,
|
|
898
|
+
),
|
|
899
|
+
loadersData: loadersData,
|
|
900
|
+
importURLs: importURLs,
|
|
901
|
+
exportKeys: exportKeys,
|
|
902
|
+
hasRootData: __vormaClientGlobal.get("hasRootData"),
|
|
903
|
+
params: matchResult.params,
|
|
904
|
+
splatValues: matchResult.splatValues,
|
|
905
|
+
deps: [],
|
|
906
|
+
cssBundles: [],
|
|
907
|
+
outermostServerError: undefined,
|
|
908
|
+
outermostServerErrorIdx: undefined,
|
|
909
|
+
errorExportKeys: [],
|
|
910
|
+
title: undefined,
|
|
911
|
+
metaHeadEls: undefined,
|
|
912
|
+
restHeadEls: undefined,
|
|
913
|
+
activeComponents: undefined as unknown as [],
|
|
914
|
+
};
|
|
774
915
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
916
|
+
const response = new Response(JSON.stringify(json), {
|
|
917
|
+
status: 200,
|
|
918
|
+
headers: {
|
|
919
|
+
"Content-Type": "application/json",
|
|
920
|
+
"X-Vorma-Build-Id": __vormaClientGlobal.get("buildID") || "1",
|
|
921
|
+
},
|
|
922
|
+
});
|
|
781
923
|
|
|
782
|
-
|
|
783
|
-
|
|
924
|
+
const currentClientLoadersData =
|
|
925
|
+
__vormaClientGlobal.get("clientLoadersData") || [];
|
|
926
|
+
const patternToWaitFnMap =
|
|
927
|
+
__vormaClientGlobal.get("patternToWaitFnMap") || {};
|
|
928
|
+
const runningLoaders = new Map<string, Promise<any>>();
|
|
784
929
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
result.props,
|
|
789
|
-
);
|
|
790
|
-
return;
|
|
791
|
-
}
|
|
930
|
+
for (let i = 0; i < json.matchedPatterns.length; i++) {
|
|
931
|
+
const pattern = json.matchedPatterns[i];
|
|
932
|
+
if (!pattern) continue;
|
|
792
933
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
934
|
+
if (patternToWaitFnMap[pattern]) {
|
|
935
|
+
const currentMatchedPatterns =
|
|
936
|
+
__vormaClientGlobal.get("matchedPatterns") || [];
|
|
937
|
+
const currentPatternIndex =
|
|
938
|
+
currentMatchedPatterns.indexOf(pattern);
|
|
939
|
+
|
|
940
|
+
if (
|
|
941
|
+
currentPatternIndex !== -1 &&
|
|
942
|
+
currentClientLoadersData[currentPatternIndex] !== undefined
|
|
943
|
+
) {
|
|
944
|
+
runningLoaders.set(
|
|
945
|
+
pattern,
|
|
946
|
+
Promise.resolve(
|
|
947
|
+
currentClientLoadersData[currentPatternIndex],
|
|
948
|
+
),
|
|
949
|
+
);
|
|
950
|
+
}
|
|
797
951
|
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
const waitFnPromise = completeClientLoaders(
|
|
955
|
+
json,
|
|
956
|
+
__vormaClientGlobal.get("buildID") || "1",
|
|
957
|
+
runningLoaders,
|
|
958
|
+
controller.signal,
|
|
959
|
+
);
|
|
960
|
+
|
|
961
|
+
return {
|
|
962
|
+
type: "success",
|
|
963
|
+
response,
|
|
964
|
+
props,
|
|
965
|
+
json,
|
|
966
|
+
cssBundlePromises: [],
|
|
967
|
+
waitFnPromise,
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
async processSuccessfulNavigation(
|
|
972
|
+
outcome: Extract<NavigationOutcome, { type: "success" }>,
|
|
973
|
+
entry: NavigationEntry,
|
|
974
|
+
): Promise<void> {
|
|
975
|
+
try {
|
|
976
|
+
const { response, json, props, cssBundlePromises, waitFnPromise } =
|
|
977
|
+
outcome;
|
|
798
978
|
|
|
799
979
|
// Only update module map and apply CSS if build IDs match
|
|
800
980
|
const currentBuildID = __vormaClientGlobal.get("buildID");
|
|
801
|
-
const responseBuildID = getBuildIDFromResponse(
|
|
981
|
+
const responseBuildID = getBuildIDFromResponse(response);
|
|
802
982
|
|
|
803
983
|
if (responseBuildID === currentBuildID) {
|
|
804
984
|
// Update module map only when builds match
|
|
805
985
|
const clientModuleMap =
|
|
806
986
|
__vormaClientGlobal.get("clientModuleMap") || {};
|
|
807
|
-
const matchedPatterns =
|
|
808
|
-
const importURLs =
|
|
809
|
-
const exportKeys =
|
|
810
|
-
const errorExportKeys =
|
|
987
|
+
const matchedPatterns = json.matchedPatterns || [];
|
|
988
|
+
const importURLs = json.importURLs || [];
|
|
989
|
+
const exportKeys = json.exportKeys || [];
|
|
990
|
+
const errorExportKeys = json.errorExportKeys || [];
|
|
811
991
|
|
|
812
992
|
for (let i = 0; i < matchedPatterns.length; i++) {
|
|
813
993
|
const pattern = matchedPatterns[i];
|
|
@@ -826,15 +1006,9 @@ class NavigationStateManager {
|
|
|
826
1006
|
|
|
827
1007
|
__vormaClientGlobal.set("clientModuleMap", clientModuleMap);
|
|
828
1008
|
|
|
829
|
-
// Apply CSS bundles immediately, even for prefetches
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
// everything still works.
|
|
833
|
-
if (
|
|
834
|
-
result.json.cssBundles &&
|
|
835
|
-
result.json.cssBundles.length > 0
|
|
836
|
-
) {
|
|
837
|
-
AssetManager.applyCSS(result.json.cssBundles);
|
|
1009
|
+
// Apply CSS bundles immediately, even for prefetches
|
|
1010
|
+
if (json.cssBundles && json.cssBundles.length > 0) {
|
|
1011
|
+
AssetManager.applyCSS(json.cssBundles);
|
|
838
1012
|
}
|
|
839
1013
|
}
|
|
840
1014
|
|
|
@@ -851,25 +1025,25 @@ class NavigationStateManager {
|
|
|
851
1025
|
this.transitionPhase(entry.targetUrl, "waiting");
|
|
852
1026
|
|
|
853
1027
|
// Skip if navigation was aborted
|
|
854
|
-
if (!this.
|
|
1028
|
+
if (!this.findNavigationEntry(entry.targetUrl)) {
|
|
855
1029
|
return;
|
|
856
1030
|
}
|
|
857
1031
|
|
|
858
1032
|
// Update build ID if needed
|
|
859
1033
|
const oldID = __vormaClientGlobal.get("buildID");
|
|
860
|
-
const newID = getBuildIDFromResponse(
|
|
1034
|
+
const newID = getBuildIDFromResponse(response);
|
|
861
1035
|
if (newID && newID !== oldID) {
|
|
862
1036
|
dispatchBuildIDEvent({ newID, oldID });
|
|
863
1037
|
}
|
|
864
1038
|
|
|
865
1039
|
// Wait for client loaders and set state
|
|
866
|
-
const clientLoadersResult = await
|
|
1040
|
+
const clientLoadersResult = await waitFnPromise;
|
|
867
1041
|
setClientLoadersState(clientLoadersResult);
|
|
868
1042
|
|
|
869
1043
|
// Wait for CSS
|
|
870
|
-
if (
|
|
1044
|
+
if (cssBundlePromises.length > 0) {
|
|
871
1045
|
try {
|
|
872
|
-
await Promise.all(
|
|
1046
|
+
await Promise.all(cssBundlePromises);
|
|
873
1047
|
} catch (error) {
|
|
874
1048
|
logError("Error preloading CSS bundles:", error);
|
|
875
1049
|
}
|
|
@@ -895,16 +1069,15 @@ class NavigationStateManager {
|
|
|
895
1069
|
// Render the app
|
|
896
1070
|
try {
|
|
897
1071
|
await __reRenderApp({
|
|
898
|
-
json
|
|
1072
|
+
json,
|
|
899
1073
|
navigationType: entry.type,
|
|
900
1074
|
runHistoryOptions:
|
|
901
1075
|
entry.intent === "navigate"
|
|
902
1076
|
? {
|
|
903
1077
|
href: entry.targetUrl,
|
|
904
1078
|
scrollStateToRestore:
|
|
905
|
-
|
|
906
|
-
replace:
|
|
907
|
-
entry.replace || result.props.replace,
|
|
1079
|
+
props.scrollStateToRestore,
|
|
1080
|
+
replace: entry.replace || props.replace,
|
|
908
1081
|
scrollToTop: entry.scrollToTop,
|
|
909
1082
|
state: entry.state,
|
|
910
1083
|
}
|
|
@@ -993,7 +1166,7 @@ class NavigationStateManager {
|
|
|
993
1166
|
|
|
994
1167
|
if (redirectData?.status === "should") {
|
|
995
1168
|
await effectuateRedirectDataResult(redirectData, 0);
|
|
996
|
-
return { success: true, data: undefined as T };
|
|
1169
|
+
return { success: true, data: undefined as T };
|
|
997
1170
|
}
|
|
998
1171
|
|
|
999
1172
|
const data = await response.json();
|
|
@@ -1021,61 +1194,100 @@ class NavigationStateManager {
|
|
|
1021
1194
|
}
|
|
1022
1195
|
}
|
|
1023
1196
|
|
|
1024
|
-
private
|
|
1025
|
-
|
|
1026
|
-
|
|
1197
|
+
private findNavigationEntry(
|
|
1198
|
+
targetUrl: string,
|
|
1199
|
+
): NavigationEntry | undefined {
|
|
1200
|
+
if (this._activeNavigation?.targetUrl === targetUrl) {
|
|
1201
|
+
return this._activeNavigation;
|
|
1202
|
+
}
|
|
1203
|
+
const prefetch = this._prefetchCache.get(targetUrl);
|
|
1204
|
+
if (prefetch) {
|
|
1205
|
+
return prefetch;
|
|
1206
|
+
}
|
|
1207
|
+
if (this._pendingRevalidation?.targetUrl === targetUrl) {
|
|
1208
|
+
return this._pendingRevalidation;
|
|
1209
|
+
}
|
|
1210
|
+
return undefined;
|
|
1027
1211
|
}
|
|
1028
1212
|
|
|
1029
1213
|
private deleteNavigation(key: string): boolean {
|
|
1030
|
-
|
|
1031
|
-
if (
|
|
1214
|
+
// Check active navigation
|
|
1215
|
+
if (this._activeNavigation?.targetUrl === key) {
|
|
1216
|
+
this._activeNavigation = null;
|
|
1217
|
+
this.scheduleStatusUpdate();
|
|
1218
|
+
return true;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// Check prefetch cache
|
|
1222
|
+
if (this._prefetchCache.has(key)) {
|
|
1223
|
+
this._prefetchCache.delete(key);
|
|
1224
|
+
// No status update for prefetches
|
|
1225
|
+
return true;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// Check pending revalidation
|
|
1229
|
+
if (this._pendingRevalidation?.targetUrl === key) {
|
|
1230
|
+
this._pendingRevalidation = null;
|
|
1032
1231
|
this.scheduleStatusUpdate();
|
|
1232
|
+
return true;
|
|
1033
1233
|
}
|
|
1034
|
-
|
|
1234
|
+
|
|
1235
|
+
return false;
|
|
1035
1236
|
}
|
|
1036
1237
|
|
|
1037
1238
|
removeNavigation(key: string): void {
|
|
1038
|
-
this.
|
|
1239
|
+
const entry = this.findNavigationEntry(key);
|
|
1240
|
+
if (entry) {
|
|
1241
|
+
entry.control.abortController?.abort();
|
|
1242
|
+
this.deleteNavigation(key);
|
|
1243
|
+
}
|
|
1039
1244
|
}
|
|
1040
1245
|
|
|
1041
1246
|
getNavigation(key: string): NavigationEntry | undefined {
|
|
1042
|
-
return this.
|
|
1247
|
+
return this.findNavigationEntry(key);
|
|
1043
1248
|
}
|
|
1044
1249
|
|
|
1045
1250
|
hasNavigation(key: string): boolean {
|
|
1046
|
-
return this.
|
|
1251
|
+
return this.findNavigationEntry(key) !== undefined;
|
|
1047
1252
|
}
|
|
1048
1253
|
|
|
1049
1254
|
getNavigationsSize(): number {
|
|
1050
|
-
|
|
1255
|
+
let size = 0;
|
|
1256
|
+
if (this._activeNavigation) size++;
|
|
1257
|
+
size += this._prefetchCache.size;
|
|
1258
|
+
if (this._pendingRevalidation) size++;
|
|
1259
|
+
return size;
|
|
1051
1260
|
}
|
|
1052
1261
|
|
|
1053
1262
|
getNavigations(): Map<string, NavigationEntry> {
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1263
|
+
// Reconstruct Map for compatibility with existing code
|
|
1264
|
+
const map = new Map<string, NavigationEntry>();
|
|
1265
|
+
if (this._activeNavigation) {
|
|
1266
|
+
map.set(this._activeNavigation.targetUrl, this._activeNavigation);
|
|
1267
|
+
}
|
|
1268
|
+
for (const [key, entry] of this._prefetchCache) {
|
|
1269
|
+
map.set(key, entry);
|
|
1270
|
+
}
|
|
1271
|
+
if (this._pendingRevalidation) {
|
|
1272
|
+
map.set(
|
|
1273
|
+
this._pendingRevalidation.targetUrl,
|
|
1274
|
+
this._pendingRevalidation,
|
|
1275
|
+
);
|
|
1063
1276
|
}
|
|
1277
|
+
return map;
|
|
1064
1278
|
}
|
|
1065
1279
|
|
|
1066
1280
|
getStatus(): StatusEventDetail {
|
|
1067
|
-
const
|
|
1068
|
-
|
|
1281
|
+
const isNavigating =
|
|
1282
|
+
this._activeNavigation !== null &&
|
|
1283
|
+
this._activeNavigation.intent === "navigate" &&
|
|
1284
|
+
this._activeNavigation.phase !== "complete";
|
|
1069
1285
|
|
|
1070
|
-
const
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
const isRevalidating = navigations.some(
|
|
1075
|
-
(nav) => nav.type === "revalidation" && nav.phase !== "complete",
|
|
1076
|
-
);
|
|
1286
|
+
const isRevalidating =
|
|
1287
|
+
this._pendingRevalidation !== null &&
|
|
1288
|
+
this._pendingRevalidation.phase !== "complete";
|
|
1077
1289
|
|
|
1078
|
-
const isSubmitting =
|
|
1290
|
+
const isSubmitting = Array.from(this._submissions.values()).some(
|
|
1079
1291
|
(x) => !x.skipGlobalLoadingIndicator,
|
|
1080
1292
|
);
|
|
1081
1293
|
|
|
@@ -1083,14 +1295,26 @@ class NavigationStateManager {
|
|
|
1083
1295
|
}
|
|
1084
1296
|
|
|
1085
1297
|
clearAll(): void {
|
|
1086
|
-
|
|
1087
|
-
|
|
1298
|
+
if (this._activeNavigation) {
|
|
1299
|
+
this._activeNavigation.control.abortController?.abort();
|
|
1300
|
+
this._activeNavigation = null;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
for (const prefetch of this._prefetchCache.values()) {
|
|
1304
|
+
prefetch.control.abortController?.abort();
|
|
1088
1305
|
}
|
|
1089
|
-
this.
|
|
1306
|
+
this._prefetchCache.clear();
|
|
1307
|
+
|
|
1308
|
+
if (this._pendingRevalidation) {
|
|
1309
|
+
this._pendingRevalidation.control.abortController?.abort();
|
|
1310
|
+
this._pendingRevalidation = null;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1090
1313
|
for (const sub of this._submissions.values()) {
|
|
1091
1314
|
sub.control.abortController?.abort();
|
|
1092
1315
|
}
|
|
1093
1316
|
this._submissions.clear();
|
|
1317
|
+
|
|
1094
1318
|
this.scheduleStatusUpdate();
|
|
1095
1319
|
}
|
|
1096
1320
|
|