@rangojs/router 0.0.0-experimental.18 → 0.0.0-experimental.19
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/README.md +46 -8
- package/dist/bin/rango.js +105 -18
- package/dist/vite/index.js +227 -93
- package/package.json +15 -14
- package/skills/hooks/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +79 -0
- package/skills/layout/SKILL.md +62 -2
- package/skills/loader/SKILL.md +94 -1
- package/skills/middleware/SKILL.md +81 -0
- package/skills/parallel/SKILL.md +57 -2
- package/skills/prerender/SKILL.md +187 -17
- package/skills/route/SKILL.md +42 -1
- package/skills/router-setup/SKILL.md +77 -0
- package/src/__internal.ts +1 -1
- package/src/bin/rango.ts +38 -19
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/event-controller.ts +25 -27
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +1 -1
- package/src/browser/link-interceptor.ts +0 -3
- package/src/browser/merge-segment-loaders.ts +9 -2
- package/src/browser/navigation-bridge.ts +46 -13
- package/src/browser/navigation-client.ts +32 -61
- package/src/browser/navigation-store.ts +1 -31
- package/src/browser/navigation-transaction.ts +46 -207
- package/src/browser/partial-update.ts +102 -150
- package/src/browser/{prefetch-cache.ts → prefetch/cache.ts} +23 -4
- package/src/browser/{prefetch-fetch.ts → prefetch/fetch.ts} +36 -8
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/{prefetch-queue.ts → prefetch/queue.ts} +10 -3
- package/src/browser/react/Link.tsx +28 -23
- package/src/browser/react/NavigationProvider.tsx +9 -1
- package/src/browser/react/index.ts +2 -6
- package/src/browser/react/location-state-shared.ts +1 -1
- package/src/browser/react/location-state.ts +2 -0
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/use-action.ts +9 -1
- package/src/browser/react/use-handle.ts +3 -25
- package/src/browser/react/use-params.ts +2 -4
- package/src/browser/react/use-pathname.ts +2 -3
- package/src/browser/react/use-router.ts +1 -1
- package/src/browser/react/use-search-params.ts +2 -1
- package/src/browser/react/use-segments.ts +7 -60
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +29 -23
- package/src/browser/scroll-restoration.ts +10 -7
- package/src/browser/server-action-bridge.ts +115 -96
- package/src/browser/types.ts +1 -31
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +5 -0
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/route-types/codegen.ts +13 -4
- package/src/build/route-types/include-resolution.ts +13 -0
- package/src/build/route-types/per-module-writer.ts +15 -3
- package/src/build/route-types/router-processing.ts +45 -3
- package/src/build/runtime-discovery.ts +13 -1
- 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 +132 -96
- package/src/cache/cache-scope.ts +71 -73
- package/src/cache/cf/cf-cache-store.ts +9 -4
- package/src/cache/document-cache.ts +72 -47
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/memory-segment-store.ts +18 -7
- package/src/cache/profile-registry.ts +43 -8
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +101 -112
- package/src/cache/taint.ts +26 -0
- package/src/client.tsx +53 -30
- package/src/errors.ts +6 -1
- package/src/handle.ts +1 -1
- package/src/handles/MetaTags.tsx +5 -2
- package/src/host/cookie-handler.ts +8 -3
- package/src/host/router.ts +14 -1
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +33 -1
- package/src/index.ts +27 -0
- package/src/loader.rsc.ts +12 -4
- package/src/loader.ts +8 -0
- package/src/prerender/store.ts +4 -3
- package/src/prerender.ts +76 -18
- package/src/reverse.ts +11 -7
- package/src/root-error-boundary.tsx +30 -26
- package/src/route-definition/dsl-helpers.ts +9 -6
- package/src/route-definition/redirect.ts +15 -3
- package/src/route-map-builder.ts +38 -2
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +7 -0
- package/src/router/content-negotiation.ts +1 -1
- package/src/router/debug-manifest.ts +16 -3
- package/src/router/handler-context.ts +94 -15
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/lazy-includes.ts +4 -0
- package/src/router/loader-resolution.ts +1 -0
- package/src/router/logging.ts +100 -3
- package/src/router/manifest.ts +32 -3
- package/src/router/match-api.ts +61 -7
- package/src/router/match-context.ts +3 -0
- package/src/router/match-handlers.ts +185 -11
- package/src/router/match-middleware/background-revalidation.ts +65 -85
- package/src/router/match-middleware/cache-lookup.ts +69 -4
- package/src/router/match-middleware/cache-store.ts +2 -0
- package/src/router/match-pipelines.ts +8 -43
- package/src/router/middleware-types.ts +7 -0
- package/src/router/middleware.ts +93 -8
- package/src/router/pattern-matching.ts +41 -5
- package/src/router/prerender-match.ts +34 -6
- package/src/router/preview-match.ts +7 -1
- package/src/router/revalidation.ts +61 -2
- package/src/router/router-context.ts +15 -0
- package/src/router/router-interfaces.ts +34 -0
- package/src/router/router-options.ts +200 -0
- package/src/router/segment-resolution/fresh.ts +123 -30
- package/src/router/segment-resolution/helpers.ts +19 -0
- package/src/router/segment-resolution/loader-cache.ts +37 -146
- package/src/router/segment-resolution/revalidation.ts +358 -94
- package/src/router/segment-wrappers.ts +3 -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/types.ts +7 -1
- package/src/router.ts +155 -11
- package/src/rsc/handler-context.ts +11 -0
- package/src/rsc/handler.ts +380 -88
- package/src/rsc/helpers.ts +25 -16
- package/src/rsc/loader-fetch.ts +84 -42
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +232 -19
- package/src/rsc/response-route-handler.ts +37 -26
- package/src/rsc/rsc-rendering.ts +12 -5
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +134 -58
- package/src/rsc/types.ts +8 -0
- package/src/search-params.ts +22 -10
- package/src/server/context.ts +53 -5
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +66 -9
- package/src/server/loader-registry.ts +11 -46
- package/src/server/request-context.ts +90 -9
- package/src/ssr/index.tsx +63 -27
- package/src/static-handler.ts +7 -0
- package/src/theme/ThemeProvider.tsx +6 -1
- package/src/theme/index.ts +1 -6
- package/src/theme/theme-context.ts +1 -28
- package/src/theme/theme-script.ts +2 -1
- package/src/types/cache-types.ts +5 -0
- package/src/types/error-types.ts +3 -0
- package/src/types/global-namespace.ts +9 -0
- package/src/types/handler-context.ts +35 -13
- package/src/types/loader-types.ts +7 -0
- package/src/types/route-entry.ts +28 -0
- package/src/urls/include-helper.ts +49 -8
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +30 -12
- package/src/urls/path-helper.ts +17 -2
- package/src/urls/pattern-types.ts +21 -1
- package/src/urls/response-types.ts +27 -2
- package/src/urls/type-extraction.ts +23 -15
- package/src/use-loader.tsx +12 -4
- package/src/vite/discovery/bundle-postprocess.ts +12 -7
- package/src/vite/discovery/discover-routers.ts +30 -18
- package/src/vite/discovery/prerender-collection.ts +24 -27
- package/src/vite/discovery/route-types-writer.ts +7 -7
- package/src/vite/discovery/virtual-module-codegen.ts +5 -2
- package/src/vite/plugins/client-ref-hashing.ts +3 -3
- package/src/vite/plugins/use-cache-transform.ts +91 -3
- package/src/vite/rango.ts +3 -3
- package/src/vite/router-discovery.ts +99 -36
- package/src/vite/utils/prerender-utils.ts +21 -0
- package/src/vite/utils/shared-utils.ts +3 -1
- package/src/browser/request-controller.ts +0 -164
- package/src/href-context.ts +0 -33
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- /package/src/browser/{prefetch-observer.ts → prefetch/observer.ts} +0 -0
|
@@ -292,10 +292,7 @@ export function createRouterDiscoveryPlugin(
|
|
|
292
292
|
serverMod.setManifestReadyPromise(discoveryPromise);
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
// Save registry for the /__rsc_prerender endpoint (avoids creating a temp server)
|
|
298
|
-
mainRegistry = serverModAfterDiscovery?.RouterRegistry ?? null;
|
|
295
|
+
await discoverRouters(s, rscEnv);
|
|
299
296
|
|
|
300
297
|
// Store server origin for dev prerender endpoint (virtual module injection)
|
|
301
298
|
s.devServerOrigin = getDevServerOrigin();
|
|
@@ -307,37 +304,8 @@ export function createRouterDiscoveryPlugin(
|
|
|
307
304
|
// won't cause unnecessary HMR triggers.
|
|
308
305
|
writeRouteTypesFiles(s);
|
|
309
306
|
|
|
310
|
-
// Populate the route map in the RSC env
|
|
311
|
-
|
|
312
|
-
serverMod.setCachedManifest(s.mergedRouteManifest);
|
|
313
|
-
}
|
|
314
|
-
if (
|
|
315
|
-
s.mergedPrecomputedEntries &&
|
|
316
|
-
s.mergedPrecomputedEntries.length > 0 &&
|
|
317
|
-
serverMod?.setPrecomputedEntries
|
|
318
|
-
) {
|
|
319
|
-
serverMod.setPrecomputedEntries(s.mergedPrecomputedEntries);
|
|
320
|
-
}
|
|
321
|
-
if (s.mergedRouteTrie && serverMod?.setRouteTrie) {
|
|
322
|
-
serverMod.setRouteTrie(s.mergedRouteTrie);
|
|
323
|
-
}
|
|
324
|
-
// Populate per-router isolated data eagerly in dev (HMR).
|
|
325
|
-
// In production builds, per-router data is loaded lazily via import().
|
|
326
|
-
if (serverMod?.setRouterManifest) {
|
|
327
|
-
for (const [routerId, manifest] of s.perRouterManifestDataMap) {
|
|
328
|
-
serverMod.setRouterManifest(routerId, manifest);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
if (serverMod?.setRouterTrie) {
|
|
332
|
-
for (const [routerId, trie] of s.perRouterTrieMap) {
|
|
333
|
-
serverMod.setRouterTrie(routerId, trie);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
if (serverMod?.setRouterPrecomputedEntries) {
|
|
337
|
-
for (const [routerId, entries] of s.perRouterPrecomputedMap) {
|
|
338
|
-
serverMod.setRouterPrecomputedEntries(routerId, entries);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
307
|
+
// Populate the route map and per-router data in the RSC env
|
|
308
|
+
await propagateDiscoveryState(rscEnv);
|
|
341
309
|
} catch (err: any) {
|
|
342
310
|
console.warn(
|
|
343
311
|
`[rsc-router] Router discovery failed: ${err.message}\n${err.stack}`,
|
|
@@ -365,6 +333,49 @@ export function createRouterDiscoveryPlugin(
|
|
|
365
333
|
// Registry from the main server's RSC environment (populated by discoverRouters)
|
|
366
334
|
let mainRegistry: Map<string, any> | null = null;
|
|
367
335
|
|
|
336
|
+
// Push discovery state (manifest, trie, precomputed entries) to the
|
|
337
|
+
// server module so runtime request handling uses the current routes.
|
|
338
|
+
// Shared by initial discovery and HMR-triggered re-discovery.
|
|
339
|
+
const propagateDiscoveryState = async (rscEnv: any) => {
|
|
340
|
+
const serverMod = await rscEnv.runner.import("@rangojs/router/server");
|
|
341
|
+
if (!serverMod) return;
|
|
342
|
+
// Clear stale per-router and global route data before repopulating.
|
|
343
|
+
// Without this, removed routers/routes survive in the per-router maps
|
|
344
|
+
// and shrunk precomputed entries or tries are never purged.
|
|
345
|
+
if (serverMod.clearAllRouterData) {
|
|
346
|
+
serverMod.clearAllRouterData();
|
|
347
|
+
}
|
|
348
|
+
mainRegistry = serverMod.RouterRegistry ?? null;
|
|
349
|
+
if (s.mergedRouteManifest && serverMod.setCachedManifest) {
|
|
350
|
+
serverMod.setCachedManifest(s.mergedRouteManifest);
|
|
351
|
+
}
|
|
352
|
+
if (
|
|
353
|
+
s.mergedPrecomputedEntries &&
|
|
354
|
+
s.mergedPrecomputedEntries.length > 0 &&
|
|
355
|
+
serverMod.setPrecomputedEntries
|
|
356
|
+
) {
|
|
357
|
+
serverMod.setPrecomputedEntries(s.mergedPrecomputedEntries);
|
|
358
|
+
}
|
|
359
|
+
if (s.mergedRouteTrie && serverMod.setRouteTrie) {
|
|
360
|
+
serverMod.setRouteTrie(s.mergedRouteTrie);
|
|
361
|
+
}
|
|
362
|
+
if (serverMod.setRouterManifest) {
|
|
363
|
+
for (const [routerId, manifest] of s.perRouterManifestDataMap) {
|
|
364
|
+
serverMod.setRouterManifest(routerId, manifest);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (serverMod.setRouterTrie) {
|
|
368
|
+
for (const [routerId, trie] of s.perRouterTrieMap) {
|
|
369
|
+
serverMod.setRouterTrie(routerId, trie);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (serverMod.setRouterPrecomputedEntries) {
|
|
373
|
+
for (const [routerId, entries] of s.perRouterPrecomputedMap) {
|
|
374
|
+
serverMod.setRouterPrecomputedEntries(routerId, entries);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
|
|
368
379
|
server.middlewares.use("/__rsc_prerender", async (req: any, res: any) => {
|
|
369
380
|
if (s.discoveryDone) await s.discoveryDone;
|
|
370
381
|
|
|
@@ -396,12 +407,24 @@ export function createRouterDiscoveryPlugin(
|
|
|
396
407
|
}
|
|
397
408
|
|
|
398
409
|
const wantIntercept = url.searchParams.get("intercept") === "1";
|
|
410
|
+
const wantRouteName = url.searchParams.get("routeName");
|
|
411
|
+
const wantPassthrough = url.searchParams.get("passthrough") === "1";
|
|
399
412
|
|
|
400
413
|
for (const [, routerInstance] of registry) {
|
|
401
414
|
if (!routerInstance.matchForPrerender) continue;
|
|
402
415
|
try {
|
|
403
|
-
const result = await routerInstance.matchForPrerender(
|
|
416
|
+
const result = await routerInstance.matchForPrerender(
|
|
417
|
+
pathname,
|
|
418
|
+
{},
|
|
419
|
+
undefined,
|
|
420
|
+
wantPassthrough,
|
|
421
|
+
);
|
|
404
422
|
if (!result) continue;
|
|
423
|
+
if (result.passthrough) continue;
|
|
424
|
+
// When routeName is specified, only accept a match for that route.
|
|
425
|
+
// This prevents returning the wrong entry when multiple routers
|
|
426
|
+
// have prerenderable routes sharing the same pathname.
|
|
427
|
+
if (wantRouteName && result.routeName !== wantRouteName) continue;
|
|
405
428
|
res.setHeader("content-type", "application/json");
|
|
406
429
|
let payload: Record<string, unknown>;
|
|
407
430
|
if (wantIntercept && result.interceptSegments?.length) {
|
|
@@ -449,6 +472,13 @@ export function createRouterDiscoveryPlugin(
|
|
|
449
472
|
): boolean => {
|
|
450
473
|
if (!isGeneratedRouteFile(filePath)) return false;
|
|
451
474
|
if (consumeSelfGenWrite(s, filePath)) return true;
|
|
475
|
+
// In Cloudflare dev (no module runner), perRouterManifests is never
|
|
476
|
+
// refreshed after HMR so regenerateGeneratedRouteFiles() would use
|
|
477
|
+
// stale data and revert user edits. Source files own route state;
|
|
478
|
+
// gen files are derived output. Skip regeneration and let the next
|
|
479
|
+
// source-file change rebuild them from the static parser.
|
|
480
|
+
const hasRunner = !!(server.environments as any)?.rsc?.runner;
|
|
481
|
+
if (!hasRunner) return true;
|
|
452
482
|
regenerateGeneratedRouteFiles();
|
|
453
483
|
return true;
|
|
454
484
|
};
|
|
@@ -459,6 +489,26 @@ export function createRouterDiscoveryPlugin(
|
|
|
459
489
|
// only the expensive regeneration is debounced.
|
|
460
490
|
let routeChangeTimer: ReturnType<typeof setTimeout> | undefined;
|
|
461
491
|
|
|
492
|
+
// Re-run runtime discovery so factory-generated routes that the
|
|
493
|
+
// static parser cannot see are refreshed after source changes.
|
|
494
|
+
let runtimeRediscoveryInProgress = false;
|
|
495
|
+
const refreshRuntimeDiscovery = async () => {
|
|
496
|
+
const rscEnv = (server.environments as any)?.rsc;
|
|
497
|
+
if (!rscEnv?.runner || runtimeRediscoveryInProgress) return;
|
|
498
|
+
runtimeRediscoveryInProgress = true;
|
|
499
|
+
try {
|
|
500
|
+
await discoverRouters(s, rscEnv);
|
|
501
|
+
writeRouteTypesFiles(s);
|
|
502
|
+
await propagateDiscoveryState(rscEnv);
|
|
503
|
+
} catch (err: any) {
|
|
504
|
+
console.warn(
|
|
505
|
+
`[rsc-router] Runtime re-discovery failed: ${err.message}`,
|
|
506
|
+
);
|
|
507
|
+
} finally {
|
|
508
|
+
runtimeRediscoveryInProgress = false;
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
|
|
462
512
|
const scheduleRouteRegeneration = () => {
|
|
463
513
|
clearTimeout(routeChangeTimer);
|
|
464
514
|
routeChangeTimer = setTimeout(() => {
|
|
@@ -473,6 +523,15 @@ export function createRouterDiscoveryPlugin(
|
|
|
473
523
|
`[rsc-router] Route regeneration error: ${err.message}`,
|
|
474
524
|
);
|
|
475
525
|
}
|
|
526
|
+
// Async: re-run runtime discovery to refresh factory-generated
|
|
527
|
+
// routes that the static parser cannot resolve.
|
|
528
|
+
if (s.perRouterManifests.length > 0) {
|
|
529
|
+
refreshRuntimeDiscovery().catch((err: any) => {
|
|
530
|
+
console.warn(
|
|
531
|
+
`[rsc-router] Runtime re-discovery error: ${err.message}`,
|
|
532
|
+
);
|
|
533
|
+
});
|
|
534
|
+
}
|
|
476
535
|
}, 100);
|
|
477
536
|
};
|
|
478
537
|
|
|
@@ -515,8 +574,12 @@ export function createRouterDiscoveryPlugin(
|
|
|
515
574
|
server.watcher.on("change", handleRouteFileChange);
|
|
516
575
|
|
|
517
576
|
// Regenerate gen files when they are deleted (e.g. manual cleanup).
|
|
577
|
+
// Same no-runner guard as change/add: stale perRouterManifests would
|
|
578
|
+
// reintroduce reverted content.
|
|
518
579
|
server.watcher.on("unlink", (filePath) => {
|
|
519
580
|
if (!isGeneratedRouteFile(filePath)) return;
|
|
581
|
+
const hasRunner = !!(server.environments as any)?.rsc?.runner;
|
|
582
|
+
if (!hasRunner) return;
|
|
520
583
|
regenerateGeneratedRouteFiles();
|
|
521
584
|
});
|
|
522
585
|
}
|
|
@@ -17,6 +17,27 @@ export function encodePathParam(value: unknown): string {
|
|
|
17
17
|
.join("/");
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Substitute route params into a pattern, stripping constraint and optional
|
|
22
|
+
* syntax (:param(a|b)? -> value). Also handles wildcard params (*key).
|
|
23
|
+
*/
|
|
24
|
+
export function substituteRouteParams(
|
|
25
|
+
pattern: string,
|
|
26
|
+
params: Record<string, string>,
|
|
27
|
+
encode: (value: string) => string = encodeURIComponent,
|
|
28
|
+
): string {
|
|
29
|
+
let result = pattern;
|
|
30
|
+
for (const [key, value] of Object.entries(params)) {
|
|
31
|
+
const escaped = escapeRegExp(key);
|
|
32
|
+
result = result.replace(
|
|
33
|
+
new RegExp(`:${escaped}(\\([^)]*\\))?\\??`),
|
|
34
|
+
encode(value),
|
|
35
|
+
);
|
|
36
|
+
result = result.replace(`*${key}`, encode(value));
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
20
41
|
/**
|
|
21
42
|
* Run an async function over items with bounded concurrency.
|
|
22
43
|
* Errors propagate immediately and abort remaining work.
|
|
@@ -89,9 +89,11 @@ export function createVirtualEntriesPlugin(
|
|
|
89
89
|
}
|
|
90
90
|
// Lazy RSC entry: routerPath may have been set by a config() hook
|
|
91
91
|
if (virtualId === VIRTUAL_IDS.rsc && routerPathRef?.path) {
|
|
92
|
-
const
|
|
92
|
+
const raw = routerPathRef.path.startsWith(".")
|
|
93
93
|
? "/" + routerPathRef.path.slice(2) // ./src/router.tsx -> /src/router.tsx
|
|
94
94
|
: routerPathRef.path;
|
|
95
|
+
// Normalize backslashes for Windows (path.join/slice preserve native separators)
|
|
96
|
+
const absoluteRouterPath = raw.replaceAll("\\", "/");
|
|
95
97
|
return getVirtualEntryRSC(absoluteRouterPath);
|
|
96
98
|
}
|
|
97
99
|
}
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import type { RequestController, DisposableAbortController } from "./types.js";
|
|
2
|
-
|
|
3
|
-
// Polyfill Symbol.dispose for Safari and older browsers
|
|
4
|
-
if (typeof Symbol.dispose === "undefined") {
|
|
5
|
-
(Symbol as any).dispose = Symbol("Symbol.dispose");
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Create a request controller for managing concurrent abort controllers
|
|
10
|
-
*
|
|
11
|
-
* This utility helps manage concurrent navigation requests by providing
|
|
12
|
-
* a way to abort all pending requests when a new navigation starts.
|
|
13
|
-
*
|
|
14
|
-
* @returns RequestController instance
|
|
15
|
-
*
|
|
16
|
-
* @example
|
|
17
|
-
* ```typescript
|
|
18
|
-
* const controller = createRequestController();
|
|
19
|
-
*
|
|
20
|
-
* // Start a new request
|
|
21
|
-
* const abortController = controller.create();
|
|
22
|
-
* fetch(url, { signal: abortController.signal });
|
|
23
|
-
*
|
|
24
|
-
* // Abort all pending requests (e.g., when starting new navigation)
|
|
25
|
-
* controller.abortAll();
|
|
26
|
-
*
|
|
27
|
-
* // Clean up completed request
|
|
28
|
-
* controller.remove(abortController);
|
|
29
|
-
* ```
|
|
30
|
-
*/
|
|
31
|
-
export function createRequestController(): RequestController {
|
|
32
|
-
// Navigation controllers - aborted on new navigation
|
|
33
|
-
// Using WeakRef to allow GC if controller is no longer referenced elsewhere
|
|
34
|
-
const controllers: WeakRef<AbortController>[] = [];
|
|
35
|
-
// Action controllers - NOT aborted by navigation, only by errors
|
|
36
|
-
const actionControllers: WeakRef<AbortController>[] = [];
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Remove stale (garbage collected) refs from an array
|
|
40
|
-
*/
|
|
41
|
-
function pruneStaleRefs(refs: WeakRef<AbortController>[]): void {
|
|
42
|
-
for (let i = refs.length - 1; i >= 0; i--) {
|
|
43
|
-
if (!refs[i].deref()) {
|
|
44
|
-
refs.splice(i, 1);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
/**
|
|
51
|
-
* Create a new abort controller and track it for navigation
|
|
52
|
-
*
|
|
53
|
-
* @returns A new AbortController
|
|
54
|
-
*/
|
|
55
|
-
create(): AbortController {
|
|
56
|
-
const controller = new AbortController();
|
|
57
|
-
controllers.push(new WeakRef(controller));
|
|
58
|
-
console.log(
|
|
59
|
-
`[Browser] Created abort controller, total: ${controllers.length}`,
|
|
60
|
-
);
|
|
61
|
-
return controller;
|
|
62
|
-
},
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Create a disposable abort controller for navigation use with `using` keyword
|
|
66
|
-
*
|
|
67
|
-
* The controller will be automatically removed from tracking when
|
|
68
|
-
* it goes out of scope, regardless of how the scope is exited.
|
|
69
|
-
*
|
|
70
|
-
* @returns A DisposableAbortController
|
|
71
|
-
*
|
|
72
|
-
* @example
|
|
73
|
-
* ```typescript
|
|
74
|
-
* async function handleNavigation() {
|
|
75
|
-
* requestController.abortAll();
|
|
76
|
-
* using { controller } = requestController.createDisposable();
|
|
77
|
-
* // ... use controller.signal ...
|
|
78
|
-
* // controller is automatically removed on scope exit
|
|
79
|
-
* }
|
|
80
|
-
* ```
|
|
81
|
-
*/
|
|
82
|
-
createDisposable(): DisposableAbortController {
|
|
83
|
-
const controller = this.create();
|
|
84
|
-
return {
|
|
85
|
-
controller,
|
|
86
|
-
[Symbol.dispose]: () => {
|
|
87
|
-
this.remove(controller);
|
|
88
|
-
},
|
|
89
|
-
};
|
|
90
|
-
},
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Create a disposable abort controller for actions
|
|
94
|
-
*
|
|
95
|
-
* Action controllers are NOT aborted by navigation - they complete
|
|
96
|
-
* independently. Only aborted by abortAllActions() on error.
|
|
97
|
-
*
|
|
98
|
-
* @returns A DisposableAbortController
|
|
99
|
-
*/
|
|
100
|
-
createActionDisposable(): DisposableAbortController {
|
|
101
|
-
const controller = new AbortController();
|
|
102
|
-
const ref = new WeakRef(controller);
|
|
103
|
-
actionControllers.push(ref);
|
|
104
|
-
console.log(
|
|
105
|
-
`[Browser] Created action controller, total: ${actionControllers.length}`,
|
|
106
|
-
);
|
|
107
|
-
return {
|
|
108
|
-
controller,
|
|
109
|
-
[Symbol.dispose]: () => {
|
|
110
|
-
const index = actionControllers.indexOf(ref);
|
|
111
|
-
if (index !== -1) {
|
|
112
|
-
actionControllers.splice(index, 1);
|
|
113
|
-
console.log(
|
|
114
|
-
`[Browser] Removed action controller, remaining: ${actionControllers.length}`,
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
},
|
|
118
|
-
};
|
|
119
|
-
},
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Abort all navigation controllers (NOT actions)
|
|
123
|
-
*
|
|
124
|
-
* Called when starting new navigation. Actions continue
|
|
125
|
-
* to complete in the background.
|
|
126
|
-
*/
|
|
127
|
-
abortAll(): void {
|
|
128
|
-
controllers.forEach((ref) => ref.deref()?.abort());
|
|
129
|
-
controllers.length = 0;
|
|
130
|
-
console.log(`[Browser] Aborted all navigation controllers`);
|
|
131
|
-
},
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Abort all action controllers
|
|
135
|
-
*
|
|
136
|
-
* Called when an action error occurs - prevents other actions
|
|
137
|
-
* from completing and overwriting the error UI.
|
|
138
|
-
*/
|
|
139
|
-
abortAllActions(): void {
|
|
140
|
-
actionControllers.forEach((ref) => ref.deref()?.abort());
|
|
141
|
-
actionControllers.length = 0;
|
|
142
|
-
console.log(`[Browser] Aborted all action controllers`);
|
|
143
|
-
},
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Remove a specific controller from tracking
|
|
147
|
-
*
|
|
148
|
-
* Call this when a request completes successfully.
|
|
149
|
-
*
|
|
150
|
-
* @param controller - The controller to remove
|
|
151
|
-
*/
|
|
152
|
-
remove(controller: AbortController): void {
|
|
153
|
-
// Prune any stale refs while searching
|
|
154
|
-
pruneStaleRefs(controllers);
|
|
155
|
-
const index = controllers.findIndex((ref) => ref.deref() === controller);
|
|
156
|
-
if (index !== -1) {
|
|
157
|
-
controllers.splice(index, 1);
|
|
158
|
-
console.log(
|
|
159
|
-
`[Browser] Removed abort controller, remaining: ${controllers.length}`,
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
},
|
|
163
|
-
};
|
|
164
|
-
}
|
package/src/href-context.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Href Context for route name resolution
|
|
5
|
-
*
|
|
6
|
-
* This module is marked "use client" so it can be imported by both:
|
|
7
|
-
* - Server (segment-system): Gets a client reference for createElement
|
|
8
|
-
* - Client (useHref): Uses the actual context for useContext
|
|
9
|
-
*
|
|
10
|
-
* The context stores:
|
|
11
|
-
* - routeMap: Map of route names to URL patterns
|
|
12
|
-
* - routeName: Current matched route name (for local name resolution)
|
|
13
|
-
*/
|
|
14
|
-
import { createContext, type Context } from "react";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Context value for href resolution
|
|
18
|
-
*/
|
|
19
|
-
export interface HrefContextValue {
|
|
20
|
-
/** Route map: route name -> URL pattern */
|
|
21
|
-
routeMap: Record<string, string>;
|
|
22
|
-
/** Current matched route name (includes name prefix from include()) */
|
|
23
|
-
routeName?: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Context for href resolution (route map and current route name)
|
|
28
|
-
*
|
|
29
|
-
* On the server: Populated by renderSegments() via HrefContext.Provider
|
|
30
|
-
* On the client: Populated by NavigationProvider from RSC metadata
|
|
31
|
-
*/
|
|
32
|
-
export const HrefContext: Context<HrefContextValue | null> =
|
|
33
|
-
createContext<HrefContextValue | null>(null);
|
package/src/router.gen.ts
DELETED
package/src/urls.gen.ts
DELETED
|
File without changes
|