@rangojs/router 0.0.0-experimental.32 → 0.0.0-experimental.3232cd17

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 (376) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +198 -44
  3. package/dist/bin/rango.js +287 -105
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3248 -1117
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +73 -21
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +107 -1
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +245 -21
  12. package/skills/caching/SKILL.md +302 -6
  13. package/skills/composability/SKILL.md +27 -2
  14. package/skills/css/SKILL.md +76 -0
  15. package/skills/document-cache/SKILL.md +78 -55
  16. package/skills/handler-use/SKILL.md +364 -0
  17. package/skills/hooks/SKILL.md +270 -30
  18. package/skills/host-router/SKILL.md +82 -22
  19. package/skills/i18n/SKILL.md +276 -0
  20. package/skills/intercept/SKILL.md +49 -5
  21. package/skills/layout/SKILL.md +35 -9
  22. package/skills/links/SKILL.md +249 -17
  23. package/skills/loader/SKILL.md +294 -30
  24. package/skills/middleware/SKILL.md +52 -13
  25. package/skills/migrate-nextjs/SKILL.md +584 -0
  26. package/skills/migrate-react-router/SKILL.md +769 -0
  27. package/skills/mime-routes/SKILL.md +27 -0
  28. package/skills/observability/SKILL.md +137 -0
  29. package/skills/parallel/SKILL.md +203 -7
  30. package/skills/prerender/SKILL.md +123 -100
  31. package/skills/rango/SKILL.md +250 -22
  32. package/skills/react-compiler/SKILL.md +168 -0
  33. package/skills/response-routes/SKILL.md +122 -47
  34. package/skills/route/SKILL.md +97 -5
  35. package/skills/router-setup/SKILL.md +90 -5
  36. package/skills/server-actions/SKILL.md +775 -0
  37. package/skills/streams-and-websockets/SKILL.md +283 -0
  38. package/skills/tailwind/SKILL.md +27 -3
  39. package/skills/testing/SKILL.md +129 -0
  40. package/skills/testing/bindings.md +89 -0
  41. package/skills/testing/cache-prerender.md +124 -0
  42. package/skills/testing/client-components.md +122 -0
  43. package/skills/testing/e2e-parity.md +125 -0
  44. package/skills/testing/flight.md +92 -0
  45. package/skills/testing/handles.md +129 -0
  46. package/skills/testing/loader.md +128 -0
  47. package/skills/testing/middleware.md +99 -0
  48. package/skills/testing/render-handler.md +121 -0
  49. package/skills/testing/response-routes.md +95 -0
  50. package/skills/testing/reverse-and-types.md +84 -0
  51. package/skills/testing/server-actions.md +107 -0
  52. package/skills/testing/server-tree.md +128 -0
  53. package/skills/testing/setup.md +120 -0
  54. package/skills/typesafety/SKILL.md +329 -27
  55. package/skills/use-cache/SKILL.md +36 -5
  56. package/skills/view-transitions/SKILL.md +294 -0
  57. package/src/__augment-tests__/augment.ts +81 -0
  58. package/src/__augment-tests__/augmented.check.ts +116 -0
  59. package/src/__internal.ts +67 -40
  60. package/src/browser/action-coordinator.ts +53 -36
  61. package/src/browser/action-fence.ts +47 -0
  62. package/src/browser/app-shell.ts +39 -0
  63. package/src/browser/app-version.ts +14 -0
  64. package/src/browser/cookie-name.ts +140 -0
  65. package/src/browser/event-controller.ts +86 -147
  66. package/src/browser/history-state.ts +21 -0
  67. package/src/browser/index.ts +3 -3
  68. package/src/browser/invalidate-client-cache.ts +52 -0
  69. package/src/browser/link-interceptor.ts +4 -0
  70. package/src/browser/navigation-bridge.ts +148 -19
  71. package/src/browser/navigation-client.ts +187 -67
  72. package/src/browser/navigation-store-handle.ts +38 -0
  73. package/src/browser/navigation-store.ts +76 -67
  74. package/src/browser/navigation-transaction.ts +18 -66
  75. package/src/browser/partial-update.ts +123 -94
  76. package/src/browser/prefetch/cache.ts +214 -36
  77. package/src/browser/prefetch/fetch.ts +260 -38
  78. package/src/browser/prefetch/policy.ts +6 -0
  79. package/src/browser/prefetch/queue.ts +126 -20
  80. package/src/browser/prefetch/resource-ready.ts +77 -0
  81. package/src/browser/rango-state.ts +158 -76
  82. package/src/browser/react/Link.tsx +93 -11
  83. package/src/browser/react/NavigationProvider.tsx +115 -34
  84. package/src/browser/react/ScrollRestoration.tsx +10 -6
  85. package/src/browser/react/context.ts +7 -2
  86. package/src/browser/react/filter-segment-order.ts +49 -7
  87. package/src/browser/react/index.ts +0 -48
  88. package/src/browser/react/location-state-shared.ts +166 -8
  89. package/src/browser/react/location-state.ts +39 -14
  90. package/src/browser/react/use-action.ts +6 -15
  91. package/src/browser/react/use-handle.ts +23 -69
  92. package/src/browser/react/use-link-status.ts +0 -4
  93. package/src/browser/react/use-navigation.ts +22 -5
  94. package/src/browser/react/use-params.ts +20 -10
  95. package/src/browser/react/use-reverse.ts +106 -0
  96. package/src/browser/react/use-router.ts +46 -11
  97. package/src/browser/react/use-search-params.ts +0 -5
  98. package/src/browser/react/use-segments.ts +11 -21
  99. package/src/browser/response-adapter.ts +52 -1
  100. package/src/browser/rsc-router.tsx +215 -76
  101. package/src/browser/scroll-restoration.ts +46 -39
  102. package/src/browser/segment-reconciler.ts +36 -9
  103. package/src/browser/segment-structure-assert.ts +2 -2
  104. package/src/browser/server-action-bridge.ts +176 -50
  105. package/src/browser/types.ts +95 -11
  106. package/src/browser/validate-redirect-origin.ts +43 -16
  107. package/src/build/collect-fallback-refs.ts +107 -0
  108. package/src/build/generate-manifest.ts +65 -40
  109. package/src/build/generate-route-types.ts +5 -0
  110. package/src/build/index.ts +8 -2
  111. package/src/build/prefix-tree-utils.ts +123 -0
  112. package/src/build/route-trie.ts +137 -32
  113. package/src/build/route-types/codegen.ts +4 -4
  114. package/src/build/route-types/include-resolution.ts +9 -2
  115. package/src/build/route-types/param-extraction.ts +6 -3
  116. package/src/build/route-types/per-module-writer.ts +7 -4
  117. package/src/build/route-types/router-processing.ts +278 -96
  118. package/src/build/route-types/scan-filter.ts +9 -2
  119. package/src/build/route-types/source-scan.ts +118 -0
  120. package/src/build/runtime-discovery.ts +9 -20
  121. package/src/cache/cache-error.ts +104 -0
  122. package/src/cache/cache-policy.ts +68 -28
  123. package/src/cache/cache-runtime.ts +149 -43
  124. package/src/cache/cache-scope.ts +148 -81
  125. package/src/cache/cache-tag.ts +98 -0
  126. package/src/cache/cf/cf-cache-store.ts +2550 -93
  127. package/src/cache/cf/index.ts +11 -17
  128. package/src/cache/document-cache.ts +78 -27
  129. package/src/cache/handle-snapshot.ts +63 -0
  130. package/src/cache/index.ts +23 -20
  131. package/src/cache/memory-segment-store.ts +136 -37
  132. package/src/cache/profile-registry.ts +6 -30
  133. package/src/cache/read-through-swr.ts +41 -11
  134. package/src/cache/segment-codec.ts +0 -16
  135. package/src/cache/tag-invalidation.ts +230 -0
  136. package/src/cache/taint.ts +55 -0
  137. package/src/cache/types.ts +33 -100
  138. package/src/cache/vercel/index.ts +11 -0
  139. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  140. package/src/client.rsc.tsx +6 -21
  141. package/src/client.tsx +108 -290
  142. package/src/component-utils.ts +19 -0
  143. package/src/context-var.ts +84 -2
  144. package/src/debug.ts +2 -2
  145. package/src/decode-loader-results.ts +36 -0
  146. package/src/defer.ts +196 -0
  147. package/src/deps/ssr.ts +0 -1
  148. package/src/errors.ts +30 -4
  149. package/src/handle.ts +70 -22
  150. package/src/handles/MetaTags.tsx +0 -14
  151. package/src/handles/breadcrumbs.ts +16 -5
  152. package/src/handles/meta.ts +0 -39
  153. package/src/host/cookie-handler.ts +0 -36
  154. package/src/host/errors.ts +0 -24
  155. package/src/host/index.ts +8 -2
  156. package/src/host/pattern-matcher.ts +7 -50
  157. package/src/host/router.ts +107 -99
  158. package/src/host/testing.ts +40 -27
  159. package/src/host/types.ts +37 -4
  160. package/src/host/utils.ts +1 -1
  161. package/src/href-client.ts +137 -22
  162. package/src/index.rsc.ts +52 -26
  163. package/src/index.ts +100 -38
  164. package/src/internal-debug.ts +2 -4
  165. package/src/loader-store.ts +500 -0
  166. package/src/loader.rsc.ts +20 -13
  167. package/src/loader.ts +12 -11
  168. package/src/missing-id-error.ts +68 -0
  169. package/src/network-error-thrower.tsx +1 -6
  170. package/src/outlet-context.ts +1 -1
  171. package/src/outlet-provider.tsx +1 -5
  172. package/src/prerender/param-hash.ts +10 -11
  173. package/src/prerender/store.ts +37 -41
  174. package/src/prerender.ts +198 -82
  175. package/src/redirect-origin.ts +100 -0
  176. package/src/response-utils.ts +37 -0
  177. package/src/reverse.ts +65 -15
  178. package/src/root-error-boundary.tsx +1 -19
  179. package/src/route-content-wrapper.tsx +7 -72
  180. package/src/route-definition/dsl-helpers.ts +437 -274
  181. package/src/route-definition/helper-factories.ts +29 -139
  182. package/src/route-definition/helpers-types.ts +113 -37
  183. package/src/route-definition/index.ts +3 -0
  184. package/src/route-definition/redirect.ts +52 -10
  185. package/src/route-definition/resolve-handler-use.ts +161 -0
  186. package/src/route-definition/use-item-types.ts +32 -0
  187. package/src/route-map-builder.ts +7 -17
  188. package/src/route-types.ts +37 -41
  189. package/src/router/basename.ts +14 -0
  190. package/src/router/content-negotiation.ts +108 -9
  191. package/src/router/error-handling.ts +13 -17
  192. package/src/router/find-match.ts +45 -22
  193. package/src/router/handler-context.ts +83 -41
  194. package/src/router/intercept-resolution.ts +25 -23
  195. package/src/router/lazy-includes.ts +19 -53
  196. package/src/router/loader-resolution.ts +213 -30
  197. package/src/router/logging.ts +5 -8
  198. package/src/router/manifest.ts +49 -45
  199. package/src/router/match-api.ts +120 -204
  200. package/src/router/match-context.ts +0 -22
  201. package/src/router/match-handlers.ts +58 -58
  202. package/src/router/match-middleware/background-revalidation.ts +27 -6
  203. package/src/router/match-middleware/cache-lookup.ts +205 -249
  204. package/src/router/match-middleware/cache-store.ts +45 -32
  205. package/src/router/match-middleware/intercept-resolution.ts +8 -28
  206. package/src/router/match-middleware/segment-resolution.ts +52 -18
  207. package/src/router/match-pipelines.ts +1 -42
  208. package/src/router/match-result.ts +104 -40
  209. package/src/router/metrics.ts +5 -34
  210. package/src/router/middleware-types.ts +13 -142
  211. package/src/router/middleware.ts +173 -143
  212. package/src/router/navigation-snapshot.ts +131 -0
  213. package/src/router/params-util.ts +23 -0
  214. package/src/router/pattern-matching.ts +109 -63
  215. package/src/router/prerender-match.ts +190 -54
  216. package/src/router/preview-match.ts +32 -102
  217. package/src/router/request-classification.ts +276 -0
  218. package/src/router/revalidation.ts +63 -55
  219. package/src/router/route-snapshot.ts +244 -0
  220. package/src/router/router-context.ts +6 -28
  221. package/src/router/router-interfaces.ts +100 -35
  222. package/src/router/router-options.ts +91 -11
  223. package/src/router/router-registry.ts +2 -5
  224. package/src/router/segment-resolution/fresh.ts +242 -75
  225. package/src/router/segment-resolution/helpers.ts +63 -24
  226. package/src/router/segment-resolution/loader-cache.ts +41 -37
  227. package/src/router/segment-resolution/revalidation.ts +456 -372
  228. package/src/router/segment-resolution/static-store.ts +19 -5
  229. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  230. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  231. package/src/router/segment-resolution.ts +4 -1
  232. package/src/router/segment-wrappers.ts +2 -3
  233. package/src/router/state-cookie-name.ts +33 -0
  234. package/src/router/substitute-pattern-params.ts +56 -0
  235. package/src/router/telemetry-otel.ts +0 -20
  236. package/src/router/telemetry.ts +96 -19
  237. package/src/router/timeout.ts +0 -20
  238. package/src/router/trie-matching.ts +91 -46
  239. package/src/router/types.ts +10 -63
  240. package/src/router/url-params.ts +44 -0
  241. package/src/router.ts +134 -43
  242. package/src/rsc/handler-context.ts +3 -2
  243. package/src/rsc/handler.ts +492 -383
  244. package/src/rsc/helpers.ts +162 -46
  245. package/src/rsc/index.ts +1 -1
  246. package/src/rsc/json-route-result.ts +38 -0
  247. package/src/rsc/loader-fetch.ts +23 -3
  248. package/src/rsc/manifest-init.ts +33 -42
  249. package/src/rsc/origin-guard.ts +39 -25
  250. package/src/rsc/progressive-enhancement.ts +30 -3
  251. package/src/rsc/redirect-guard.ts +99 -0
  252. package/src/rsc/response-error.ts +79 -12
  253. package/src/rsc/response-route-handler.ts +90 -63
  254. package/src/rsc/rsc-rendering.ts +56 -54
  255. package/src/rsc/runtime-warnings.ts +23 -10
  256. package/src/rsc/server-action.ts +74 -67
  257. package/src/rsc/ssr-setup.ts +18 -2
  258. package/src/rsc/types.ts +25 -6
  259. package/src/runtime-env.ts +18 -0
  260. package/src/search-params.ts +4 -20
  261. package/src/segment-content-promise.ts +67 -0
  262. package/src/segment-loader-promise.ts +134 -0
  263. package/src/segment-system.tsx +272 -129
  264. package/src/serialize.ts +243 -0
  265. package/src/server/context.ts +309 -61
  266. package/src/server/cookie-store.ts +80 -5
  267. package/src/server/handle-store.ts +26 -24
  268. package/src/server/loader-registry.ts +10 -28
  269. package/src/server/request-context.ts +338 -126
  270. package/src/ssr/index.tsx +23 -15
  271. package/src/static-handler.ts +27 -18
  272. package/src/testing/cache-status.ts +162 -0
  273. package/src/testing/collect-handle.ts +40 -0
  274. package/src/testing/dispatch.ts +618 -0
  275. package/src/testing/dom.entry.ts +22 -0
  276. package/src/testing/e2e/fixture.ts +188 -0
  277. package/src/testing/e2e/index.ts +128 -0
  278. package/src/testing/e2e/matchers.ts +35 -0
  279. package/src/testing/e2e/page-helpers.ts +272 -0
  280. package/src/testing/e2e/parity.ts +387 -0
  281. package/src/testing/e2e/server.ts +195 -0
  282. package/src/testing/flight-matchers.ts +97 -0
  283. package/src/testing/flight-normalize.ts +11 -0
  284. package/src/testing/flight-runtime.d.ts +57 -0
  285. package/src/testing/flight-tree.ts +682 -0
  286. package/src/testing/flight.entry.ts +52 -0
  287. package/src/testing/flight.ts +232 -0
  288. package/src/testing/generated-routes.ts +183 -0
  289. package/src/testing/index.ts +99 -0
  290. package/src/testing/internal/context.ts +348 -0
  291. package/src/testing/internal/flight-client-globals.ts +30 -0
  292. package/src/testing/internal/seed-vars.ts +54 -0
  293. package/src/testing/render-handler.ts +330 -0
  294. package/src/testing/render-route.tsx +566 -0
  295. package/src/testing/run-loader.ts +378 -0
  296. package/src/testing/run-middleware.ts +205 -0
  297. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  298. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  299. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  300. package/src/testing/vitest-stubs/version.ts +5 -0
  301. package/src/testing/vitest.ts +305 -0
  302. package/src/theme/ThemeProvider.tsx +0 -52
  303. package/src/theme/ThemeScript.tsx +0 -6
  304. package/src/theme/constants.ts +0 -12
  305. package/src/theme/index.ts +0 -7
  306. package/src/theme/theme-context.ts +1 -5
  307. package/src/theme/theme-script.ts +0 -14
  308. package/src/theme/use-theme.ts +0 -3
  309. package/src/types/boundaries.ts +0 -35
  310. package/src/types/cache-types.ts +17 -8
  311. package/src/types/error-types.ts +30 -90
  312. package/src/types/global-namespace.ts +54 -41
  313. package/src/types/handler-context.ts +233 -81
  314. package/src/types/index.ts +1 -10
  315. package/src/types/loader-types.ts +44 -15
  316. package/src/types/request-scope.ts +107 -0
  317. package/src/types/route-config.ts +6 -50
  318. package/src/types/route-entry.ts +19 -7
  319. package/src/types/segments.ts +37 -14
  320. package/src/urls/include-helper.ts +33 -70
  321. package/src/urls/index.ts +1 -11
  322. package/src/urls/path-helper-types.ts +58 -11
  323. package/src/urls/path-helper.ts +57 -111
  324. package/src/urls/pattern-types.ts +48 -19
  325. package/src/urls/response-types.ts +25 -22
  326. package/src/urls/type-extraction.ts +58 -139
  327. package/src/urls/urls-function.ts +1 -18
  328. package/src/use-loader.tsx +346 -89
  329. package/src/vite/debug.ts +185 -0
  330. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  331. package/src/vite/discovery/discover-routers.ts +130 -85
  332. package/src/vite/discovery/discovery-errors.ts +194 -0
  333. package/src/vite/discovery/gate-state.ts +171 -0
  334. package/src/vite/discovery/prerender-collection.ts +192 -99
  335. package/src/vite/discovery/route-types-writer.ts +40 -84
  336. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  337. package/src/vite/discovery/state.ts +51 -6
  338. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  339. package/src/vite/index.ts +8 -0
  340. package/src/vite/plugin-types.ts +187 -69
  341. package/src/vite/plugins/cjs-to-esm.ts +8 -18
  342. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  343. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  344. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  345. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  346. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  347. package/src/vite/plugins/expose-action-id.ts +49 -98
  348. package/src/vite/plugins/expose-id-utils.ts +11 -50
  349. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  350. package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
  351. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  352. package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
  353. package/src/vite/plugins/expose-internal-ids.ts +554 -317
  354. package/src/vite/plugins/performance-tracks.ts +89 -0
  355. package/src/vite/plugins/refresh-cmd.ts +89 -27
  356. package/src/vite/plugins/use-cache-transform.ts +73 -83
  357. package/src/vite/plugins/vercel-output.ts +258 -0
  358. package/src/vite/plugins/version-injector.ts +21 -25
  359. package/src/vite/plugins/version-plugin.ts +41 -20
  360. package/src/vite/plugins/virtual-entries.ts +2 -17
  361. package/src/vite/rango.ts +257 -289
  362. package/src/vite/router-discovery.ts +930 -140
  363. package/src/vite/utils/ast-handler-extract.ts +15 -31
  364. package/src/vite/utils/banner.ts +4 -4
  365. package/src/vite/utils/bundle-analysis.ts +10 -15
  366. package/src/vite/utils/client-chunks.ts +184 -0
  367. package/src/vite/utils/forward-user-plugins.ts +171 -0
  368. package/src/vite/utils/manifest-utils.ts +4 -59
  369. package/src/vite/utils/package-resolution.ts +20 -52
  370. package/src/vite/utils/prerender-utils.ts +27 -29
  371. package/src/vite/utils/shared-utils.ts +92 -42
  372. package/src/browser/action-response-classifier.ts +0 -99
  373. package/src/browser/react/use-client-cache.ts +0 -58
  374. package/src/browser/shallow.ts +0 -40
  375. package/src/handles/index.ts +0 -7
  376. package/src/router/middleware-cookies.ts +0 -55
@@ -1,15 +1,8 @@
1
1
  /// <reference types="vite/types/importMeta.d.ts" />
2
- /**
3
- * Middleware Execution
4
- *
5
- * True middleware that wraps the entire RSC handler.
6
- * - `await next()` returns actual Response
7
- * - Can modify response headers
8
- * - Can catch errors from RSC rendering
9
- * - Forgiving API: if middleware doesn't return, original response is used
10
- */
11
2
 
12
3
  import { contextGet, contextSet } from "../context-var.js";
4
+ import { safeDecodeURIComponent } from "./url-params.js";
5
+ import { fireAndForgetWaitUntil } from "../types/request-scope.js";
13
6
  import type {
14
7
  CollectedMiddleware,
15
8
  MiddlewareCollectableEntry,
@@ -19,26 +12,28 @@ import type {
19
12
  ResponseHolder,
20
13
  } from "./middleware-types.js";
21
14
  import { _getRequestContext } from "../server/request-context.js";
15
+ import {
16
+ EXTERNAL_REDIRECT_MARKER,
17
+ isExternalRedirect,
18
+ markExternalRedirect,
19
+ } from "../redirect-origin.js";
22
20
  import { isAutoGeneratedRouteName } from "../route-name.js";
23
21
  import { appendMetric, createMetricsStore } from "./metrics.js";
22
+ import { stripInternalParams } from "./handler-context.js";
23
+ import { isWebSocketUpgradeResponse } from "../response-utils.js";
24
24
 
25
- // Re-export types and cookie utilities for backward compatibility
25
+ // Re-export types consumed through this module's path.
26
26
  export type {
27
27
  CookieOptions,
28
- CollectedMiddleware,
29
- MiddlewareCollectableEntry,
30
28
  MiddlewareContext,
31
29
  MiddlewareEntry,
32
30
  MiddlewareFn,
33
- ResponseHolder,
34
31
  } from "./middleware-types.js";
35
- export { parseCookies, serializeCookie } from "./middleware-cookies.js";
36
32
 
37
33
  const MIDDLEWARE_METRIC_DEPTH = 1;
38
- /** Ignore post-next() durations below this threshold (measurement noise). */
39
34
  const POST_METRIC_MIN_DURATION_MS = 0.01;
40
35
 
41
- function getMiddlewareMetricBase<TEnv>(
36
+ function getMiddlewareMetricLabel<TEnv>(
42
37
  entry: MiddlewareEntry<TEnv>,
43
38
  ordinal: number,
44
39
  ): string {
@@ -46,23 +41,12 @@ function getMiddlewareMetricBase<TEnv>(
46
41
  const scope = entry.pattern ?? "*";
47
42
 
48
43
  if (handlerName) {
49
- return `${handlerName}@${scope}`;
44
+ return `middleware:${handlerName}@${scope}`;
50
45
  }
51
46
 
52
- return `${scope}#${ordinal + 1}`;
47
+ return `middleware:${scope}#${ordinal + 1}`;
53
48
  }
54
49
 
55
- function getMiddlewareMetricLabel<TEnv>(
56
- entry: MiddlewareEntry<TEnv>,
57
- ordinal: number,
58
- ): string {
59
- return `middleware:${getMiddlewareMetricBase(entry, ordinal)}`;
60
- }
61
-
62
- /**
63
- * Parse a route pattern into regex and param names
64
- * Supports: *, /path, /path/*, /path/:param, /path/:param/*
65
- */
66
50
  export function parsePattern(pattern: string): {
67
51
  regex: RegExp;
68
52
  paramNames: string[];
@@ -111,7 +95,12 @@ function escapeRegex(str: string): string {
111
95
  }
112
96
 
113
97
  /**
114
- * Extract params from a pathname using a pattern's regex and param names
98
+ * Extract params from a pathname using a pattern's regex and param names.
99
+ *
100
+ * Values are URL-decoded so apps see the raw string (e.g. "ivo@example.com")
101
+ * instead of the percent-encoded form ("ivo%40example.com"). This matches the
102
+ * contract assumed by ctx.reverse (which re-encodes) and aligns with
103
+ * Express/React Router/Fastify/Koa.
115
104
  */
116
105
  export function extractParams(
117
106
  pathname: string,
@@ -123,7 +112,7 @@ export function extractParams(
123
112
 
124
113
  const params: Record<string, string> = {};
125
114
  for (let i = 0; i < paramNames.length; i++) {
126
- params[paramNames[i]] = match[i + 1] || "";
115
+ params[paramNames[i]] = safeDecodeURIComponent(match[i + 1] || "");
127
116
  }
128
117
  return params;
129
118
  }
@@ -147,7 +136,7 @@ export function createMiddlewareContext<TEnv>(
147
136
  search?: Record<string, unknown>,
148
137
  ) => string,
149
138
  ): MiddlewareContext<TEnv> {
150
- const url = new URL(request.url);
139
+ const url = stripInternalParams(new URL(request.url));
151
140
 
152
141
  // Track the initial response to detect pre/post-next() phase.
153
142
  // Before next(): responseHolder.response === initialResponse (the stub).
@@ -178,14 +167,22 @@ export function createMiddlewareContext<TEnv>(
178
167
  return responseHolder.response;
179
168
  };
180
169
 
170
+ // Capture reqCtx once: the request-scoped platform fields
171
+ // (originalUrl, executionContext, waitUntil) are immutable per request,
172
+ // so snapshotting beats re-reading ALS on every access. The lazy getters
173
+ // below (routeName, theme, setTheme) stay lazy because those can change
174
+ // during `await next()`.
175
+ const reqCtx = _getRequestContext();
181
176
  return {
182
177
  request,
183
178
  url,
184
- originalUrl: new URL(request.url),
179
+ originalUrl: reqCtx?.originalUrl ?? new URL(request.url),
185
180
  pathname: url.pathname,
186
181
  searchParams: url.searchParams,
187
182
  env: env as MiddlewareContext<TEnv>["env"],
188
183
  params,
184
+ executionContext: reqCtx?.executionContext,
185
+ waitUntil: reqCtx ? reqCtx.waitUntil.bind(reqCtx) : fireAndForgetWaitUntil,
189
186
  // Getter: re-derives from request context on each access so that global
190
187
  // middleware sees the matched route name after await next().
191
188
  get routeName(): MiddlewareContext<TEnv>["routeName"] {
@@ -203,12 +200,9 @@ export function createMiddlewareContext<TEnv>(
203
200
  get: ((keyOrVar: any) =>
204
201
  contextGet(variables, keyOrVar)) as MiddlewareContext<TEnv>["get"],
205
202
 
206
- set: ((keyOrVar: any, value: unknown) => {
207
- contextSet(variables, keyOrVar, value);
203
+ set: ((keyOrVar: any, value: unknown, options?: any) => {
204
+ contextSet(variables, keyOrVar, value, options);
208
205
  }) as MiddlewareContext<TEnv>["set"],
209
-
210
- var: variables as MiddlewareContext<TEnv>["var"],
211
-
212
206
  header(name: string, value: string): void {
213
207
  // Before next(): delegate to shared RequestContext stub
214
208
  if (isPreNext()) {
@@ -247,9 +241,13 @@ export function createMiddlewareContext<TEnv>(
247
241
 
248
242
  reverse:
249
243
  reverse ??
250
- ((name: string) => {
244
+ ((
245
+ name: string,
246
+ _params?: Record<string, string>,
247
+ _search?: Record<string, unknown>,
248
+ ) => {
251
249
  throw new Error(
252
- `ctx.reverse() is not available - route map was not provided to middleware context`,
250
+ `ctx.reverse(${JSON.stringify(name)}) is not available: no route map is bound to this middleware context.`,
253
251
  );
254
252
  }),
255
253
 
@@ -293,6 +291,88 @@ export function matchMiddleware<TEnv>(
293
291
  return matches;
294
292
  }
295
293
 
294
+ // Set-Cookie is appended; for other headers stubOverridesNonCookie=true
295
+ // overwrites (chain ran to completion), false fills only missing slots (an
296
+ // explicit short-circuit Response's own headers win).
297
+ function mergeStubHeaders(
298
+ target: Headers,
299
+ stub: Headers,
300
+ stubOverridesNonCookie: boolean,
301
+ ): void {
302
+ stub.forEach((value, name) => {
303
+ // The reserved external-redirect marker is internal and never a trust
304
+ // signal; never copy a stub value (e.g. a stray ctx.header() call) onto a
305
+ // browser-facing response. The opt-in is the out-of-band brand.
306
+ if (name.toLowerCase() === EXTERNAL_REDIRECT_MARKER) return;
307
+ if (name.toLowerCase() === "set-cookie") {
308
+ target.append(name, value);
309
+ } else if (stubOverridesNonCookie || !target.has(name)) {
310
+ target.set(name, value);
311
+ }
312
+ });
313
+ }
314
+
315
+ // Set-Cookie is deduped so a nested inner executeMiddleware that already merged
316
+ // the same reqCtx cookies does not duplicate them; other headers fill if missing.
317
+ function mergeReqCtxStub(
318
+ target: Headers,
319
+ reqCtx: ReturnType<typeof _getRequestContext>,
320
+ ): void {
321
+ if (!reqCtx) return;
322
+ const stubCookies = reqCtx.res.headers.getSetCookie();
323
+ if (stubCookies.length > 0) {
324
+ const existing = new Set(target.getSetCookie());
325
+ for (const cookie of stubCookies) {
326
+ if (!existing.has(cookie)) {
327
+ target.append("set-cookie", cookie);
328
+ }
329
+ }
330
+ }
331
+ reqCtx.res.headers.forEach((value, name) => {
332
+ // Never propagate the reserved external-redirect marker (see mergeStubHeaders).
333
+ if (name.toLowerCase() === EXTERNAL_REDIRECT_MARKER) return;
334
+ if (name !== "set-cookie" && !target.has(name)) {
335
+ target.set(name, value);
336
+ }
337
+ });
338
+ }
339
+
340
+ // Clone `base` with stub headers merged into a fresh Headers (the clone keeps
341
+ // the body mutable for post-next() modifications). Set-Cookie is always
342
+ // appended; other headers obey stubOverridesNonCookie (see mergeStubHeaders).
343
+ // mergeReqCtx folds in RequestContext stub cookies/headers; the intercept
344
+ // short-circuit path passes false (its reqCtx headers are not merged here),
345
+ // which is the one deliberate divergence between the call sites.
346
+ function mergeResponse(
347
+ base: Response,
348
+ stub: Headers,
349
+ opts: { stubOverridesNonCookie: boolean; mergeReqCtx: boolean },
350
+ ): Response {
351
+ const mergedHeaders = new Headers(base.headers);
352
+ // The reserved external-redirect marker is never a trust signal and must never
353
+ // reach the browser. The guard strips it on 3xx redirects; strip it here too so
354
+ // a forged value cannot ride a non-3xx middleware response (which the 3xx-only
355
+ // guard would not touch) to the client. The opt-in is the out-of-band brand.
356
+ mergedHeaders.delete(EXTERNAL_REDIRECT_MARKER);
357
+ mergeStubHeaders(mergedHeaders, stub, opts.stubOverridesNonCookie);
358
+ if (opts.mergeReqCtx) {
359
+ mergeReqCtxStub(mergedHeaders, _getRequestContext());
360
+ }
361
+ const merged = new Response(base.body, {
362
+ status: base.status,
363
+ statusText: base.statusText,
364
+ headers: mergedHeaders,
365
+ });
366
+ // Transfer the out-of-band external-redirect brand across this rebuild: a
367
+ // middleware short-circuit `return redirect(url, { external: true })` reaches
368
+ // the open-redirect guard only after this merge, and the brand lives on the
369
+ // Response object, not in its headers.
370
+ if (isExternalRedirect(base)) {
371
+ markExternalRedirect(merged);
372
+ }
373
+ return merged;
374
+ }
375
+
296
376
  /**
297
377
  * Execute middleware chain
298
378
  *
@@ -301,7 +381,7 @@ export function matchMiddleware<TEnv>(
301
381
  * - `ctx.headers` available before and after `await next()`
302
382
  * - `ctx.header()` shorthand for setting a single header
303
383
  * - Forgiving: if middleware doesn't return, uses the downstream response
304
- * - Short-circuit: return Response to stop chain
384
+ * - Short-circuit: return OR throw a Response to stop chain
305
385
  * - Error catching: try/catch around `next()` works
306
386
  */
307
387
  export async function executeMiddleware<TEnv>(
@@ -331,42 +411,16 @@ export async function executeMiddleware<TEnv>(
331
411
  // End of chain - call actual RSC handler
332
412
  const response = await finalHandler();
333
413
 
334
- // Merge headers set on stub into the real response.
335
- // Use append for Set-Cookie to preserve multiple cookies.
336
- const mergedHeaders = new Headers(response.headers);
337
- stubResponse.headers.forEach((value, name) => {
338
- if (name.toLowerCase() === "set-cookie") {
339
- mergedHeaders.append(name, value);
340
- } else {
341
- mergedHeaders.set(name, value);
342
- }
343
- });
344
- // Also merge shared RequestContext stub (cookies written via cookies().set()).
345
- // Dedup Set-Cookie: an inner executeMiddleware (route-level middleware)
346
- // may have already merged the same reqCtx cookies into the response.
347
- const reqCtx = _getRequestContext();
348
- if (reqCtx) {
349
- const stubCookies = reqCtx.res.headers.getSetCookie();
350
- if (stubCookies.length > 0) {
351
- const existing = new Set(mergedHeaders.getSetCookie());
352
- for (const cookie of stubCookies) {
353
- if (!existing.has(cookie)) {
354
- mergedHeaders.append("set-cookie", cookie);
355
- }
356
- }
357
- }
358
- reqCtx.res.headers.forEach((value, name) => {
359
- if (name !== "set-cookie" && !mergedHeaders.has(name)) {
360
- mergedHeaders.set(name, value);
361
- }
362
- });
414
+ if (isWebSocketUpgradeResponse(response)) {
415
+ responseHolder.response = response;
416
+ return response;
363
417
  }
364
418
 
365
- // Clone response with merged headers (mutable for post-next() modifications)
366
- responseHolder.response = new Response(response.body, {
367
- status: response.status,
368
- statusText: response.statusText,
369
- headers: mergedHeaders,
419
+ // Chain ran to completion: stub headers overwrite (stubOverridesNonCookie)
420
+ // and reqCtx stub headers are merged in.
421
+ responseHolder.response = mergeResponse(response, stubResponse.headers, {
422
+ stubOverridesNonCookie: true,
423
+ mergeReqCtx: true,
370
424
  });
371
425
 
372
426
  return responseHolder.response;
@@ -428,8 +482,16 @@ export async function executeMiddleware<TEnv>(
428
482
  try {
429
483
  result = await entry.handler(ctx, wrappedNext);
430
484
  } catch (error) {
431
- finishMiddleware();
432
- throw error;
485
+ // Thrown Response is short-circuit control flow, not an error.
486
+ // Fall through to the `if (result instanceof Response)` branch below
487
+ // so stub headers and request-context cookies merge as they do for
488
+ // an explicit `return new Response(...)`. Real errors propagate.
489
+ if (error instanceof Response) {
490
+ result = error;
491
+ } else {
492
+ finishMiddleware();
493
+ throw error;
494
+ }
433
495
  }
434
496
  finishMiddleware();
435
497
 
@@ -450,41 +512,18 @@ export async function executeMiddleware<TEnv>(
450
512
 
451
513
  // Explicit return takes precedence (middleware short-circuit).
452
514
  // Merge stub headers (from ctx.header before this point) and
453
- // RequestContext stub headers (from ctx.setCookie) into the
515
+ // RequestContext stub headers (from cookies().set()) into the
454
516
  // returned Response so they are not lost.
455
517
  if (result instanceof Response) {
456
- const mergedHeaders = new Headers(result.headers);
457
- stubResponse.headers.forEach((value, name) => {
458
- if (name.toLowerCase() === "set-cookie") {
459
- mergedHeaders.append(name, value);
460
- } else if (!mergedHeaders.has(name)) {
461
- mergedHeaders.set(name, value);
462
- }
463
- });
464
- // Also merge shared RequestContext stub (cookies written via setCookie).
465
- // Dedup Set-Cookie: an inner executeMiddleware (route-level middleware)
466
- // may have already merged the same reqCtx cookies into the response.
467
- const reqCtx = _getRequestContext();
468
- if (reqCtx) {
469
- const stubCookies = reqCtx.res.headers.getSetCookie();
470
- if (stubCookies.length > 0) {
471
- const existing = new Set(mergedHeaders.getSetCookie());
472
- for (const cookie of stubCookies) {
473
- if (!existing.has(cookie)) {
474
- mergedHeaders.append("set-cookie", cookie);
475
- }
476
- }
477
- }
478
- reqCtx.res.headers.forEach((value, name) => {
479
- if (name !== "set-cookie" && !mergedHeaders.has(name)) {
480
- mergedHeaders.set(name, value);
481
- }
482
- });
518
+ if (isWebSocketUpgradeResponse(result)) {
519
+ responseHolder.response = result;
520
+ return result;
483
521
  }
484
- const merged = new Response(result.body, {
485
- status: result.status,
486
- statusText: result.statusText,
487
- headers: mergedHeaders,
522
+ // Explicit short-circuit: the returned Response's own headers win
523
+ // (stubOverridesNonCookie=false); reqCtx stub headers still merge in.
524
+ const merged = mergeResponse(result, stubResponse.headers, {
525
+ stubOverridesNonCookie: false,
526
+ mergeReqCtx: true,
488
527
  });
489
528
  responseHolder.response = merged;
490
529
  return merged;
@@ -529,23 +568,12 @@ export async function executeMiddleware<TEnv>(
529
568
  // last merge point (e.g. cookies().set() called after await next()).
530
569
  // The reqCtx stub may have already been partially merged during finalHandler
531
570
  // or early-return paths; only append *new* Set-Cookie entries to avoid dupes.
571
+ //
572
+ // Skip for upgrade responses: upgrade headers are semantically immutable and
573
+ // set-cookie on an upgrade is not meaningful.
532
574
  const reqCtx = _getRequestContext();
533
- if (reqCtx) {
534
- const stubCookies = reqCtx.res.headers.getSetCookie();
535
- if (stubCookies.length > 0) {
536
- const existingCookies = new Set(finalResponse.headers.getSetCookie());
537
- for (const cookie of stubCookies) {
538
- if (!existingCookies.has(cookie)) {
539
- finalResponse.headers.append("set-cookie", cookie);
540
- }
541
- }
542
- }
543
- // Fill in non-cookie headers that aren't already on the response
544
- reqCtx.res.headers.forEach((value, name) => {
545
- if (name !== "set-cookie" && !finalResponse.headers.has(name)) {
546
- finalResponse.headers.set(name, value);
547
- }
548
- });
575
+ if (reqCtx && !isWebSocketUpgradeResponse(finalResponse)) {
576
+ mergeReqCtxStub(finalResponse.headers, reqCtx);
549
577
  }
550
578
 
551
579
  return finalResponse;
@@ -556,7 +584,7 @@ export async function executeMiddleware<TEnv>(
556
584
  *
557
585
  * Intercepts use a shared stubResponse from the request context. This function:
558
586
  * - Runs middleware in sequence with a simple next() chain
559
- * - Returns Response if any middleware short-circuits (returns Response or redirects BEFORE next())
587
+ * - Returns Response if any middleware short-circuits (returns OR throws a Response, or redirects, BEFORE next())
560
588
  * - Returns null if all middleware calls next() - headers set after next() remain on stubResponse
561
589
  *
562
590
  * @param middlewares - Array of middleware functions
@@ -615,7 +643,18 @@ export async function executeInterceptMiddleware<TEnv>(
615
643
  return next();
616
644
  };
617
645
 
618
- const result = await middleware(ctx, guardedNext);
646
+ let result: Response | void;
647
+ try {
648
+ result = await middleware(ctx, guardedNext);
649
+ } catch (error) {
650
+ // Thrown Response is short-circuit control flow, parity with the
651
+ // explicit-return path below. Real errors propagate.
652
+ if (error instanceof Response) {
653
+ result = error;
654
+ } else {
655
+ throw error;
656
+ }
657
+ }
619
658
 
620
659
  if (result instanceof Response) {
621
660
  earlyResponse = result;
@@ -639,21 +678,13 @@ export async function executeInterceptMiddleware<TEnv>(
639
678
  });
640
679
 
641
680
  if (hasStubHeaders) {
642
- // Clone and merge headers from stub into early response.
643
- // Only fill in missing headers — the returned Response's explicit
644
- // headers take precedence, matching executeMiddleware behavior.
645
- const mergedHeaders = new Headers(response.headers);
646
- stubResponse.headers.forEach((value, name) => {
647
- if (name.toLowerCase() === "set-cookie") {
648
- mergedHeaders.append(name, value);
649
- } else if (!mergedHeaders.has(name)) {
650
- mergedHeaders.set(name, value);
651
- }
652
- });
653
- return new Response(response.body, {
654
- status: response.status,
655
- statusText: response.statusText,
656
- headers: mergedHeaders,
681
+ // Only fill in missing headers the returned Response's explicit headers
682
+ // take precedence (stubOverridesNonCookie=false), matching executeMiddleware.
683
+ // mergeReqCtx=false: the intercept path deliberately does NOT merge reqCtx
684
+ // stub headers here (pinned by intercept-middleware-headers.test.ts).
685
+ return mergeResponse(response, stubResponse.headers, {
686
+ stubOverridesNonCookie: false,
687
+ mergeReqCtx: false,
657
688
  });
658
689
  }
659
690
  return response;
@@ -694,7 +725,6 @@ export async function executeLoaderMiddleware<TEnv>(
694
725
  regex: null,
695
726
  paramNames: [],
696
727
  handler,
697
- mountPrefix: null,
698
728
  } as MiddlewareEntry<TEnv>,
699
729
  params,
700
730
  }));
@@ -0,0 +1,131 @@
1
+ import type { RouteMatchResult } from "./pattern-matching.js";
2
+
3
+ export interface NavigationSnapshot {
4
+ prevUrl: URL;
5
+ prevParams: Record<string, string>;
6
+ prevMatch: RouteMatchResult | null;
7
+
8
+ interceptContextUrl: URL;
9
+ interceptContextMatch: RouteMatchResult | null;
10
+
11
+ clientSegmentIds: string[];
12
+ clientSegmentSet: Set<string>;
13
+ filteredSegmentIds: string[];
14
+
15
+ stale: boolean;
16
+
17
+ isSameRouteNavigation: boolean;
18
+
19
+ effectiveFromUrl: URL;
20
+ effectiveFromMatch: RouteMatchResult | null;
21
+
22
+ hasInterceptSource: boolean;
23
+
24
+ isHmr: boolean;
25
+ }
26
+
27
+ export interface ResolveNavigationDeps {
28
+ findMatch: (pathname: string) => RouteMatchResult | null;
29
+ }
30
+
31
+ export function resolveNavigation(
32
+ request: Request,
33
+ url: URL,
34
+ currentRouteKey: string,
35
+ deps: ResolveNavigationDeps,
36
+ ): NavigationSnapshot | null {
37
+ const clientSegmentIds =
38
+ url.searchParams.get("_rsc_segments")?.split(",").filter(Boolean) || [];
39
+ const stale = url.searchParams.get("_rsc_stale") === "true";
40
+ const previousUrl =
41
+ request.headers.get("X-RSC-Router-Client-Path") ||
42
+ request.headers.get("Referer");
43
+ const interceptSourceUrl = request.headers.get(
44
+ "X-RSC-Router-Intercept-Source",
45
+ );
46
+ const isHmr = !!request.headers.get("X-RSC-HMR");
47
+
48
+ if (!previousUrl) {
49
+ return null;
50
+ }
51
+
52
+ let prevUrl: URL;
53
+ try {
54
+ prevUrl = new URL(previousUrl, url.origin);
55
+ } catch {
56
+ return null;
57
+ }
58
+
59
+ let interceptContextUrl: URL;
60
+ try {
61
+ interceptContextUrl = interceptSourceUrl
62
+ ? new URL(interceptSourceUrl, url.origin)
63
+ : prevUrl;
64
+ } catch {
65
+ interceptContextUrl = prevUrl;
66
+ }
67
+
68
+ const prevMatch = deps.findMatch(prevUrl.pathname);
69
+ const prevParams = prevMatch?.params || {};
70
+ const interceptContextMatch = interceptSourceUrl
71
+ ? deps.findMatch(interceptContextUrl.pathname)
72
+ : prevMatch;
73
+
74
+ const isSameRouteNavigation = !!(
75
+ interceptContextMatch && interceptContextMatch.routeKey === currentRouteKey
76
+ );
77
+
78
+ const hasInterceptSource = !!interceptSourceUrl;
79
+ const effectiveFromUrl = hasInterceptSource ? interceptContextUrl : prevUrl;
80
+ const effectiveFromMatch = hasInterceptSource
81
+ ? interceptContextMatch
82
+ : prevMatch;
83
+
84
+ const filteredSegmentIds = clientSegmentIds.filter((id) => {
85
+ if (id.includes(".@")) return false;
86
+ if (/D\d+\./.test(id)) return false;
87
+ return true;
88
+ });
89
+
90
+ const clientSegmentSet = new Set(clientSegmentIds);
91
+
92
+ return {
93
+ prevUrl,
94
+ prevParams,
95
+ prevMatch,
96
+ interceptContextUrl,
97
+ interceptContextMatch,
98
+ clientSegmentIds,
99
+ clientSegmentSet,
100
+ filteredSegmentIds,
101
+ stale,
102
+ isSameRouteNavigation,
103
+ effectiveFromUrl,
104
+ effectiveFromMatch,
105
+ hasInterceptSource,
106
+ isHmr,
107
+ };
108
+ }
109
+
110
+ export function createNavigationSnapshot(
111
+ overrides?: Partial<NavigationSnapshot>,
112
+ ): NavigationSnapshot {
113
+ const defaultUrl = new URL("http://localhost/");
114
+ return {
115
+ prevUrl: defaultUrl,
116
+ prevParams: {},
117
+ prevMatch: null,
118
+ interceptContextUrl: defaultUrl,
119
+ interceptContextMatch: null,
120
+ clientSegmentIds: [],
121
+ clientSegmentSet: new Set(),
122
+ filteredSegmentIds: [],
123
+ stale: false,
124
+ isSameRouteNavigation: false,
125
+ effectiveFromUrl: defaultUrl,
126
+ effectiveFromMatch: null,
127
+ hasInterceptSource: false,
128
+ isHmr: false,
129
+ ...overrides,
130
+ };
131
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared route-param comparison helpers.
3
+ */
4
+
5
+ /**
6
+ * Shallow equality for two route-param records. Same-reference is a fast path;
7
+ * otherwise compares key count then each value.
8
+ */
9
+ export function paramsEqual(
10
+ a: Record<string, string>,
11
+ b: Record<string, string>,
12
+ ): boolean {
13
+ if (a === b) return true;
14
+
15
+ const keysA = Object.keys(a);
16
+ if (keysA.length !== Object.keys(b).length) return false;
17
+
18
+ for (const key of keysA) {
19
+ if (a[key] !== b[key]) return false;
20
+ }
21
+
22
+ return true;
23
+ }