@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.26

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 (225) hide show
  1. package/README.md +294 -28
  2. package/dist/bin/rango.js +355 -47
  3. package/dist/vite/index.js +1658 -1239
  4. package/package.json +3 -3
  5. package/skills/cache-guide/SKILL.md +9 -5
  6. package/skills/caching/SKILL.md +4 -4
  7. package/skills/document-cache/SKILL.md +2 -2
  8. package/skills/hooks/SKILL.md +40 -29
  9. package/skills/host-router/SKILL.md +218 -0
  10. package/skills/intercept/SKILL.md +79 -0
  11. package/skills/layout/SKILL.md +62 -2
  12. package/skills/loader/SKILL.md +229 -15
  13. package/skills/middleware/SKILL.md +109 -30
  14. package/skills/parallel/SKILL.md +57 -2
  15. package/skills/prerender/SKILL.md +189 -19
  16. package/skills/rango/SKILL.md +1 -2
  17. package/skills/response-routes/SKILL.md +3 -3
  18. package/skills/route/SKILL.md +44 -3
  19. package/skills/router-setup/SKILL.md +80 -3
  20. package/skills/theme/SKILL.md +5 -4
  21. package/skills/typesafety/SKILL.md +59 -16
  22. package/skills/use-cache/SKILL.md +16 -2
  23. package/src/__internal.ts +1 -1
  24. package/src/bin/rango.ts +56 -19
  25. package/src/browser/action-coordinator.ts +97 -0
  26. package/src/browser/event-controller.ts +29 -48
  27. package/src/browser/history-state.ts +80 -0
  28. package/src/browser/intercept-utils.ts +1 -1
  29. package/src/browser/link-interceptor.ts +19 -3
  30. package/src/browser/merge-segment-loaders.ts +9 -2
  31. package/src/browser/navigation-bridge.ts +66 -443
  32. package/src/browser/navigation-client.ts +34 -62
  33. package/src/browser/navigation-store.ts +4 -33
  34. package/src/browser/navigation-transaction.ts +295 -0
  35. package/src/browser/partial-update.ts +103 -151
  36. package/src/browser/prefetch/cache.ts +67 -0
  37. package/src/browser/prefetch/fetch.ts +137 -0
  38. package/src/browser/prefetch/observer.ts +65 -0
  39. package/src/browser/prefetch/policy.ts +42 -0
  40. package/src/browser/prefetch/queue.ts +88 -0
  41. package/src/browser/rango-state.ts +112 -0
  42. package/src/browser/react/Link.tsx +154 -44
  43. package/src/browser/react/NavigationProvider.tsx +32 -0
  44. package/src/browser/react/context.ts +6 -0
  45. package/src/browser/react/filter-segment-order.ts +11 -0
  46. package/src/browser/react/index.ts +2 -6
  47. package/src/browser/react/location-state-shared.ts +29 -11
  48. package/src/browser/react/location-state.ts +6 -4
  49. package/src/browser/react/nonce-context.ts +23 -0
  50. package/src/browser/react/shallow-equal.ts +27 -0
  51. package/src/browser/react/use-action.ts +23 -45
  52. package/src/browser/react/use-client-cache.ts +5 -3
  53. package/src/browser/react/use-handle.ts +21 -64
  54. package/src/browser/react/use-navigation.ts +7 -32
  55. package/src/browser/react/use-params.ts +5 -34
  56. package/src/browser/react/use-pathname.ts +2 -3
  57. package/src/browser/react/use-router.ts +3 -6
  58. package/src/browser/react/use-search-params.ts +2 -1
  59. package/src/browser/react/use-segments.ts +75 -114
  60. package/src/browser/response-adapter.ts +73 -0
  61. package/src/browser/rsc-router.tsx +46 -22
  62. package/src/browser/scroll-restoration.ts +10 -7
  63. package/src/browser/server-action-bridge.ts +458 -405
  64. package/src/browser/types.ts +21 -35
  65. package/src/browser/validate-redirect-origin.ts +29 -0
  66. package/src/build/generate-manifest.ts +38 -13
  67. package/src/build/generate-route-types.ts +4 -0
  68. package/src/build/index.ts +1 -0
  69. package/src/build/route-trie.ts +19 -3
  70. package/src/build/route-types/codegen.ts +13 -4
  71. package/src/build/route-types/include-resolution.ts +13 -0
  72. package/src/build/route-types/per-module-writer.ts +15 -3
  73. package/src/build/route-types/router-processing.ts +170 -18
  74. package/src/build/runtime-discovery.ts +13 -1
  75. package/src/cache/background-task.ts +34 -0
  76. package/src/cache/cache-key-utils.ts +44 -0
  77. package/src/cache/cache-policy.ts +125 -0
  78. package/src/cache/cache-runtime.ts +136 -123
  79. package/src/cache/cache-scope.ts +76 -83
  80. package/src/cache/cf/cf-cache-store.ts +12 -7
  81. package/src/cache/document-cache.ts +93 -69
  82. package/src/cache/handle-capture.ts +81 -0
  83. package/src/cache/index.ts +0 -15
  84. package/src/cache/memory-segment-store.ts +43 -69
  85. package/src/cache/profile-registry.ts +43 -8
  86. package/src/cache/read-through-swr.ts +134 -0
  87. package/src/cache/segment-codec.ts +140 -117
  88. package/src/cache/taint.ts +30 -3
  89. package/src/cache/types.ts +1 -115
  90. package/src/client.rsc.tsx +0 -1
  91. package/src/client.tsx +53 -76
  92. package/src/errors.ts +6 -1
  93. package/src/handle.ts +1 -1
  94. package/src/handles/MetaTags.tsx +5 -2
  95. package/src/host/cookie-handler.ts +8 -3
  96. package/src/host/index.ts +0 -3
  97. package/src/host/router.ts +14 -1
  98. package/src/href-client.ts +3 -1
  99. package/src/index.rsc.ts +53 -10
  100. package/src/index.ts +73 -43
  101. package/src/loader.rsc.ts +12 -4
  102. package/src/loader.ts +8 -0
  103. package/src/prerender/store.ts +60 -18
  104. package/src/prerender.ts +76 -18
  105. package/src/reverse.ts +11 -7
  106. package/src/root-error-boundary.tsx +30 -26
  107. package/src/route-definition/dsl-helpers.ts +9 -6
  108. package/src/route-definition/index.ts +0 -3
  109. package/src/route-definition/redirect.ts +15 -3
  110. package/src/route-map-builder.ts +38 -2
  111. package/src/route-name.ts +53 -0
  112. package/src/route-types.ts +7 -0
  113. package/src/router/content-negotiation.ts +1 -1
  114. package/src/router/debug-manifest.ts +16 -3
  115. package/src/router/handler-context.ts +96 -17
  116. package/src/router/intercept-resolution.ts +6 -4
  117. package/src/router/lazy-includes.ts +4 -0
  118. package/src/router/loader-resolution.ts +6 -11
  119. package/src/router/logging.ts +100 -3
  120. package/src/router/manifest.ts +32 -3
  121. package/src/router/match-api.ts +62 -54
  122. package/src/router/match-context.ts +3 -0
  123. package/src/router/match-handlers.ts +185 -11
  124. package/src/router/match-middleware/background-revalidation.ts +65 -85
  125. package/src/router/match-middleware/cache-lookup.ts +78 -10
  126. package/src/router/match-middleware/cache-store.ts +2 -0
  127. package/src/router/match-pipelines.ts +8 -43
  128. package/src/router/match-result.ts +0 -9
  129. package/src/router/metrics.ts +233 -13
  130. package/src/router/middleware-types.ts +34 -39
  131. package/src/router/middleware.ts +290 -130
  132. package/src/router/pattern-matching.ts +61 -10
  133. package/src/router/prerender-match.ts +36 -6
  134. package/src/router/preview-match.ts +7 -1
  135. package/src/router/revalidation.ts +61 -2
  136. package/src/router/router-context.ts +15 -0
  137. package/src/router/router-interfaces.ts +158 -40
  138. package/src/router/router-options.ts +223 -1
  139. package/src/router/router-registry.ts +5 -2
  140. package/src/router/segment-resolution/fresh.ts +165 -242
  141. package/src/router/segment-resolution/helpers.ts +263 -0
  142. package/src/router/segment-resolution/loader-cache.ts +102 -98
  143. package/src/router/segment-resolution/revalidation.ts +394 -272
  144. package/src/router/segment-resolution/static-store.ts +2 -2
  145. package/src/router/segment-resolution.ts +1 -3
  146. package/src/router/segment-wrappers.ts +3 -0
  147. package/src/router/telemetry-otel.ts +299 -0
  148. package/src/router/telemetry.ts +300 -0
  149. package/src/router/timeout.ts +148 -0
  150. package/src/router/trie-matching.ts +20 -2
  151. package/src/router/types.ts +7 -1
  152. package/src/router.ts +203 -18
  153. package/src/rsc/handler-context.ts +13 -2
  154. package/src/rsc/handler.ts +489 -438
  155. package/src/rsc/helpers.ts +125 -5
  156. package/src/rsc/index.ts +0 -20
  157. package/src/rsc/loader-fetch.ts +84 -42
  158. package/src/rsc/manifest-init.ts +3 -2
  159. package/src/rsc/origin-guard.ts +141 -0
  160. package/src/rsc/progressive-enhancement.ts +245 -19
  161. package/src/rsc/response-route-handler.ts +347 -0
  162. package/src/rsc/rsc-rendering.ts +47 -43
  163. package/src/rsc/runtime-warnings.ts +42 -0
  164. package/src/rsc/server-action.ts +166 -66
  165. package/src/rsc/ssr-setup.ts +128 -0
  166. package/src/rsc/types.ts +20 -2
  167. package/src/search-params.ts +38 -23
  168. package/src/server/context.ts +61 -7
  169. package/src/server/cookie-store.ts +190 -0
  170. package/src/server/fetchable-loader-store.ts +11 -6
  171. package/src/server/handle-store.ts +84 -12
  172. package/src/server/loader-registry.ts +11 -46
  173. package/src/server/request-context.ts +275 -49
  174. package/src/server.ts +6 -0
  175. package/src/ssr/index.tsx +67 -28
  176. package/src/static-handler.ts +7 -0
  177. package/src/theme/ThemeProvider.tsx +6 -1
  178. package/src/theme/index.ts +4 -18
  179. package/src/theme/theme-context.ts +1 -28
  180. package/src/theme/theme-script.ts +2 -1
  181. package/src/types/cache-types.ts +6 -1
  182. package/src/types/error-types.ts +3 -0
  183. package/src/types/global-namespace.ts +22 -0
  184. package/src/types/handler-context.ts +103 -16
  185. package/src/types/index.ts +1 -1
  186. package/src/types/loader-types.ts +9 -6
  187. package/src/types/route-config.ts +17 -26
  188. package/src/types/route-entry.ts +28 -0
  189. package/src/types/segments.ts +0 -5
  190. package/src/urls/include-helper.ts +49 -8
  191. package/src/urls/index.ts +1 -0
  192. package/src/urls/path-helper-types.ts +30 -12
  193. package/src/urls/path-helper.ts +17 -2
  194. package/src/urls/pattern-types.ts +21 -1
  195. package/src/urls/response-types.ts +29 -7
  196. package/src/urls/type-extraction.ts +23 -15
  197. package/src/use-loader.tsx +27 -9
  198. package/src/vite/discovery/bundle-postprocess.ts +32 -52
  199. package/src/vite/discovery/discover-routers.ts +52 -26
  200. package/src/vite/discovery/prerender-collection.ts +58 -41
  201. package/src/vite/discovery/route-types-writer.ts +7 -7
  202. package/src/vite/discovery/state.ts +7 -7
  203. package/src/vite/discovery/virtual-module-codegen.ts +5 -2
  204. package/src/vite/index.ts +10 -51
  205. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  206. package/src/vite/plugins/client-ref-hashing.ts +3 -3
  207. package/src/vite/plugins/expose-internal-ids.ts +4 -3
  208. package/src/vite/plugins/refresh-cmd.ts +65 -0
  209. package/src/vite/plugins/use-cache-transform.ts +91 -3
  210. package/src/vite/plugins/version-plugin.ts +188 -18
  211. package/src/vite/rango.ts +61 -36
  212. package/src/vite/router-discovery.ts +173 -100
  213. package/src/vite/utils/prerender-utils.ts +81 -0
  214. package/src/vite/utils/shared-utils.ts +19 -9
  215. package/skills/testing/SKILL.md +0 -226
  216. package/src/browser/lru-cache.ts +0 -61
  217. package/src/browser/react/prefetch.ts +0 -27
  218. package/src/browser/request-controller.ts +0 -164
  219. package/src/cache/memory-store.ts +0 -253
  220. package/src/href-context.ts +0 -33
  221. package/src/route-definition/route-function.ts +0 -119
  222. package/src/router.gen.ts +0 -6
  223. package/src/static-handler.gen.ts +0 -5
  224. package/src/urls.gen.ts +0 -8
  225. /package/{CLAUDE.md → AGENTS.md} +0 -0
@@ -46,7 +46,7 @@ export const sharedEsbuildOptions: {
46
46
  */
47
47
  export function createVirtualEntriesPlugin(
48
48
  entries: { client: string; ssr: string; rsc?: string },
49
- routerPath?: string,
49
+ routerPathRef?: { path?: string },
50
50
  ): Plugin {
51
51
  // Build virtual modules map based on which entries use virtual IDs
52
52
  const virtualModules: Record<string, string> = {};
@@ -57,12 +57,13 @@ export function createVirtualEntriesPlugin(
57
57
  if (entries.ssr === VIRTUAL_IDS.ssr) {
58
58
  virtualModules[VIRTUAL_IDS.ssr] = VIRTUAL_ENTRY_SSR;
59
59
  }
60
- if (entries.rsc === VIRTUAL_IDS.rsc && routerPath) {
61
- // Convert relative path to absolute for virtual module imports
62
- const absoluteRouterPath = routerPath.startsWith(".")
63
- ? "/" + routerPath.slice(2) // ./src/router.tsx -> /src/router.tsx
64
- : routerPath;
65
- virtualModules[VIRTUAL_IDS.rsc] = getVirtualEntryRSC(absoluteRouterPath);
60
+
61
+ // RSC entry is resolved lazily in load() because routerPath may be
62
+ // set after plugin creation (e.g. by the auto-discover config() hook).
63
+ // Track all known virtual IDs for resolveId (content is separate).
64
+ const knownIds = new Set(Object.keys(virtualModules));
65
+ if (entries.rsc === VIRTUAL_IDS.rsc) {
66
+ knownIds.add(VIRTUAL_IDS.rsc);
66
67
  }
67
68
 
68
69
  return {
@@ -70,11 +71,11 @@ export function createVirtualEntriesPlugin(
70
71
  enforce: "pre",
71
72
 
72
73
  resolveId(id) {
73
- if (id in virtualModules) {
74
+ if (knownIds.has(id)) {
74
75
  return "\0" + id;
75
76
  }
76
77
  // Handle if the id already has the null prefix (RSC plugin wrapper imports)
77
- if (id.startsWith("\0") && id.slice(1) in virtualModules) {
78
+ if (id.startsWith("\0") && knownIds.has(id.slice(1))) {
78
79
  return id;
79
80
  }
80
81
  return null;
@@ -86,6 +87,15 @@ export function createVirtualEntriesPlugin(
86
87
  if (virtualId in virtualModules) {
87
88
  return virtualModules[virtualId];
88
89
  }
90
+ // Lazy RSC entry: routerPath may have been set by a config() hook
91
+ if (virtualId === VIRTUAL_IDS.rsc && routerPathRef?.path) {
92
+ const raw = routerPathRef.path.startsWith(".")
93
+ ? "/" + routerPathRef.path.slice(2) // ./src/router.tsx -> /src/router.tsx
94
+ : routerPathRef.path;
95
+ // Normalize backslashes for Windows (path.join/slice preserve native separators)
96
+ const absoluteRouterPath = raw.replaceAll("\\", "/");
97
+ return getVirtualEntryRSC(absoluteRouterPath);
98
+ }
89
99
  }
90
100
  return null;
91
101
  },
@@ -1,226 +0,0 @@
1
- ---
2
- name: testing
3
- description: Unit test route trees with buildRouteTree()
4
- argument-hint:
5
- ---
6
-
7
- # Route Tree Unit Testing
8
-
9
- Unit test route definitions by inspecting the route tree, segment IDs, middleware, intercepts, loaders, and pattern matching without running a dev server.
10
-
11
- ## Setup
12
-
13
- The `buildRouteTree` helper lives in `src/__tests__/helpers/route-tree.ts` (not shipped with npm). Import it in your test files:
14
-
15
- ```typescript
16
- import { buildRouteTree } from "./helpers/route-tree.js";
17
- ```
18
-
19
- ## buildRouteTree(urlPatterns)
20
-
21
- Takes a `urls()` result and returns a `RouteTree` with inspection methods:
22
-
23
- ```typescript
24
- import { urls } from "@rangojs/router";
25
- import { buildRouteTree } from "./helpers/route-tree.js";
26
-
27
- const tree = buildRouteTree(
28
- urls(({ path, layout, middleware, loader, intercept, when }) => [
29
- layout(RootLayout, () => [
30
- middleware(authMiddleware),
31
- path("/", HomePage, { name: "home" }),
32
- path("/blog/:slug", BlogPost, { name: "blog.post" }, () => [
33
- loader(PostLoader),
34
- ]),
35
- ]),
36
- ]),
37
- );
38
- ```
39
-
40
- ## RouteTree API
41
-
42
- ### Route Patterns
43
-
44
- ```typescript
45
- tree.routes(); // { home: "/", "blog.post": "/blog/:slug" }
46
- tree.routeNames(); // ["home", "blog.post"]
47
- ```
48
-
49
- ### URL Matching
50
-
51
- ```typescript
52
- const m = tree.match("/blog/hello");
53
- m.routeKey; // "blog.post"
54
- m.params; // { slug: "hello" }
55
-
56
- tree.match("/nonexistent"); // null
57
- ```
58
-
59
- ### Segment IDs
60
-
61
- ```typescript
62
- tree.segmentId("home"); // "M0L0L0R0"
63
- tree.segmentIds(); // { home: "M0L0L0R0", "blog.post": "M0L0L0R1" }
64
- tree.segmentPath("blog.post");
65
- // [
66
- // { id: "M0L0", type: "layout" }, // synthetic root
67
- // { id: "M0L0L0", type: "layout" }, // RootLayout
68
- // { id: "M0L0L0R1", type: "route", pattern: "/blog/:slug" },
69
- // ]
70
- ```
71
-
72
- ### Entry Access
73
-
74
- ```typescript
75
- tree.entry("blog.post"); // EntryData
76
- tree.entry("blog.post")!.parent!.type; // "layout"
77
- tree.entryByPattern("/blog/:slug"); // EntryData (lookup by URL pattern)
78
- ```
79
-
80
- ### Middleware
81
-
82
- ```typescript
83
- tree.hasMiddleware("home"); // true (inherited from layout)
84
- tree.middleware("home"); // [authMiddleware] (direct only)
85
- tree.middlewareChain("home");
86
- // [{ segmentId: "M0L0L0", count: 1 }] // all middleware root-to-route
87
- ```
88
-
89
- ### Loaders
90
-
91
- ```typescript
92
- tree.hasLoaders("blog.post"); // true
93
- tree.loaders("blog.post"); // [LoaderEntry { loader, revalidate, cache? }]
94
- ```
95
-
96
- ### Intercepts
97
-
98
- ```typescript
99
- tree.intercepts("home");
100
- // [{ slotName: "@modal", routeName: "card", hasWhen: true, whenCount: 1, hasLoader: false, hasMiddleware: false }]
101
- tree.interceptEntries("home"); // raw InterceptEntry[]
102
- ```
103
-
104
- ### Parallel Slots
105
-
106
- ```typescript
107
- tree.parallelSlots("home"); // EntryData[] of type="parallel"
108
- tree.parallelSlotNames("home"); // ["@sidebar", "@main"]
109
- ```
110
-
111
- ### Boundaries
112
-
113
- ```typescript
114
- tree.hasErrorBoundary("home"); // boolean
115
- tree.hasNotFoundBoundary("home"); // boolean
116
- ```
117
-
118
- ### Cache & Loading
119
-
120
- ```typescript
121
- tree.hasCache("home"); // boolean
122
- tree.hasLoading("home"); // boolean
123
- ```
124
-
125
- ### Debug
126
-
127
- ```typescript
128
- console.log(tree.debug());
129
- // Route Tree:
130
- // home: / [M0L0L0R0] (M0L0 > M0L0L0 > M0L0L0R0) {mw:1}
131
- // blog.post: /blog/:slug [M0L0L0R1] (M0L0 > M0L0L0 > M0L0L0R1) {mw:1, ld:1}
132
- ```
133
-
134
- ## Segment ID Format
135
-
136
- | Prefix | Meaning |
137
- | ------ | ----------------------------- |
138
- | `M0` | Mount index (router instance) |
139
- | `L` | Layout |
140
- | `R` | Route |
141
- | `P` | Parallel slot |
142
- | `D` | Loader (data) |
143
- | `C` | Cache boundary |
144
-
145
- Example: `M0L0L0R1` = mount 0, synthetic root layout, user layout, second route.
146
-
147
- ## Examples
148
-
149
- ### include() with prefix
150
-
151
- ```typescript
152
- const blogPatterns = urls(({ path }) => [
153
- path("/", BlogIndex, { name: "index" }),
154
- path("/:slug", BlogPost, { name: "post" }),
155
- ]);
156
-
157
- const tree = buildRouteTree(
158
- urls(({ path, include }) => [
159
- path("/", HomePage, { name: "home" }),
160
- include("/blog", blogPatterns, { name: "blog" }),
161
- ]),
162
- );
163
-
164
- expect(tree.routes()).toEqual({
165
- home: "/",
166
- "blog.index": "/blog",
167
- "blog.post": "/blog/:slug",
168
- });
169
- ```
170
-
171
- ### Middleware chain
172
-
173
- ```typescript
174
- const authMw = async (ctx, next) => next();
175
- const logMw = async (ctx, next) => next();
176
-
177
- const tree = buildRouteTree(
178
- urls(({ path, layout, middleware }) => [
179
- layout(RootLayout, () => [
180
- middleware(logMw),
181
- layout(AuthLayout, () => [
182
- middleware(authMw),
183
- path("/dashboard", Dashboard, { name: "dashboard" }),
184
- ]),
185
- ]),
186
- ]),
187
- );
188
-
189
- expect(tree.middlewareChain("dashboard")).toEqual([
190
- { segmentId: "M0L0L0", count: 1 }, // logMw on RootLayout
191
- { segmentId: "M0L0L0L0", count: 1 }, // authMw on AuthLayout
192
- ]);
193
- ```
194
-
195
- ### Intercepts with when()
196
-
197
- ```typescript
198
- const tree = buildRouteTree(
199
- urls(({ path, layout, intercept, when }) => [
200
- layout(ShopLayout, () => [
201
- path("/products", ProductList, { name: "products" }),
202
- path("/products/:id", ProductDetail, { name: "product.detail" }),
203
- intercept("@modal", "product.detail", ProductModal, () => [
204
- when((ctx) => ctx.from.pathname.startsWith("/products")),
205
- ]),
206
- ]),
207
- ]),
208
- );
209
-
210
- const intercepts = tree.intercepts("products");
211
- // Note: intercepts are on the parent where intercept() is called
212
- ```
213
-
214
- ### Constrained parameters
215
-
216
- ```typescript
217
- const tree = buildRouteTree(
218
- urls(({ path }) => [
219
- path("/:locale(en|fr)?/about", AboutPage, { name: "about" }),
220
- ]),
221
- );
222
-
223
- expect(tree.match("/about")).not.toBeNull();
224
- expect(tree.match("/fr/about")!.params).toEqual({ locale: "fr" });
225
- expect(tree.match("/de/about")).toBeNull();
226
- ```
@@ -1,61 +0,0 @@
1
- /**
2
- * Simple LRU (Least Recently Used) cache implementation
3
- * Used for caching navigation segments to enable instant back/forward
4
- */
5
- export class LRUCache<K, V> {
6
- private cache = new Map<K, V>();
7
- private maxSize: number;
8
-
9
- constructor(maxSize: number) {
10
- this.maxSize = maxSize;
11
- }
12
-
13
- get(key: K): V | undefined {
14
- if (!this.cache.has(key)) {
15
- return undefined;
16
- }
17
-
18
- // Move to end (most recently used)
19
- const value = this.cache.get(key)!;
20
- this.cache.delete(key);
21
- this.cache.set(key, value);
22
- return value;
23
- }
24
-
25
- set(key: K, value: V): void {
26
- // If key exists, delete it first to update position
27
- if (this.cache.has(key)) {
28
- this.cache.delete(key);
29
- }
30
-
31
- this.cache.set(key, value);
32
-
33
- // Evict oldest entries if over capacity
34
- while (this.cache.size > this.maxSize) {
35
- const oldestKey = this.cache.keys().next().value;
36
- if (oldestKey !== undefined) {
37
- this.cache.delete(oldestKey);
38
- }
39
- }
40
- }
41
-
42
- has(key: K): boolean {
43
- return this.cache.has(key);
44
- }
45
-
46
- delete(key: K): boolean {
47
- return this.cache.delete(key);
48
- }
49
-
50
- clear(): void {
51
- this.cache.clear();
52
- }
53
-
54
- keys(): IterableIterator<K> {
55
- return this.cache.keys();
56
- }
57
-
58
- get size(): number {
59
- return this.cache.size;
60
- }
61
- }
@@ -1,27 +0,0 @@
1
- "use client";
2
-
3
- // Track prefetched URLs to avoid duplicate <link> elements
4
- const prefetchedUrls = new Set<string>();
5
-
6
- /**
7
- * Inject a <link rel="prefetch"> element into the document head
8
- * for the given URL with RSC partial request parameters.
9
- */
10
- export function prefetchUrl(url: string, segmentIds: string[]): void {
11
- if (prefetchedUrls.has(url)) return;
12
- prefetchedUrls.add(url);
13
-
14
- // Build RSC partial URL with segment IDs
15
- const targetUrl = new URL(url, window.location.origin);
16
- targetUrl.searchParams.set("_rsc_partial", "true");
17
- if (segmentIds.length > 0) {
18
- targetUrl.searchParams.set("_rsc_segments", segmentIds.join(","));
19
- }
20
-
21
- // Inject <link rel="prefetch"> into head
22
- const link = document.createElement("link");
23
- link.rel = "prefetch";
24
- link.href = targetUrl.toString();
25
- link.as = "fetch";
26
- document.head.appendChild(link);
27
- }
@@ -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
- }