@rangojs/router 0.2.0 → 0.4.1

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.
Files changed (105) hide show
  1. package/README.md +19 -1
  2. package/dist/types/browser/event-controller.d.ts +4 -0
  3. package/dist/types/browser/link-interceptor.d.ts +17 -7
  4. package/dist/types/browser/navigation-bridge.d.ts +13 -1
  5. package/dist/types/browser/notify-listeners.d.ts +2 -0
  6. package/dist/types/browser/prefetch/cache.d.ts +1 -1
  7. package/dist/types/browser/prefetch/default-strategy.d.ts +31 -0
  8. package/dist/types/browser/prefetch/invalidation.d.ts +6 -0
  9. package/dist/types/browser/prefetch/loader.d.ts +3 -1
  10. package/dist/types/browser/prefetch/observer.d.ts +3 -6
  11. package/dist/types/browser/prefetch/runtime.d.ts +0 -1
  12. package/dist/types/browser/rango-state.d.ts +4 -0
  13. package/dist/types/browser/react/Link.d.ts +11 -19
  14. package/dist/types/browser/react/context.d.ts +3 -0
  15. package/dist/types/browser/rsc-router.d.ts +7 -2
  16. package/dist/types/browser/types.d.ts +6 -0
  17. package/dist/types/cache/cache-key-utils.d.ts +11 -3
  18. package/dist/types/cache/cache-scope.d.ts +82 -3
  19. package/dist/types/cache/cf/cf-cache-store.d.ts +2 -2
  20. package/dist/types/cache/index.d.ts +3 -1
  21. package/dist/types/cache/search-params-filter.d.ts +64 -0
  22. package/dist/types/cache/shell-snapshot.d.ts +4 -4
  23. package/dist/types/cache/types.d.ts +36 -2
  24. package/dist/types/cache/vercel/vercel-cache-store.d.ts +2 -2
  25. package/dist/types/index.d.ts +1 -0
  26. package/dist/types/index.rsc.d.ts +1 -0
  27. package/dist/types/router/match-middleware/cache-lookup.d.ts +13 -0
  28. package/dist/types/router/navigation-snapshot.d.ts +29 -0
  29. package/dist/types/router/prefetch-default.d.ts +28 -0
  30. package/dist/types/router/router-interfaces.d.ts +7 -0
  31. package/dist/types/router/router-options.d.ts +54 -0
  32. package/dist/types/rsc/capture-queue.d.ts +6 -0
  33. package/dist/types/rsc/shell-build-manifest.d.ts +7 -1
  34. package/dist/types/rsc/shell-capture.d.ts +15 -7
  35. package/dist/types/rsc/shell-serve.d.ts +11 -4
  36. package/dist/types/rsc/types.d.ts +19 -0
  37. package/dist/types/server/request-context.d.ts +57 -4
  38. package/dist/types/testing/e2e/index.d.ts +2 -2
  39. package/dist/types/testing/e2e/page-helpers.d.ts +24 -0
  40. package/dist/types/testing/render-route.d.ts +10 -1
  41. package/dist/types/testing/shell-status.d.ts +6 -3
  42. package/dist/types/vite/plugin-types.d.ts +1 -1
  43. package/dist/vite/index.js +9 -6
  44. package/package.json +23 -22
  45. package/skills/caching/SKILL.md +39 -0
  46. package/skills/comparison/references/framework-comparison.md +9 -2
  47. package/skills/links/SKILL.md +24 -0
  48. package/skills/ppr/SKILL.md +80 -16
  49. package/skills/router-setup/SKILL.md +9 -0
  50. package/skills/testing/client-components.md +15 -14
  51. package/skills/vercel/SKILL.md +1 -1
  52. package/src/browser/event-controller.ts +110 -11
  53. package/src/browser/link-interceptor.ts +469 -20
  54. package/src/browser/navigation-bridge.ts +71 -2
  55. package/src/browser/navigation-store.ts +24 -1
  56. package/src/browser/notify-listeners.ts +22 -0
  57. package/src/browser/prefetch/cache.ts +4 -2
  58. package/src/browser/prefetch/default-strategy.ts +74 -0
  59. package/src/browser/prefetch/invalidation.ts +30 -0
  60. package/src/browser/prefetch/loader.ts +18 -17
  61. package/src/browser/prefetch/observer.ts +50 -22
  62. package/src/browser/prefetch/runtime.ts +0 -1
  63. package/src/browser/rango-state.ts +21 -0
  64. package/src/browser/react/Link.tsx +111 -101
  65. package/src/browser/react/NavigationProvider.tsx +1 -0
  66. package/src/browser/react/context.ts +4 -0
  67. package/src/browser/rsc-router.tsx +30 -9
  68. package/src/browser/types.ts +6 -0
  69. package/src/cache/cache-key-utils.ts +18 -4
  70. package/src/cache/cache-runtime.ts +7 -2
  71. package/src/cache/cache-scope.ts +152 -23
  72. package/src/cache/cf/cf-cache-store.ts +55 -16
  73. package/src/cache/document-cache.ts +7 -1
  74. package/src/cache/index.ts +7 -0
  75. package/src/cache/search-params-filter.ts +118 -0
  76. package/src/cache/shell-snapshot.ts +8 -4
  77. package/src/cache/types.ts +39 -2
  78. package/src/cache/vercel/vercel-cache-store.ts +22 -3
  79. package/src/index.rsc.ts +4 -0
  80. package/src/index.ts +6 -0
  81. package/src/router/match-middleware/cache-lookup.ts +146 -33
  82. package/src/router/match-middleware/cache-store.ts +70 -0
  83. package/src/router/navigation-snapshot.ts +46 -6
  84. package/src/router/prefetch-default.ts +59 -0
  85. package/src/router/router-interfaces.ts +8 -0
  86. package/src/router/router-options.ts +59 -1
  87. package/src/router.ts +7 -0
  88. package/src/rsc/capture-queue.ts +24 -1
  89. package/src/rsc/full-payload.ts +1 -0
  90. package/src/rsc/handler.ts +10 -0
  91. package/src/rsc/response-cache-serve.ts +1 -1
  92. package/src/rsc/rsc-rendering.ts +367 -40
  93. package/src/rsc/shell-build-manifest.ts +8 -1
  94. package/src/rsc/shell-capture.ts +99 -50
  95. package/src/rsc/shell-serve.ts +14 -6
  96. package/src/rsc/types.ts +19 -0
  97. package/src/server/request-context.ts +62 -3
  98. package/src/testing/dispatch.ts +21 -3
  99. package/src/testing/e2e/index.ts +6 -0
  100. package/src/testing/e2e/page-helpers.ts +47 -0
  101. package/src/testing/e2e/parity.ts +13 -0
  102. package/src/testing/render-route.tsx +86 -17
  103. package/src/testing/shell-status.ts +20 -3
  104. package/src/vite/plugin-types.ts +1 -1
  105. package/src/vite/plugins/vercel-output.ts +2 -2
@@ -14,6 +14,7 @@ import {
14
14
  filterSegmentOrder,
15
15
  filterRouteSegmentIds,
16
16
  } from "./react/filter-segment-order.js";
17
+ import { notifyListeners } from "./notify-listeners.js";
17
18
 
18
19
  // Polyfill Symbol.dispose for Safari and older browsers
19
20
  if (typeof Symbol.dispose === "undefined") {
@@ -269,6 +270,71 @@ export interface EventController {
269
270
  hadAnyConcurrentActions(): boolean;
270
271
  }
271
272
 
273
+ type LocationChangeController = Pick<EventController, "getState" | "subscribe">;
274
+
275
+ interface LocationChangeSubscription {
276
+ registrations: Map<
277
+ symbol,
278
+ { href: string; listener: (href: string) => void }
279
+ >;
280
+ notificationVersion: number;
281
+ unsubscribe: () => void;
282
+ }
283
+
284
+ const locationChangeSubscriptions = new WeakMap<
285
+ LocationChangeController,
286
+ LocationChangeSubscription
287
+ >();
288
+
289
+ /** Share one controller subscription across all location-change consumers. */
290
+ export function subscribeToLocationChange(
291
+ eventController: LocationChangeController,
292
+ listener: (href: string) => void,
293
+ ): () => void {
294
+ let subscription = locationChangeSubscriptions.get(eventController);
295
+ if (!subscription) {
296
+ subscription = {
297
+ registrations: new Map(),
298
+ notificationVersion: 0,
299
+ unsubscribe: () => {},
300
+ };
301
+ locationChangeSubscriptions.set(eventController, subscription);
302
+ const currentSubscription = subscription;
303
+ subscription.unsubscribe = eventController.subscribe(() => {
304
+ const notificationVersion = ++currentSubscription.notificationVersion;
305
+ const nextHref = eventController.getState().location.href;
306
+ notifyListeners(
307
+ [...currentSubscription.registrations],
308
+ ([, registration]) => {
309
+ if (registration.href === nextHref) return;
310
+ registration.href = nextHref;
311
+ registration.listener(nextHref);
312
+ },
313
+ ([token, registration]) =>
314
+ currentSubscription.registrations.get(token) === registration,
315
+ () => notificationVersion === currentSubscription.notificationVersion,
316
+ );
317
+ });
318
+ }
319
+
320
+ const token = Symbol();
321
+ subscription.registrations.set(token, {
322
+ href: eventController.getState().location.href,
323
+ listener,
324
+ });
325
+ let active = true;
326
+ return () => {
327
+ if (!active) return;
328
+ active = false;
329
+ subscription!.registrations.delete(token);
330
+ if (subscription!.registrations.size > 0) return;
331
+ if (locationChangeSubscriptions.get(eventController) === subscription) {
332
+ locationChangeSubscriptions.delete(eventController);
333
+ }
334
+ subscription!.unsubscribe();
335
+ };
336
+ }
337
+
272
338
  const DEFAULT_ACTION_STATE: TrackedActionState = {
273
339
  state: "idle",
274
340
  actionId: null,
@@ -308,7 +374,11 @@ function makeDebouncedNotifier(listeners: Set<() => void>): () => void {
308
374
  if (timeout !== null) clearTimeout(timeout);
309
375
  timeout = setTimeout(() => {
310
376
  timeout = null;
311
- listeners.forEach((listener) => listener());
377
+ notifyListeners(
378
+ [...listeners],
379
+ (listener) => listener(),
380
+ (listener) => listeners.has(listener),
381
+ );
312
382
  }, 0);
313
383
  };
314
384
  }
@@ -394,6 +464,39 @@ export function createEventController(
394
464
 
395
465
  const actionNotifyTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
396
466
 
467
+ function notifyActionListenerSets(
468
+ subscriptions: Iterable<[string, Set<ActionStateListener>]>,
469
+ ): void {
470
+ const subscriptionSnapshots = [...subscriptions].map(
471
+ ([subscriptionId, listeners]) => ({
472
+ listenerSnapshot: [...listeners],
473
+ listeners,
474
+ subscriptionId,
475
+ }),
476
+ );
477
+ function* notifications(): Generator<{
478
+ listener: ActionStateListener;
479
+ listeners: Set<ActionStateListener>;
480
+ state: TrackedActionState;
481
+ }> {
482
+ for (const {
483
+ listenerSnapshot,
484
+ listeners,
485
+ subscriptionId,
486
+ } of subscriptionSnapshots) {
487
+ const state = getActionState(subscriptionId);
488
+ for (const listener of listenerSnapshot) {
489
+ yield { listener, listeners, state };
490
+ }
491
+ }
492
+ }
493
+ notifyListeners(
494
+ notifications(),
495
+ ({ listener, state }) => listener(state),
496
+ ({ listener, listeners }) => listeners.has(listener),
497
+ );
498
+ }
499
+
397
500
  function notifyAction(actionId: string) {
398
501
  const existing = actionNotifyTimeouts.get(actionId);
399
502
  if (existing !== undefined) {
@@ -403,12 +506,11 @@ export function createEventController(
403
506
  actionId,
404
507
  setTimeout(() => {
405
508
  actionNotifyTimeouts.delete(actionId);
406
- for (const [subscriptionId, listeners] of actionListeners) {
407
- if (matchesActionId(subscriptionId, actionId)) {
408
- const state = getActionState(subscriptionId);
409
- listeners.forEach((listener) => listener(state));
410
- }
411
- }
509
+ notifyActionListenerSets(
510
+ [...actionListeners].filter(([subscriptionId]) =>
511
+ matchesActionId(subscriptionId, actionId),
512
+ ),
513
+ );
412
514
  }, 0),
413
515
  );
414
516
  }
@@ -846,10 +948,7 @@ export function createEventController(
846
948
  // "addToCart"), not full entry actionIds. Passing them to notifyAction
847
949
  // would fail the suffix matcher — instead, notify each subscriber with
848
950
  // its own state.
849
- for (const [subscriptionId, listeners] of actionListeners) {
850
- const state = getActionState(subscriptionId);
851
- listeners.forEach((listener) => listener(state));
852
- }
951
+ notifyActionListenerSets(actionListeners);
853
952
  }
854
953
 
855
954
  // ========================================================================