@rangojs/router 0.0.0-experimental.1b930379 → 0.0.0-experimental.1c0bdfad

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 (400) hide show
  1. package/AGENTS.md +12 -0
  2. package/README.md +245 -49
  3. package/dist/bin/rango.js +441 -134
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3453 -1240
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +76 -21
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +64 -2
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +247 -23
  12. package/skills/caching/SKILL.md +318 -15
  13. package/skills/composability/SKILL.md +27 -2
  14. package/skills/css/SKILL.md +76 -0
  15. package/skills/debug-manifest/SKILL.md +4 -2
  16. package/skills/document-cache/SKILL.md +78 -55
  17. package/skills/handler-use/SKILL.md +364 -0
  18. package/skills/hooks/SKILL.md +250 -30
  19. package/skills/host-router/SKILL.md +83 -23
  20. package/skills/i18n/SKILL.md +276 -0
  21. package/skills/intercept/SKILL.md +50 -6
  22. package/skills/layout/SKILL.md +35 -9
  23. package/skills/links/SKILL.md +249 -17
  24. package/skills/loader/SKILL.md +279 -53
  25. package/skills/middleware/SKILL.md +52 -13
  26. package/skills/migrate-nextjs/SKILL.md +584 -0
  27. package/skills/migrate-react-router/SKILL.md +771 -0
  28. package/skills/mime-routes/SKILL.md +28 -1
  29. package/skills/observability/SKILL.md +172 -0
  30. package/skills/parallel/SKILL.md +203 -7
  31. package/skills/prerender/SKILL.md +153 -109
  32. package/skills/rango/SKILL.md +251 -22
  33. package/skills/react-compiler/SKILL.md +168 -0
  34. package/skills/response-routes/SKILL.md +123 -48
  35. package/skills/route/SKILL.md +101 -5
  36. package/skills/router-setup/SKILL.md +116 -8
  37. package/skills/scripts/SKILL.md +179 -0
  38. package/skills/server-actions/SKILL.md +775 -0
  39. package/skills/streams-and-websockets/SKILL.md +283 -0
  40. package/skills/tailwind/SKILL.md +27 -3
  41. package/skills/testing/SKILL.md +129 -0
  42. package/skills/testing/bindings.md +103 -0
  43. package/skills/testing/cache-prerender.md +127 -0
  44. package/skills/testing/client-components.md +124 -0
  45. package/skills/testing/e2e-parity.md +125 -0
  46. package/skills/testing/flight.md +91 -0
  47. package/skills/testing/handles.md +129 -0
  48. package/skills/testing/loader.md +128 -0
  49. package/skills/testing/middleware.md +99 -0
  50. package/skills/testing/render-handler.md +121 -0
  51. package/skills/testing/response-routes.md +95 -0
  52. package/skills/testing/reverse-and-types.md +84 -0
  53. package/skills/testing/server-actions.md +107 -0
  54. package/skills/testing/server-tree.md +128 -0
  55. package/skills/testing/setup.md +123 -0
  56. package/skills/typesafety/SKILL.md +332 -29
  57. package/skills/use-cache/SKILL.md +46 -14
  58. package/skills/view-transitions/SKILL.md +294 -0
  59. package/src/__augment-tests__/augment.ts +81 -0
  60. package/src/__augment-tests__/augmented.check.ts +116 -0
  61. package/src/__internal.ts +1 -66
  62. package/src/browser/action-coordinator.ts +53 -36
  63. package/src/browser/action-fence.ts +47 -0
  64. package/src/browser/app-shell.ts +39 -0
  65. package/src/browser/app-version.ts +14 -0
  66. package/src/browser/connection-warmup.ts +134 -0
  67. package/src/browser/cookie-name.ts +140 -0
  68. package/src/browser/event-controller.ts +197 -150
  69. package/src/browser/history-state.ts +21 -0
  70. package/src/browser/index.ts +3 -3
  71. package/src/browser/invalidate-client-cache.ts +52 -0
  72. package/src/browser/navigation-bridge.ts +111 -31
  73. package/src/browser/navigation-client.ts +201 -67
  74. package/src/browser/navigation-store-handle.ts +38 -0
  75. package/src/browser/navigation-store.ts +76 -67
  76. package/src/browser/navigation-transaction.ts +18 -66
  77. package/src/browser/network-error-handler.ts +34 -7
  78. package/src/browser/partial-update.ts +187 -112
  79. package/src/browser/prefetch/cache.ts +230 -35
  80. package/src/browser/prefetch/fetch.ts +338 -39
  81. package/src/browser/prefetch/queue.ts +126 -20
  82. package/src/browser/prefetch/resource-ready.ts +77 -0
  83. package/src/browser/rango-state.ts +158 -76
  84. package/src/browser/react/Link.tsx +111 -16
  85. package/src/browser/react/NavigationProvider.tsx +135 -120
  86. package/src/browser/react/ScrollRestoration.tsx +10 -6
  87. package/src/browser/react/context.ts +7 -2
  88. package/src/browser/react/filter-segment-order.ts +66 -7
  89. package/src/browser/react/index.ts +0 -48
  90. package/src/browser/react/location-state-shared.ts +178 -8
  91. package/src/browser/react/location-state.ts +39 -14
  92. package/src/browser/react/use-action.ts +6 -15
  93. package/src/browser/react/use-handle.ts +23 -69
  94. package/src/browser/react/use-href.tsx +8 -1
  95. package/src/browser/react/use-link-status.ts +33 -8
  96. package/src/browser/react/use-navigation.ts +32 -7
  97. package/src/browser/react/use-params.ts +20 -10
  98. package/src/browser/react/use-reverse.ts +106 -0
  99. package/src/browser/react/use-router.ts +46 -11
  100. package/src/browser/react/use-search-params.ts +0 -5
  101. package/src/browser/react/use-segments.ts +11 -21
  102. package/src/browser/response-adapter.ts +80 -5
  103. package/src/browser/rsc-router.tsx +218 -76
  104. package/src/browser/scroll-restoration.ts +54 -42
  105. package/src/browser/segment-reconciler.ts +36 -9
  106. package/src/browser/segment-structure-assert.ts +2 -2
  107. package/src/browser/server-action-bridge.ts +222 -61
  108. package/src/browser/types.ts +91 -11
  109. package/src/browser/validate-redirect-origin.ts +43 -16
  110. package/src/build/collect-fallback-refs.ts +107 -0
  111. package/src/build/generate-manifest.ts +65 -40
  112. package/src/build/generate-route-types.ts +5 -1
  113. package/src/build/index.ts +8 -2
  114. package/src/build/prefix-tree-utils.ts +123 -0
  115. package/src/build/route-trie.ts +165 -36
  116. package/src/build/route-types/ast-route-extraction.ts +15 -8
  117. package/src/build/route-types/codegen.ts +16 -5
  118. package/src/build/route-types/include-resolution.ts +125 -24
  119. package/src/build/route-types/param-extraction.ts +6 -3
  120. package/src/build/route-types/per-module-writer.ts +22 -6
  121. package/src/build/route-types/router-processing.ts +272 -96
  122. package/src/build/route-types/scan-filter.ts +9 -2
  123. package/src/build/route-types/source-scan.ts +216 -0
  124. package/src/build/runtime-discovery.ts +9 -20
  125. package/src/cache/cache-error.ts +104 -0
  126. package/src/cache/cache-key-utils.ts +29 -13
  127. package/src/cache/cache-policy.ts +108 -34
  128. package/src/cache/cache-runtime.ts +214 -48
  129. package/src/cache/cache-scope.ts +236 -89
  130. package/src/cache/cache-tag.ts +103 -0
  131. package/src/cache/cf/cf-base64.ts +33 -0
  132. package/src/cache/cf/cf-cache-constants.ts +127 -0
  133. package/src/cache/cf/cf-cache-store.ts +2224 -171
  134. package/src/cache/cf/cf-cache-types.ts +349 -0
  135. package/src/cache/cf/cf-kv-utils.ts +46 -0
  136. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  137. package/src/cache/cf/index.ts +11 -17
  138. package/src/cache/document-cache.ts +89 -27
  139. package/src/cache/handle-snapshot.ts +70 -0
  140. package/src/cache/index.ts +11 -20
  141. package/src/cache/memory-segment-store.ts +136 -37
  142. package/src/cache/profile-registry.ts +31 -31
  143. package/src/cache/read-through-swr.ts +41 -11
  144. package/src/cache/segment-codec.ts +9 -17
  145. package/src/cache/tag-invalidation.ts +230 -0
  146. package/src/cache/taint.ts +55 -0
  147. package/src/cache/types.ts +37 -100
  148. package/src/client.rsc.tsx +44 -21
  149. package/src/client.tsx +119 -290
  150. package/src/cloudflare/index.ts +11 -0
  151. package/src/cloudflare/tracing.ts +109 -0
  152. package/src/component-utils.ts +19 -0
  153. package/src/components/DefaultDocument.tsx +8 -2
  154. package/src/context-var.ts +84 -2
  155. package/src/debug.ts +2 -2
  156. package/src/decode-loader-results.ts +52 -0
  157. package/src/defer.ts +196 -0
  158. package/src/deps/ssr.ts +0 -1
  159. package/src/encode-kv.ts +49 -0
  160. package/src/errors.ts +30 -4
  161. package/src/escape-script.ts +52 -0
  162. package/src/handle.ts +70 -22
  163. package/src/handles/MetaTags.tsx +56 -19
  164. package/src/handles/Scripts.tsx +183 -0
  165. package/src/handles/breadcrumbs.ts +37 -8
  166. package/src/handles/is-thenable.ts +19 -0
  167. package/src/handles/meta.ts +51 -40
  168. package/src/handles/script.ts +244 -0
  169. package/src/host/cookie-handler.ts +9 -60
  170. package/src/host/errors.ts +0 -24
  171. package/src/host/index.ts +8 -2
  172. package/src/host/pattern-matcher.ts +23 -52
  173. package/src/host/router.ts +107 -99
  174. package/src/host/testing.ts +40 -27
  175. package/src/host/types.ts +37 -4
  176. package/src/host/utils.ts +1 -1
  177. package/src/href-client.ts +137 -22
  178. package/src/index.rsc.ts +93 -12
  179. package/src/index.ts +133 -15
  180. package/src/internal-debug.ts +11 -10
  181. package/src/loader-store.ts +500 -0
  182. package/src/loader.rsc.ts +20 -13
  183. package/src/loader.ts +12 -11
  184. package/src/missing-id-error.ts +68 -0
  185. package/src/outlet-context.ts +1 -1
  186. package/src/outlet-provider.tsx +1 -5
  187. package/src/prerender/param-hash.ts +16 -16
  188. package/src/prerender/store.ts +37 -41
  189. package/src/prerender.ts +198 -82
  190. package/src/redirect-origin.ts +100 -0
  191. package/src/regex-escape.ts +8 -0
  192. package/src/render-error-thrower.tsx +20 -0
  193. package/src/response-utils.ts +62 -0
  194. package/src/reverse.ts +65 -15
  195. package/src/root-error-boundary.tsx +1 -19
  196. package/src/route-content-wrapper.tsx +7 -72
  197. package/src/route-definition/dsl-helpers.ts +469 -276
  198. package/src/route-definition/helper-factories.ts +29 -139
  199. package/src/route-definition/helpers-types.ts +113 -37
  200. package/src/route-definition/index.ts +3 -0
  201. package/src/route-definition/redirect.ts +53 -12
  202. package/src/route-definition/resolve-handler-use.ts +161 -0
  203. package/src/route-definition/use-item-types.ts +32 -0
  204. package/src/route-map-builder.ts +7 -17
  205. package/src/route-types.ts +37 -41
  206. package/src/router/basename.ts +14 -0
  207. package/src/router/content-negotiation.ts +164 -17
  208. package/src/router/error-handling.ts +45 -18
  209. package/src/router/find-match.ts +45 -22
  210. package/src/router/handler-context.ts +83 -39
  211. package/src/router/instrument.ts +350 -0
  212. package/src/router/intercept-resolution.ts +50 -24
  213. package/src/router/lazy-includes.ts +19 -53
  214. package/src/router/loader-resolution.ts +274 -56
  215. package/src/router/logging.ts +5 -8
  216. package/src/router/manifest.ts +49 -45
  217. package/src/router/match-api.ts +120 -204
  218. package/src/router/match-context.ts +0 -22
  219. package/src/router/match-handlers.ts +58 -58
  220. package/src/router/match-middleware/background-revalidation.ts +33 -6
  221. package/src/router/match-middleware/cache-lookup.ts +214 -263
  222. package/src/router/match-middleware/cache-store.ts +73 -33
  223. package/src/router/match-middleware/intercept-resolution.ts +8 -28
  224. package/src/router/match-middleware/segment-resolution.ts +52 -18
  225. package/src/router/match-pipelines.ts +1 -42
  226. package/src/router/match-result.ts +104 -40
  227. package/src/router/metrics.ts +5 -34
  228. package/src/router/middleware-types.ts +13 -142
  229. package/src/router/middleware.ts +270 -172
  230. package/src/router/navigation-snapshot.ts +131 -0
  231. package/src/router/params-util.ts +23 -0
  232. package/src/router/pattern-matching.ts +132 -90
  233. package/src/router/prefetch-cache-ttl.ts +51 -0
  234. package/src/router/prerender-match.ts +195 -56
  235. package/src/router/preview-match.ts +32 -102
  236. package/src/router/request-classification.ts +276 -0
  237. package/src/router/revalidation.ts +123 -73
  238. package/src/router/route-snapshot.ts +244 -0
  239. package/src/router/router-context.ts +8 -28
  240. package/src/router/router-interfaces.ts +115 -35
  241. package/src/router/router-options.ts +172 -15
  242. package/src/router/router-registry.ts +2 -5
  243. package/src/router/segment-resolution/fresh.ts +264 -77
  244. package/src/router/segment-resolution/helpers.ts +115 -30
  245. package/src/router/segment-resolution/loader-cache.ts +63 -37
  246. package/src/router/segment-resolution/revalidation.ts +474 -385
  247. package/src/router/segment-resolution/static-store.ts +19 -5
  248. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  249. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  250. package/src/router/segment-resolution.ts +5 -1
  251. package/src/router/segment-wrappers.ts +8 -5
  252. package/src/router/state-cookie-name.ts +33 -0
  253. package/src/router/substitute-pattern-params.ts +56 -0
  254. package/src/router/telemetry-otel.ts +161 -199
  255. package/src/router/telemetry.ts +96 -19
  256. package/src/router/timeout.ts +0 -20
  257. package/src/router/tracing.ts +206 -0
  258. package/src/router/trie-matching.ts +163 -59
  259. package/src/router/types.ts +10 -63
  260. package/src/router/url-params.ts +44 -0
  261. package/src/router.ts +163 -55
  262. package/src/rsc/handler-context.ts +3 -2
  263. package/src/rsc/handler.ts +658 -511
  264. package/src/rsc/helpers.ts +168 -46
  265. package/src/rsc/index.ts +2 -5
  266. package/src/rsc/json-route-result.ts +38 -0
  267. package/src/rsc/loader-fetch.ts +127 -31
  268. package/src/rsc/manifest-init.ts +33 -42
  269. package/src/rsc/origin-guard.ts +39 -25
  270. package/src/rsc/progressive-enhancement.ts +77 -11
  271. package/src/rsc/redirect-guard.ts +99 -0
  272. package/src/rsc/response-cache-serve.ts +238 -0
  273. package/src/rsc/response-error.ts +79 -12
  274. package/src/rsc/response-route-handler.ts +99 -189
  275. package/src/rsc/rsc-rendering.ts +105 -72
  276. package/src/rsc/runtime-warnings.ts +23 -10
  277. package/src/rsc/server-action.ts +263 -112
  278. package/src/rsc/ssr-setup.ts +18 -2
  279. package/src/rsc/types.ts +32 -6
  280. package/src/runtime-env.ts +18 -0
  281. package/src/search-params.ts +35 -30
  282. package/src/segment-content-promise.ts +67 -0
  283. package/src/segment-loader-promise.ts +149 -0
  284. package/src/segment-system.tsx +281 -129
  285. package/src/serialize.ts +243 -0
  286. package/src/server/context.ts +309 -61
  287. package/src/server/cookie-parse.ts +32 -0
  288. package/src/server/cookie-store.ts +80 -5
  289. package/src/server/handle-store.ts +40 -38
  290. package/src/server/loader-registry.ts +26 -46
  291. package/src/server/request-context.ts +398 -172
  292. package/src/ssr/index.tsx +25 -16
  293. package/src/static-handler.ts +27 -18
  294. package/src/testing/cache-status.ts +162 -0
  295. package/src/testing/collect-handle.ts +40 -0
  296. package/src/testing/dispatch.ts +701 -0
  297. package/src/testing/dom.entry.ts +22 -0
  298. package/src/testing/e2e/fixture.ts +188 -0
  299. package/src/testing/e2e/index.ts +128 -0
  300. package/src/testing/e2e/matchers.ts +35 -0
  301. package/src/testing/e2e/page-helpers.ts +272 -0
  302. package/src/testing/e2e/parity.ts +387 -0
  303. package/src/testing/e2e/server.ts +195 -0
  304. package/src/testing/flight-matchers.ts +97 -0
  305. package/src/testing/flight-normalize.ts +11 -0
  306. package/src/testing/flight-runtime.d.ts +57 -0
  307. package/src/testing/flight-tree.ts +682 -0
  308. package/src/testing/flight.entry.ts +52 -0
  309. package/src/testing/flight.ts +257 -0
  310. package/src/testing/generated-routes.ts +183 -0
  311. package/src/testing/index.ts +99 -0
  312. package/src/testing/internal/context.ts +371 -0
  313. package/src/testing/internal/flight-client-globals.ts +30 -0
  314. package/src/testing/internal/seed-vars.ts +54 -0
  315. package/src/testing/render-handler.ts +343 -0
  316. package/src/testing/render-route.tsx +581 -0
  317. package/src/testing/run-loader.ts +385 -0
  318. package/src/testing/run-middleware.ts +205 -0
  319. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  320. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  321. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  322. package/src/testing/vitest-stubs/version.ts +5 -0
  323. package/src/testing/vitest.ts +305 -0
  324. package/src/theme/ThemeProvider.tsx +20 -58
  325. package/src/theme/ThemeScript.tsx +7 -9
  326. package/src/theme/constants.ts +52 -13
  327. package/src/theme/index.ts +0 -7
  328. package/src/theme/theme-context.ts +1 -5
  329. package/src/theme/theme-script.ts +22 -21
  330. package/src/theme/use-theme.ts +0 -3
  331. package/src/types/boundaries.ts +0 -35
  332. package/src/types/cache-types.ts +17 -8
  333. package/src/types/error-types.ts +30 -90
  334. package/src/types/global-namespace.ts +54 -41
  335. package/src/types/handler-context.ts +233 -81
  336. package/src/types/index.ts +1 -10
  337. package/src/types/loader-types.ts +44 -15
  338. package/src/types/request-scope.ts +112 -0
  339. package/src/types/route-config.ts +6 -50
  340. package/src/types/route-entry.ts +19 -7
  341. package/src/types/segments.ts +37 -14
  342. package/src/urls/include-helper.ts +33 -70
  343. package/src/urls/index.ts +1 -11
  344. package/src/urls/path-helper-types.ts +58 -11
  345. package/src/urls/path-helper.ts +57 -111
  346. package/src/urls/pattern-types.ts +48 -19
  347. package/src/urls/response-types.ts +25 -22
  348. package/src/urls/type-extraction.ts +58 -139
  349. package/src/urls/urls-function.ts +1 -18
  350. package/src/use-loader.tsx +346 -89
  351. package/src/vite/debug.ts +185 -0
  352. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  353. package/src/vite/discovery/discover-routers.ts +130 -85
  354. package/src/vite/discovery/discovery-errors.ts +194 -0
  355. package/src/vite/discovery/gate-state.ts +171 -0
  356. package/src/vite/discovery/prerender-collection.ts +214 -132
  357. package/src/vite/discovery/route-types-writer.ts +40 -84
  358. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  359. package/src/vite/discovery/state.ts +57 -6
  360. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  361. package/src/vite/index.ts +6 -0
  362. package/src/vite/inject-client-debug.ts +36 -0
  363. package/src/vite/plugin-types.ts +155 -65
  364. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  365. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  366. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  367. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  368. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  369. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  370. package/src/vite/plugins/expose-action-id.ts +49 -98
  371. package/src/vite/plugins/expose-id-utils.ts +96 -51
  372. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  373. package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
  374. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  375. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  376. package/src/vite/plugins/expose-internal-ids.ts +553 -317
  377. package/src/vite/plugins/performance-tracks.ts +89 -0
  378. package/src/vite/plugins/refresh-cmd.ts +89 -27
  379. package/src/vite/plugins/use-cache-transform.ts +73 -83
  380. package/src/vite/plugins/version-injector.ts +21 -25
  381. package/src/vite/plugins/version-plugin.ts +46 -37
  382. package/src/vite/plugins/virtual-entries.ts +13 -18
  383. package/src/vite/rango.ts +238 -295
  384. package/src/vite/router-discovery.ts +940 -149
  385. package/src/vite/utils/ast-handler-extract.ts +26 -35
  386. package/src/vite/utils/banner.ts +4 -4
  387. package/src/vite/utils/bundle-analysis.ts +10 -15
  388. package/src/vite/utils/client-chunks.ts +184 -0
  389. package/src/vite/utils/directive-prologue.ts +40 -0
  390. package/src/vite/utils/forward-user-plugins.ts +171 -0
  391. package/src/vite/utils/manifest-utils.ts +4 -59
  392. package/src/vite/utils/package-resolution.ts +20 -52
  393. package/src/vite/utils/prerender-utils.ts +81 -34
  394. package/src/vite/utils/shared-utils.ts +92 -42
  395. package/src/browser/action-response-classifier.ts +0 -99
  396. package/src/browser/react/use-client-cache.ts +0 -58
  397. package/src/browser/shallow.ts +0 -40
  398. package/src/handles/index.ts +0 -7
  399. package/src/network-error-thrower.tsx +0 -23
  400. package/src/router/middleware-cookies.ts +0 -55
@@ -16,14 +16,38 @@ import {
16
16
  getRequestContext,
17
17
  _getRequestContext,
18
18
  } from "../server/request-context.js";
19
- import { serializeSegments, deserializeSegments } from "./segment-codec.js";
20
- import { captureHandles, restoreHandles } from "./handle-snapshot.js";
19
+ import { recordRequestTags } from "./cache-tag.js";
20
+ import { reportCacheError } from "./cache-error.js";
21
+ // segment-codec is the only module on cache-scope's import graph that eagerly
22
+ // pulls @vitejs/plugin-rsc (a virtual: module the plain node/vitest runner cannot
23
+ // resolve). It is imported LAZILY at the two call sites below (deserializeSegments
24
+ // in lookupRoute, serializeSegments in cacheRoute) so that requiring cache-scope —
25
+ // e.g. dispatch's lazy `import("../cache/cache-scope.js")` for the response-route
26
+ // cache path — does not crash a consumer test that never mocks plugin-rsc. Behavior
27
+ // is unchanged: both methods are async and already awaited the codec.
28
+ import {
29
+ captureHandles,
30
+ restoreHandles,
31
+ encodeHandles,
32
+ decodeHandles,
33
+ } from "./handle-snapshot.js";
21
34
  import { sortedSearchString, sortedRouteParams } from "./cache-key-utils.js";
22
35
  import {
23
36
  DEFAULT_ROUTE_TTL,
37
+ isFiniteNonNegativeSeconds,
24
38
  resolveCacheKey,
25
39
  resolveCacheStore,
40
+ resolveTagsOption,
26
41
  } from "./cache-policy.js";
42
+ import type { RequestContext } from "../server/request-context.js";
43
+
44
+ export function resolveCacheTags(
45
+ config: PartialCacheOptions | false,
46
+ ctx: RequestContext | undefined,
47
+ ): string[] | undefined {
48
+ if (config === false) return undefined;
49
+ return resolveTagsOption(config.tags, ctx, "CacheScope");
50
+ }
27
51
 
28
52
  function debugCacheLog(message: string): void {
29
53
  if (INTERNAL_RANGO_DEBUG) {
@@ -31,17 +55,36 @@ function debugCacheLog(message: string): void {
31
55
  }
32
56
  }
33
57
 
34
- // ============================================================================
35
- // Key Generation (internal)
36
- // ============================================================================
37
-
38
58
  /**
39
- * Generate cache key base from host, pathname, route params, and search params.
40
- * Host is included to prevent cross-host cache collisions on shared stores.
41
- * Route params and search params are sorted alphabetically for deterministic keys.
42
- * Internal _rsc* and __* query params are excluded.
43
- * @internal
59
+ * A finite, non-negative seconds value? A NaN/Infinity ttl/swr (from a bad
60
+ * cache() option or store defaults) flows into computeExpiration ->
61
+ * staleAt/expiresAt = NaN, where every `now > NaN` is false so the entry never
62
+ * evicts and is served fresh forever; a negative value makes every read a miss.
63
+ * Mirror profile-registry.ts's Number.isFinite + >= 0 check, but the callers
64
+ * degrade to a default (warning in dev) rather than throw — this runs on the
65
+ * foreground render.
44
66
  */
67
+ function isValidCacheSeconds(value: number, label: string): boolean {
68
+ if (isFiniteNonNegativeSeconds(value)) return true;
69
+ if (process.env.NODE_ENV !== "production") {
70
+ console.warn(
71
+ `[CacheScope] Invalid ${label} ${value}; falling back to default`,
72
+ );
73
+ }
74
+ return false;
75
+ }
76
+
77
+ /** Coerce a resolved ttl to a finite, non-negative number (default on invalid). */
78
+ function validatedTtl(value: number): number {
79
+ return isValidCacheSeconds(value, "ttl") ? value : DEFAULT_ROUTE_TTL;
80
+ }
81
+
82
+ /** Coerce a resolved swr to a finite, non-negative number, or undefined (no SWR window). */
83
+ function validatedSwr(value: number | undefined): number | undefined {
84
+ if (value === undefined) return undefined;
85
+ return isValidCacheSeconds(value, "swr") ? value : undefined;
86
+ }
87
+
45
88
  function getCacheKeyBase(
46
89
  host: string,
47
90
  pathname: string,
@@ -57,23 +100,13 @@ function getCacheKeyBase(
57
100
  return key;
58
101
  }
59
102
 
60
- /**
61
- * Generate default cache key for a route request.
62
- * Includes pathname, route params, and user-facing search params for
63
- * correct scoping. Internal _rsc* params are excluded.
64
- * Includes request type prefix since they produce different segment sets:
65
- * - doc: document requests (full page load)
66
- * - partial: navigation requests (client-side navigation)
67
- * - intercept: intercept navigation (modal/overlay routes)
68
- * @internal
69
- */
70
103
  function getDefaultRouteCacheKey(
71
104
  pathname: string,
72
105
  params?: Record<string, string>,
73
106
  isIntercept?: boolean,
74
107
  ): string {
75
108
  const ctx = getRequestContext();
76
- const isPartial = ctx?.url.searchParams.has("_rsc_partial") ?? false;
109
+ const isPartial = ctx?.originalUrl?.searchParams.has("_rsc_partial") ?? false;
77
110
  const searchParams = ctx?.url.searchParams;
78
111
  const host = ctx?.url.host ?? "localhost";
79
112
 
@@ -128,20 +161,26 @@ export class CacheScope {
128
161
  }
129
162
 
130
163
  /**
131
- * Get effective TTL from config or store defaults
164
+ * Get effective TTL from config or store defaults.
165
+ *
166
+ * Unlike profile-registry.ts (which fails fast at config time), the render
167
+ * path must DEGRADE: a non-finite/negative ttl (NaN/Infinity from a bad
168
+ * defaults config) would make computeExpiration produce NaN deadlines so the
169
+ * entry never evicts, or a guaranteed miss for a negative value. Fall back to
170
+ * DEFAULT_ROUTE_TTL instead of throwing in the foreground render.
132
171
  */
133
172
  get ttl(): number {
134
173
  if (this.config === false) return 0;
135
174
 
136
175
  // Explicit TTL in cache() options
137
176
  if (this.config.ttl !== undefined) {
138
- return this.config.ttl;
177
+ return validatedTtl(this.config.ttl);
139
178
  }
140
179
 
141
180
  // Fall back to store defaults (explicit store first, then app-level)
142
181
  const store = this.getStore();
143
182
  if (store?.defaults?.ttl !== undefined) {
144
- return store.defaults.ttl;
183
+ return validatedTtl(store.defaults.ttl);
145
184
  }
146
185
 
147
186
  // Hardcoded fallback
@@ -149,19 +188,22 @@ export class CacheScope {
149
188
  }
150
189
 
151
190
  /**
152
- * Get SWR window from config or store defaults
191
+ * Get SWR window from config or store defaults.
192
+ *
193
+ * A non-finite/negative swr is degraded to undefined (no SWR window) rather
194
+ * than fed into expiry math; see the ttl getter for the rationale.
153
195
  */
154
196
  get swr(): number | undefined {
155
197
  if (this.config === false) return undefined;
156
198
 
157
199
  // Explicit SWR in cache() options
158
200
  if (this.config.swr !== undefined) {
159
- return this.config.swr;
201
+ return validatedSwr(this.config.swr);
160
202
  }
161
203
 
162
204
  // Fall back to store defaults
163
205
  const store = this.getStore();
164
- return store?.defaults?.swr;
206
+ return validatedSwr(store?.defaults?.swr);
165
207
  }
166
208
 
167
209
  /**
@@ -187,6 +229,32 @@ export class CacheScope {
187
229
  return resolveCacheKey(keyFn, this.getStore(), defaultKey, "CacheScope");
188
230
  }
189
231
 
232
+ /**
233
+ * Evaluate the cache `condition` predicate. Returns false (skip the cache
234
+ * operation) when the predicate returns false or throws; returns true when
235
+ * there is no condition or no request context to evaluate it against.
236
+ */
237
+ private conditionAllows(op: "read" | "write"): boolean {
238
+ if (this.config === false || !this.config.condition) return true;
239
+ const requestCtx = getRequestContext();
240
+ if (!requestCtx) return true;
241
+ try {
242
+ if (!this.config.condition(requestCtx)) {
243
+ debugCacheLog(
244
+ `[CacheScope] condition returned false, skipping cache ${op}`,
245
+ );
246
+ return false;
247
+ }
248
+ return true;
249
+ } catch (error) {
250
+ console.error(
251
+ `[CacheScope] condition function threw, skipping cache ${op}:`,
252
+ error,
253
+ );
254
+ return false;
255
+ }
256
+ }
257
+
190
258
  /**
191
259
  * Lookup cached segments for a route (single cache entry per request).
192
260
  * Returns { segments, shouldRevalidate } or null if cache miss.
@@ -204,35 +272,21 @@ export class CacheScope {
204
272
  shouldRevalidate: boolean;
205
273
  } | null> {
206
274
  if (!this.enabled) return null;
207
-
208
- // Evaluate condition — skip cache read when condition returns false
209
- if (this.config !== false && this.config.condition) {
210
- const requestCtx = getRequestContext();
211
- if (requestCtx) {
212
- try {
213
- if (!this.config.condition(requestCtx)) {
214
- debugCacheLog(
215
- `[CacheScope] condition returned false, skipping cache read`,
216
- );
217
- return null;
218
- }
219
- } catch (error) {
220
- console.error(
221
- `[CacheScope] condition function threw, skipping cache read:`,
222
- error,
223
- );
224
- return null;
225
- }
226
- }
227
- }
275
+ if (!this.conditionAllows("read")) return null;
228
276
 
229
277
  const store = this.getStore();
230
278
  if (!store) return null;
231
279
 
232
- // Resolve cache key (may use custom key functions)
233
- const key = await this.resolveKey(pathname, params, isIntercept);
234
-
280
+ // Resolve cache key INSIDE the try so a throwing consumer key() (or a
281
+ // store.keyGenerator) degrades to a cache miss (return null -> render
282
+ // uncached) instead of crashing the foreground render. resolveCacheKey
283
+ // itself keeps its hard-fail/no-fallback-to-default contract (a throw must
284
+ // not silently collide onto the default slot); the graceful degradation
285
+ // happens here, where a miss is a safe outcome.
286
+ let key: string | undefined;
235
287
  try {
288
+ key = await this.resolveKey(pathname, params, isIntercept);
289
+
236
290
  const result = await store.get(key);
237
291
 
238
292
  if (!result) {
@@ -242,13 +296,44 @@ export class CacheScope {
242
296
 
243
297
  const { data: cached, shouldRevalidate } = result;
244
298
 
245
- // Deserialize segments
246
- const segments = await deserializeSegments(cached.segments);
299
+ // Deserialize segments. A failure means the cached segments are corrupt/
300
+ // partial: evict the entry (self-heal - the re-render re-caches under the
301
+ // same key) and report it as corruption, distinct from a transient infra
302
+ // error (handled by the outer catch).
303
+ let segments: ResolvedSegment[];
304
+ try {
305
+ const { deserializeSegments } = await import("./segment-codec.js");
306
+ segments = await deserializeSegments(cached.segments);
307
+ } catch (error) {
308
+ reportCacheError(
309
+ error,
310
+ "cache-corrupt",
311
+ `[CacheScope] ${key}: corrupt cached segments, evicting`,
312
+ );
313
+ await store
314
+ .delete(key)
315
+ .catch((e) =>
316
+ reportCacheError(e, "cache-delete", `[CacheScope] ${key}: evict`),
317
+ );
318
+ return null;
319
+ }
320
+
321
+ // A hit serves content that was tagged at write time, so the document
322
+ // tag union must include this entry's tags for updateTag()/revalidateTag()
323
+ // to invalidate any full-page entry built on top of it. The write path
324
+ // records via cacheRoute (resolveCacheTags); the hit path records here.
325
+ recordRequestTags(cached.tags);
247
326
 
248
- // Replay handle data
327
+ // Replay handle data. An empty string means the route pushed no handles —
328
+ // skip the decode entirely (the common case). Otherwise decode the
329
+ // Flight-encoded blob; a decode failure skips handle restore but keeps the
330
+ // valid cached segments.
249
331
  const handleStore = _getRequestContext()?._handleStore;
250
- if (handleStore) {
251
- restoreHandles(cached.handles, handleStore);
332
+ if (handleStore && cached.handles) {
333
+ const handlesRecord = await decodeHandles(cached.handles);
334
+ if (handlesRecord) {
335
+ restoreHandles(handlesRecord, handleStore);
336
+ }
252
337
  }
253
338
 
254
339
  if (INTERNAL_RANGO_DEBUG) {
@@ -262,11 +347,37 @@ export class CacheScope {
262
347
 
263
348
  return { segments, shouldRevalidate };
264
349
  } catch (error) {
265
- console.error(`[CacheScope] Failed to lookup ${key}:`, error);
350
+ // Covers a store.get() failure AND a throwing consumer key()/keyGenerator
351
+ // (resolveKey). Either way degrade to a cache miss so the render proceeds.
352
+ reportCacheError(
353
+ error,
354
+ "cache-read",
355
+ `[CacheScope] lookup ${key ?? "(key resolution failed)"}`,
356
+ );
266
357
  return null;
267
358
  }
268
359
  }
269
360
 
361
+ /**
362
+ * Record this scope's segment-DSL cache({ tags }) into the request tag union
363
+ * synchronously, under the same gate cacheRoute() uses for a write.
364
+ *
365
+ * cacheRoute() already records these tags, but it is invoked inside
366
+ * requestCtx.waitUntil() by the cache-store middleware (and the proactive path
367
+ * re-resolves the whole tree before calling it), so its recording is deferred
368
+ * and RACES the document cache's post-body-drain snapshot of _requestTags. On a
369
+ * first-write (segment-cache miss) the document tag union could miss these
370
+ * tags, and updateTag()/revalidateTag() would then fail to invalidate the
371
+ * cached document until a later write reseeded it. Calling this synchronously
372
+ * in the request pipeline (before the snapshot) closes that window. Idempotent
373
+ * (the tag union is a Set), so the duplicate record in cacheRoute is harmless.
374
+ */
375
+ recordTags(requestCtx: RequestContext | undefined): void {
376
+ if (!this.enabled) return;
377
+ if (!this.conditionAllows("write")) return;
378
+ recordRequestTags(resolveCacheTags(this.config, requestCtx), requestCtx);
379
+ }
380
+
270
381
  /**
271
382
  * Cache all segments for a route (non-blocking via waitUntil)
272
383
  * Single cache entry per route request.
@@ -284,27 +395,7 @@ export class CacheScope {
284
395
  isIntercept?: boolean,
285
396
  ): Promise<void> {
286
397
  if (!this.enabled || segments.length === 0) return;
287
-
288
- // Evaluate condition — skip cache write when condition returns false
289
- if (this.config !== false && this.config.condition) {
290
- const conditionCtx = getRequestContext();
291
- if (conditionCtx) {
292
- try {
293
- if (!this.config.condition(conditionCtx)) {
294
- debugCacheLog(
295
- `[CacheScope] condition returned false, skipping cache write`,
296
- );
297
- return;
298
- }
299
- } catch (error) {
300
- console.error(
301
- `[CacheScope] condition function threw, skipping cache write:`,
302
- error,
303
- );
304
- return;
305
- }
306
- }
307
- }
398
+ if (!this.conditionAllows("write")) return;
308
399
 
309
400
  const store = this.getStore();
310
401
  if (!store) return;
@@ -325,34 +416,86 @@ export class CacheScope {
325
416
  // Resolve cache key early (while request context is available)
326
417
  const key = await this.resolveKey(pathname, params, isIntercept);
327
418
 
419
+ // Resolve tags early (while request context is available, before waitUntil)
420
+ const tags = resolveCacheTags(this.config, requestCtx);
421
+ recordRequestTags(tags, requestCtx);
422
+
328
423
  // Check if this is a partial request (navigation) vs document request
329
- const isPartial = requestCtx.url.searchParams.has("_rsc_partial");
424
+ const isPartial = requestCtx.originalUrl.searchParams.has("_rsc_partial");
425
+
426
+ if (INTERNAL_RANGO_DEBUG) {
427
+ debugCacheLog(
428
+ `[CacheScope] cacheRoute: scheduling waitUntil for ${key} (${nonLoaderSegments.length} segments, isPartial=${isPartial})`,
429
+ );
430
+ }
330
431
 
331
432
  requestCtx.waitUntil(async () => {
433
+ if (INTERNAL_RANGO_DEBUG) {
434
+ debugCacheLog(
435
+ `[CacheScope] waitUntil: awaiting handleStore.settled for ${key}`,
436
+ );
437
+ }
438
+
332
439
  await handleStore.settled;
333
440
 
334
- // For document requests: only cache if ALL segments have components (complete render)
335
- // For partial requests: null components are expected (client already has them)
441
+ if (INTERNAL_RANGO_DEBUG) {
442
+ debugCacheLog(`[CacheScope] waitUntil: handleStore settled for ${key}`);
443
+ }
444
+
445
+ // For document requests: only cache if layout segments have components
446
+ // (complete render). Parallel and route segments may legitimately have
447
+ // null components — UI-less @meta parallels return null, and void route
448
+ // handlers produce null when the UI lives in parallel slots/layouts.
449
+ // Partial requests always allow null components (client already has them).
336
450
  if (!isPartial) {
337
- const hasAllComponents = nonLoaderSegments.every(
338
- (s) => s.component !== null,
451
+ const hasIncompleteLayouts = nonLoaderSegments.some(
452
+ (s) => s.component === null && s.type === "layout",
339
453
  );
340
- if (!hasAllComponents) return;
454
+ if (hasIncompleteLayouts) {
455
+ const nullSegments = nonLoaderSegments
456
+ .filter((s) => s.component === null && s.type === "layout")
457
+ .map((s) => s.id);
458
+ const error = new Error(
459
+ `[CacheScope] Cache write skipped: layout segments have null components ` +
460
+ `(${nullSegments.join(", ")}). This indicates an incomplete render — ` +
461
+ `layout handlers must return JSX for document requests to be cacheable.`,
462
+ );
463
+ error.name = "CacheScopeInvariantError";
464
+ console.error(error.message);
465
+ return;
466
+ }
341
467
  }
342
468
 
343
469
  // Collect handle data for non-loader segments only
344
470
  const handles = captureHandles(nonLoaderSegments, handleStore);
345
471
 
346
472
  try {
347
- // Serialize non-loader segments only
348
- const serializedSegments = await serializeSegments(nonLoaderSegments);
473
+ if (INTERNAL_RANGO_DEBUG) {
474
+ debugCacheLog(
475
+ `[CacheScope] waitUntil: serializing ${nonLoaderSegments.length} segments for ${key}`,
476
+ );
477
+ }
478
+
479
+ // Serialize segments and Flight-encode handles in parallel. Handles go
480
+ // through the codec (not raw into the entry) so Promise/ReactNode handle
481
+ // values survive a JSON-serializing store — see encodeHandles.
482
+ const { serializeSegments } = await import("./segment-codec.js");
483
+ const [serializedSegments, encodedHandles] = await Promise.all([
484
+ serializeSegments(nonLoaderSegments),
485
+ encodeHandles(handles),
486
+ ]);
349
487
 
350
488
  const data: CachedEntryData = {
351
489
  segments: serializedSegments,
352
- handles,
490
+ handles: encodedHandles,
353
491
  expiresAt: Date.now() + ttl * 1000,
492
+ tags,
354
493
  };
355
494
 
495
+ if (INTERNAL_RANGO_DEBUG) {
496
+ debugCacheLog(`[CacheScope] waitUntil: calling store.set for ${key}`);
497
+ }
498
+
356
499
  await store.set(key, data, ttl, swr);
357
500
 
358
501
  if (INTERNAL_RANGO_DEBUG) {
@@ -364,7 +507,11 @@ export class CacheScope {
364
507
  );
365
508
  }
366
509
  } catch (error) {
367
- console.error(`[CacheScope] Failed to cache ${key}:`, error);
510
+ reportCacheError(
511
+ error,
512
+ "cache-write",
513
+ `[CacheScope] Failed to cache ${key}`,
514
+ );
368
515
  }
369
516
  });
370
517
  }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Cache Tag API
3
+ *
4
+ * Provides cacheTag() for tagging cached entries at runtime inside "use cache"
5
+ * functions. Tags are scoped via AsyncLocalStorage; calling cacheTag() outside
6
+ * a "use cache" execution throws.
7
+ *
8
+ * The runtime (cache-runtime.ts) wraps "use cache" execution in
9
+ * runWithCacheTagScope(), collects the runtime tags, and merges them with the
10
+ * profile/DSL tags before storing.
11
+ */
12
+
13
+ import { AsyncLocalStorage } from "node:async_hooks";
14
+ import {
15
+ _getRequestContext,
16
+ type RequestContext,
17
+ } from "../server/request-context.js";
18
+
19
+ const cacheTagStorage = new AsyncLocalStorage<Set<string>>();
20
+
21
+ export function normalizeTag(tag: string): string | null {
22
+ // Trim and return the canonical (trimmed) form, not the raw tag. Both the
23
+ // write path (cacheTag) and the invalidate path (updateTag/revalidateTag)
24
+ // route through here, and matching is exact-string: returning the untrimmed
25
+ // tag made cacheTag(" products ") and updateTag("products") two different
26
+ // logical tags, a silent failure-to-invalidate (stale data served forever).
27
+ const trimmed = tag?.trim();
28
+ return trimmed ? trimmed : null;
29
+ }
30
+
31
+ export function normalizeTags(tags: Iterable<string>): string[] {
32
+ const out: string[] = [];
33
+ for (const tag of tags) {
34
+ const normalized = normalizeTag(tag);
35
+ if (normalized !== null) out.push(normalized);
36
+ }
37
+ return out;
38
+ }
39
+
40
+ /**
41
+ * Tag the current "use cache" entry for later invalidation via
42
+ * updateTag() / revalidateTag().
43
+ *
44
+ * Must be called inside a function marked with "use cache".
45
+ * Tags are additive - multiple calls accumulate.
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * async function getProduct(ctx) {
50
+ * "use cache";
51
+ * cacheTag(`product:${ctx.params.id}`, "products");
52
+ * return db.getProduct(ctx.params.id);
53
+ * }
54
+ * ```
55
+ */
56
+ export function cacheTag(...tags: string[]): void {
57
+ const store = cacheTagStorage.getStore();
58
+ if (!store) {
59
+ throw new Error('cacheTag() must be called inside a "use cache" function.');
60
+ }
61
+ for (const tag of tags) {
62
+ const normalized = normalizeTag(tag);
63
+ if (normalized === null) {
64
+ if (process.env.NODE_ENV !== "production") {
65
+ console.warn(`[cacheTag] Ignoring empty or whitespace-only tag.`);
66
+ }
67
+ continue;
68
+ }
69
+ store.add(normalized);
70
+ }
71
+ }
72
+
73
+ export function recordRequestTags(
74
+ tags: Iterable<string> | undefined,
75
+ ctx: RequestContext | undefined = _getRequestContext(),
76
+ ): void {
77
+ if (!tags) return;
78
+ const set = ctx?._requestTags;
79
+ if (!set) return;
80
+ for (const tag of tags) {
81
+ const normalized = normalizeTag(tag);
82
+ if (normalized !== null) set.add(normalized);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Run a function within a cache tag scope. Any cacheTag() calls inside `fn`
88
+ * accumulate into the returned Set.
89
+ *
90
+ * The returned Set is the LIVE reference - the caller must await `result`
91
+ * before reading `tags`, because an async cached function may call cacheTag()
92
+ * after an await boundary.
93
+ *
94
+ * @internal Used by cache-runtime.ts to wrap "use cache" execution.
95
+ */
96
+ export function runWithCacheTagScope<T>(fn: () => T): {
97
+ result: T;
98
+ tags: Set<string>;
99
+ } {
100
+ const tagSet = new Set<string>();
101
+ const result = cacheTagStorage.run(tagSet, fn);
102
+ return { result, tags: tagSet };
103
+ }
@@ -0,0 +1,33 @@
1
+ // ============================================================================
2
+ // Base64 Helpers (binary-safe response body encoding for KV)
3
+ // ============================================================================
4
+
5
+ // Chunk size for String.fromCharCode.apply: large enough to amortize the call
6
+ // overhead, small enough to stay well under the JS engine argument-count limit
7
+ // (~65k). 8192 turns a per-byte concat loop into O(n/8192) apply calls.
8
+ const FROM_CHARCODE_CHUNK = 8192;
9
+
10
+ /** Encode ArrayBuffer to base64 string. */
11
+ export function bufferToBase64(buffer: ArrayBuffer): string {
12
+ const bytes = new Uint8Array(buffer);
13
+ // Build the binary (latin1) string in fixed-size chunks instead of one
14
+ // String.fromCharCode per byte. Identical output to the per-byte loop (each
15
+ // byte maps to the same code unit); just far fewer string concatenations for
16
+ // large document payloads.
17
+ let binary = "";
18
+ for (let i = 0; i < bytes.length; i += FROM_CHARCODE_CHUNK) {
19
+ const chunk = bytes.subarray(i, i + FROM_CHARCODE_CHUNK);
20
+ binary += String.fromCharCode.apply(null, chunk as unknown as number[]);
21
+ }
22
+ return btoa(binary);
23
+ }
24
+
25
+ /** Decode base64 string to ArrayBuffer. */
26
+ export function base64ToBuffer(base64: string): ArrayBuffer {
27
+ const binary = atob(base64);
28
+ const bytes = new Uint8Array(binary.length);
29
+ for (let i = 0; i < binary.length; i++) {
30
+ bytes[i] = binary.charCodeAt(i);
31
+ }
32
+ return bytes.buffer;
33
+ }