@nifrajs/web 5.5.0 → 5.6.0

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 (457) hide show
  1. package/CHANGELOG.md +162 -0
  2. package/dist/boundary.d.ts +67 -7
  3. package/dist/boundary.d.ts.map +1 -1
  4. package/dist/boundary.js +6 -4
  5. package/dist/boundary.js.map +1 -1
  6. package/dist/build-plan.d.ts +3 -0
  7. package/dist/build-plan.d.ts.map +1 -1
  8. package/dist/build-plan.js.map +1 -1
  9. package/dist/build-vite.d.ts.map +1 -1
  10. package/dist/build-vite.js +2 -0
  11. package/dist/build-vite.js.map +1 -1
  12. package/dist/build.d.ts +9 -2
  13. package/dist/build.d.ts.map +1 -1
  14. package/dist/build.js +9 -0
  15. package/dist/build.js.map +1 -1
  16. package/dist/client.d.ts +2 -1
  17. package/dist/client.d.ts.map +1 -1
  18. package/dist/client.js +140 -12
  19. package/dist/client.js.map +1 -1
  20. package/dist/csp.d.ts +8 -0
  21. package/dist/csp.d.ts.map +1 -1
  22. package/dist/csp.js +11 -2
  23. package/dist/csp.js.map +1 -1
  24. package/dist/deferred.d.ts +6 -4
  25. package/dist/deferred.d.ts.map +1 -1
  26. package/dist/deferred.js +10 -6
  27. package/dist/deferred.js.map +1 -1
  28. package/dist/dev-feed.d.ts.map +1 -1
  29. package/dist/dev-feed.js +11 -2
  30. package/dist/dev-feed.js.map +1 -1
  31. package/dist/dev-session.d.ts.map +1 -1
  32. package/dist/dev-session.js +13 -6
  33. package/dist/dev-session.js.map +1 -1
  34. package/dist/dev.d.ts +4 -1
  35. package/dist/dev.d.ts.map +1 -1
  36. package/dist/dev.js +12 -5
  37. package/dist/dev.js.map +1 -1
  38. package/dist/diagnostic-catalog.d.ts +4 -0
  39. package/dist/diagnostic-catalog.d.ts.map +1 -1
  40. package/dist/diagnostic-catalog.js +48 -0
  41. package/dist/diagnostic-catalog.js.map +1 -1
  42. package/dist/diagnostic.d.ts +1 -1
  43. package/dist/diagnostic.d.ts.map +1 -1
  44. package/dist/diagnostic.js +1 -1
  45. package/dist/diagnostic.js.map +1 -1
  46. package/dist/fn.d.ts +5 -3
  47. package/dist/fn.d.ts.map +1 -1
  48. package/dist/fn.js +20 -5
  49. package/dist/fn.js.map +1 -1
  50. package/dist/fs.d.ts.map +1 -1
  51. package/dist/fs.js +13 -3
  52. package/dist/fs.js.map +1 -1
  53. package/dist/identity.d.ts +22 -0
  54. package/dist/identity.d.ts.map +1 -0
  55. package/dist/identity.js +60 -0
  56. package/dist/identity.js.map +1 -0
  57. package/dist/index.d.ts +7 -6
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +4 -2
  60. package/dist/index.js.map +1 -1
  61. package/dist/internal/cdn/cloudflare.d.ts +2 -1
  62. package/dist/internal/cdn/cloudflare.d.ts.map +1 -1
  63. package/dist/internal/cdn/cloudflare.js +2 -1
  64. package/dist/internal/cdn/cloudflare.js.map +1 -1
  65. package/dist/internal/certified-components.d.ts +1 -1
  66. package/dist/internal/certified-components.js +1 -1
  67. package/dist/internal/codegen.d.ts +11 -10
  68. package/dist/internal/codegen.d.ts.map +1 -1
  69. package/dist/internal/codegen.js +65 -8
  70. package/dist/internal/codegen.js.map +1 -1
  71. package/dist/internal/dev-websocket.d.ts +73 -0
  72. package/dist/internal/dev-websocket.d.ts.map +1 -0
  73. package/dist/internal/dev-websocket.js +145 -0
  74. package/dist/internal/dev-websocket.js.map +1 -0
  75. package/dist/internal/hole-runtime.d.ts +66 -0
  76. package/dist/internal/hole-runtime.d.ts.map +1 -0
  77. package/dist/internal/hole-runtime.js +211 -0
  78. package/dist/internal/hole-runtime.js.map +1 -0
  79. package/dist/internal/identity-epoch.d.ts +50 -0
  80. package/dist/internal/identity-epoch.d.ts.map +1 -0
  81. package/dist/internal/identity-epoch.js +179 -0
  82. package/dist/internal/identity-epoch.js.map +1 -0
  83. package/dist/internal/invalidation-contract.d.ts +23 -0
  84. package/dist/internal/invalidation-contract.d.ts.map +1 -0
  85. package/dist/internal/invalidation-contract.js +11 -0
  86. package/dist/internal/invalidation-contract.js.map +1 -0
  87. package/dist/internal/invalidation.d.ts +62 -0
  88. package/dist/internal/invalidation.d.ts.map +1 -0
  89. package/dist/internal/invalidation.js +149 -0
  90. package/dist/internal/invalidation.js.map +1 -0
  91. package/dist/internal/island-bindings.d.ts +51 -0
  92. package/dist/internal/island-bindings.d.ts.map +1 -0
  93. package/dist/internal/island-bindings.js +147 -0
  94. package/dist/internal/island-bindings.js.map +1 -0
  95. package/dist/internal/island-list.d.ts +26 -0
  96. package/dist/internal/island-list.d.ts.map +1 -0
  97. package/dist/internal/island-list.js +78 -0
  98. package/dist/internal/island-list.js.map +1 -0
  99. package/dist/internal/island-mount.d.ts +4 -0
  100. package/dist/internal/island-mount.d.ts.map +1 -0
  101. package/dist/internal/island-mount.js +66 -0
  102. package/dist/internal/island-mount.js.map +1 -0
  103. package/dist/internal/island-owner.d.ts +5 -0
  104. package/dist/internal/island-owner.d.ts.map +1 -0
  105. package/dist/internal/island-owner.js +20 -0
  106. package/dist/internal/island-owner.js.map +1 -0
  107. package/dist/internal/island-resource.d.ts +30 -0
  108. package/dist/internal/island-resource.d.ts.map +1 -0
  109. package/dist/internal/island-resource.js +57 -0
  110. package/dist/internal/island-resource.js.map +1 -0
  111. package/dist/internal/island-signals.d.ts +51 -0
  112. package/dist/internal/island-signals.d.ts.map +1 -0
  113. package/dist/internal/island-signals.js +220 -0
  114. package/dist/internal/island-signals.js.map +1 -0
  115. package/dist/internal/keep-alive-table.d.ts +23 -0
  116. package/dist/internal/keep-alive-table.d.ts.map +1 -0
  117. package/dist/internal/keep-alive-table.js +53 -0
  118. package/dist/internal/keep-alive-table.js.map +1 -0
  119. package/dist/internal/live-contract.d.ts +96 -0
  120. package/dist/internal/live-contract.d.ts.map +1 -0
  121. package/dist/internal/live-contract.js +5 -0
  122. package/dist/internal/live-contract.js.map +1 -0
  123. package/dist/internal/live-queries.d.ts +22 -0
  124. package/dist/internal/live-queries.d.ts.map +1 -0
  125. package/dist/internal/live-queries.js +54 -0
  126. package/dist/internal/live-queries.js.map +1 -0
  127. package/dist/internal/page-events.d.ts +10 -0
  128. package/dist/internal/page-events.d.ts.map +1 -0
  129. package/dist/internal/page-events.js +26 -0
  130. package/dist/internal/page-events.js.map +1 -0
  131. package/dist/internal/page-execution.d.ts +14 -1
  132. package/dist/internal/page-execution.d.ts.map +1 -1
  133. package/dist/internal/page-execution.js +415 -31
  134. package/dist/internal/page-execution.js.map +1 -1
  135. package/dist/internal/ppr/admission.d.ts +83 -0
  136. package/dist/internal/ppr/admission.d.ts.map +1 -0
  137. package/dist/internal/ppr/admission.js +221 -0
  138. package/dist/internal/ppr/admission.js.map +1 -0
  139. package/dist/internal/ppr/canary.d.ts +24 -0
  140. package/dist/internal/ppr/canary.d.ts.map +1 -0
  141. package/dist/internal/ppr/canary.js +102 -0
  142. package/dist/internal/ppr/canary.js.map +1 -0
  143. package/dist/internal/ppr/compile-shell.d.ts +39 -0
  144. package/dist/internal/ppr/compile-shell.d.ts.map +1 -0
  145. package/dist/internal/ppr/compile-shell.js +298 -0
  146. package/dist/internal/ppr/compile-shell.js.map +1 -0
  147. package/dist/internal/ppr/contract.d.ts +177 -0
  148. package/dist/internal/ppr/contract.d.ts.map +1 -0
  149. package/dist/internal/ppr/contract.js +80 -0
  150. package/dist/internal/ppr/contract.js.map +1 -0
  151. package/dist/internal/ppr/frames.d.ts +71 -0
  152. package/dist/internal/ppr/frames.d.ts.map +1 -0
  153. package/dist/internal/ppr/frames.js +119 -0
  154. package/dist/internal/ppr/frames.js.map +1 -0
  155. package/dist/internal/ppr/integration.d.ts +19 -0
  156. package/dist/internal/ppr/integration.d.ts.map +1 -0
  157. package/dist/internal/ppr/integration.js +106 -0
  158. package/dist/internal/ppr/integration.js.map +1 -0
  159. package/dist/internal/ppr/options.d.ts +96 -0
  160. package/dist/internal/ppr/options.d.ts.map +1 -0
  161. package/dist/internal/ppr/options.js +125 -0
  162. package/dist/internal/ppr/options.js.map +1 -0
  163. package/dist/internal/ppr/route-rules.d.ts +12 -0
  164. package/dist/internal/ppr/route-rules.d.ts.map +1 -0
  165. package/dist/internal/ppr/route-rules.js +53 -0
  166. package/dist/internal/ppr/route-rules.js.map +1 -0
  167. package/dist/internal/ppr/runtime.d.ts +29 -0
  168. package/dist/internal/ppr/runtime.d.ts.map +1 -0
  169. package/dist/internal/ppr/runtime.js +32 -0
  170. package/dist/internal/ppr/runtime.js.map +1 -0
  171. package/dist/internal/ppr/server.d.ts +29 -0
  172. package/dist/internal/ppr/server.d.ts.map +1 -0
  173. package/dist/internal/ppr/server.js +1407 -0
  174. package/dist/internal/ppr/server.js.map +1 -0
  175. package/dist/internal/ppr/shell-store.d.ts +38 -0
  176. package/dist/internal/ppr/shell-store.d.ts.map +1 -0
  177. package/dist/internal/ppr/shell-store.js +90 -0
  178. package/dist/internal/ppr/shell-store.js.map +1 -0
  179. package/dist/internal/prerendered-pages.d.ts +1 -1
  180. package/dist/internal/prerendered-pages.d.ts.map +1 -1
  181. package/dist/internal/prerendered-pages.js +7 -4
  182. package/dist/internal/prerendered-pages.js.map +1 -1
  183. package/dist/internal/query-digest.d.ts +9 -0
  184. package/dist/internal/query-digest.d.ts.map +1 -0
  185. package/dist/internal/query-digest.js +23 -0
  186. package/dist/internal/query-digest.js.map +1 -0
  187. package/dist/internal/query-runtime.d.ts +6 -1
  188. package/dist/internal/query-runtime.d.ts.map +1 -1
  189. package/dist/internal/query-runtime.js +9 -0
  190. package/dist/internal/query-runtime.js.map +1 -1
  191. package/dist/internal/query-seam.d.ts +29 -0
  192. package/dist/internal/query-seam.d.ts.map +1 -0
  193. package/dist/internal/query-seam.js +23 -0
  194. package/dist/internal/query-seam.js.map +1 -0
  195. package/dist/internal/render-document.d.ts +28 -1
  196. package/dist/internal/render-document.d.ts.map +1 -1
  197. package/dist/internal/render-document.js +83 -18
  198. package/dist/internal/render-document.js.map +1 -1
  199. package/dist/internal/request-abort.d.ts +8 -0
  200. package/dist/internal/request-abort.d.ts.map +1 -0
  201. package/dist/internal/request-abort.js +22 -0
  202. package/dist/internal/request-abort.js.map +1 -0
  203. package/dist/internal/server-boundary.d.ts +2 -0
  204. package/dist/internal/server-boundary.d.ts.map +1 -1
  205. package/dist/internal/server-boundary.js +8 -1
  206. package/dist/internal/server-boundary.js.map +1 -1
  207. package/dist/internal/share-data.d.ts +10 -0
  208. package/dist/internal/share-data.d.ts.map +1 -0
  209. package/dist/internal/share-data.js +135 -0
  210. package/dist/internal/share-data.js.map +1 -0
  211. package/dist/internal/store-signature.d.ts +15 -0
  212. package/dist/internal/store-signature.d.ts.map +1 -0
  213. package/dist/internal/store-signature.js +114 -0
  214. package/dist/internal/store-signature.js.map +1 -0
  215. package/dist/internal/vite-plugin-compat.d.ts +2 -2
  216. package/dist/internal/vite-plugin-compat.js +2 -2
  217. package/dist/internal/web-app.d.ts +25 -0
  218. package/dist/internal/web-app.d.ts.map +1 -1
  219. package/dist/internal/web-app.js +78 -3
  220. package/dist/internal/web-app.js.map +1 -1
  221. package/dist/islands-swap.d.ts +41 -0
  222. package/dist/islands-swap.d.ts.map +1 -0
  223. package/dist/islands-swap.js +270 -0
  224. package/dist/islands-swap.js.map +1 -0
  225. package/dist/islands.d.ts +86 -24
  226. package/dist/islands.d.ts.map +1 -1
  227. package/dist/islands.js +138 -78
  228. package/dist/islands.js.map +1 -1
  229. package/dist/isr.d.ts +67 -13
  230. package/dist/isr.d.ts.map +1 -1
  231. package/dist/isr.js +247 -99
  232. package/dist/isr.js.map +1 -1
  233. package/dist/keep-alive.d.ts +147 -0
  234. package/dist/keep-alive.d.ts.map +1 -0
  235. package/dist/keep-alive.js +1069 -0
  236. package/dist/keep-alive.js.map +1 -0
  237. package/dist/live/bus.d.ts +56 -0
  238. package/dist/live/bus.d.ts.map +1 -0
  239. package/dist/live/bus.js +32 -0
  240. package/dist/live/bus.js.map +1 -0
  241. package/dist/live/core.d.ts +25 -0
  242. package/dist/live/core.d.ts.map +1 -0
  243. package/dist/live/core.js +29 -0
  244. package/dist/live/core.js.map +1 -0
  245. package/dist/live/history.d.ts +38 -0
  246. package/dist/live/history.d.ts.map +1 -0
  247. package/dist/live/history.js +104 -0
  248. package/dist/live/history.js.map +1 -0
  249. package/dist/live/hub-protocol.d.ts +46 -0
  250. package/dist/live/hub-protocol.d.ts.map +1 -0
  251. package/dist/live/hub-protocol.js +108 -0
  252. package/dist/live/hub-protocol.js.map +1 -0
  253. package/dist/live/hub.d.ts +181 -0
  254. package/dist/live/hub.d.ts.map +1 -0
  255. package/dist/live/hub.js +539 -0
  256. package/dist/live/hub.js.map +1 -0
  257. package/dist/live/keyring.d.ts +87 -0
  258. package/dist/live/keyring.d.ts.map +1 -0
  259. package/dist/live/keyring.js +260 -0
  260. package/dist/live/keyring.js.map +1 -0
  261. package/dist/live/read-capped.d.ts +6 -0
  262. package/dist/live/read-capped.d.ts.map +1 -0
  263. package/dist/live/read-capped.js +42 -0
  264. package/dist/live/read-capped.js.map +1 -0
  265. package/dist/live/revocations.d.ts +52 -0
  266. package/dist/live/revocations.d.ts.map +1 -0
  267. package/dist/live/revocations.js +116 -0
  268. package/dist/live/revocations.js.map +1 -0
  269. package/dist/live/settings.d.ts +14 -0
  270. package/dist/live/settings.d.ts.map +1 -0
  271. package/dist/live/settings.js +38 -0
  272. package/dist/live/settings.js.map +1 -0
  273. package/dist/live-client.d.ts +34 -0
  274. package/dist/live-client.d.ts.map +1 -0
  275. package/dist/live-client.js +840 -0
  276. package/dist/live-client.js.map +1 -0
  277. package/dist/live-hub.d.ts +130 -0
  278. package/dist/live-hub.d.ts.map +1 -0
  279. package/dist/live-hub.js +507 -0
  280. package/dist/live-hub.js.map +1 -0
  281. package/dist/live-redis.d.ts +48 -0
  282. package/dist/live-redis.d.ts.map +1 -0
  283. package/dist/live-redis.js +201 -0
  284. package/dist/live-redis.js.map +1 -0
  285. package/dist/live.d.ts +94 -0
  286. package/dist/live.d.ts.map +1 -0
  287. package/dist/live.js +869 -0
  288. package/dist/live.js.map +1 -0
  289. package/dist/manifest.d.ts +113 -2
  290. package/dist/manifest.d.ts.map +1 -1
  291. package/dist/manifest.js +109 -5
  292. package/dist/manifest.js.map +1 -1
  293. package/dist/navigation.d.ts +9 -3
  294. package/dist/navigation.d.ts.map +1 -1
  295. package/dist/navigation.js +13 -3
  296. package/dist/navigation.js.map +1 -1
  297. package/dist/plugins/vite-zone-guard.d.ts +6 -0
  298. package/dist/plugins/vite-zone-guard.d.ts.map +1 -1
  299. package/dist/plugins/vite-zone-guard.js +8 -2
  300. package/dist/plugins/vite-zone-guard.js.map +1 -1
  301. package/dist/ppr.d.ts +73 -0
  302. package/dist/ppr.d.ts.map +1 -0
  303. package/dist/ppr.js +76 -0
  304. package/dist/ppr.js.map +1 -0
  305. package/dist/query.d.ts +8 -13
  306. package/dist/query.d.ts.map +1 -1
  307. package/dist/query.js +112 -18
  308. package/dist/query.js.map +1 -1
  309. package/dist/render-seam.d.ts +30 -0
  310. package/dist/render-seam.d.ts.map +1 -1
  311. package/dist/render-seam.js +5 -0
  312. package/dist/render-seam.js.map +1 -1
  313. package/dist/route-types.d.ts.map +1 -1
  314. package/dist/route-types.js +2 -2
  315. package/dist/route-types.js.map +1 -1
  316. package/dist/router.d.ts +72 -4
  317. package/dist/router.d.ts.map +1 -1
  318. package/dist/router.js +374 -99
  319. package/dist/router.js.map +1 -1
  320. package/dist/server-components/build.d.ts.map +1 -1
  321. package/dist/server-components/build.js +3 -3
  322. package/dist/server-components/build.js.map +1 -1
  323. package/dist/server-components/client.d.ts +23 -1
  324. package/dist/server-components/client.d.ts.map +1 -1
  325. package/dist/server-components/client.js +92 -5
  326. package/dist/server-components/client.js.map +1 -1
  327. package/dist/server-components/hole.d.ts +8 -0
  328. package/dist/server-components/hole.d.ts.map +1 -0
  329. package/dist/server-components/hole.js +20 -0
  330. package/dist/server-components/hole.js.map +1 -0
  331. package/dist/server-components/html-policy.d.ts.map +1 -1
  332. package/dist/server-components/html-policy.js +62 -4
  333. package/dist/server-components/html-policy.js.map +1 -1
  334. package/dist/server-components/html-scan.d.ts +17 -0
  335. package/dist/server-components/html-scan.d.ts.map +1 -0
  336. package/dist/server-components/html-scan.js +180 -0
  337. package/dist/server-components/html-scan.js.map +1 -0
  338. package/dist/server-components/html.d.ts.map +1 -1
  339. package/dist/server-components/html.js +19 -4
  340. package/dist/server-components/html.js.map +1 -1
  341. package/dist/server-components/router-integration.d.ts +4 -1
  342. package/dist/server-components/router-integration.d.ts.map +1 -1
  343. package/dist/server-components/router-integration.js +23 -2
  344. package/dist/server-components/router-integration.js.map +1 -1
  345. package/dist/server-components/runtime.d.ts +6 -1
  346. package/dist/server-components/runtime.d.ts.map +1 -1
  347. package/dist/server-components/runtime.js +86 -28
  348. package/dist/server-components/runtime.js.map +1 -1
  349. package/dist/server-components/state.d.ts +3 -1
  350. package/dist/server-components/state.d.ts.map +1 -1
  351. package/dist/server-components/state.js +50 -38
  352. package/dist/server-components/state.js.map +1 -1
  353. package/dist/server-components/types.d.ts +9 -0
  354. package/dist/server-components/types.d.ts.map +1 -1
  355. package/dist/vite.d.ts +5 -1
  356. package/dist/vite.d.ts.map +1 -1
  357. package/dist/vite.js +58 -4
  358. package/dist/vite.js.map +1 -1
  359. package/package.json +48 -8
  360. package/src/boundary.ts +77 -5
  361. package/src/build-plan.ts +3 -0
  362. package/src/build-vite.ts +2 -0
  363. package/src/build.ts +19 -1
  364. package/src/client.ts +147 -10
  365. package/src/csp.ts +16 -2
  366. package/src/deferred.ts +11 -5
  367. package/src/dev-feed.ts +11 -2
  368. package/src/dev-session.ts +22 -11
  369. package/src/dev.ts +29 -8
  370. package/src/diagnostic-catalog.ts +53 -0
  371. package/src/diagnostic.ts +1 -0
  372. package/src/fn.ts +24 -7
  373. package/src/fs.ts +22 -8
  374. package/src/identity.ts +61 -0
  375. package/src/index.ts +19 -0
  376. package/src/internal/cdn/cloudflare.ts +2 -1
  377. package/src/internal/certified-components.ts +1 -1
  378. package/src/internal/codegen.ts +87 -8
  379. package/src/internal/dev-websocket.ts +230 -0
  380. package/src/internal/hole-runtime.ts +286 -0
  381. package/src/internal/identity-epoch.ts +235 -0
  382. package/src/internal/invalidation-contract.ts +35 -0
  383. package/src/internal/invalidation.ts +213 -0
  384. package/src/internal/island-bindings.ts +191 -0
  385. package/src/internal/island-list.ts +92 -0
  386. package/src/internal/island-mount.ts +68 -0
  387. package/src/internal/island-owner.ts +20 -0
  388. package/src/internal/island-resource.ts +76 -0
  389. package/src/internal/island-signals.ts +249 -0
  390. package/src/internal/keep-alive-table.ts +72 -0
  391. package/src/internal/live-contract.ts +106 -0
  392. package/src/internal/live-queries.ts +60 -0
  393. package/src/internal/page-events.ts +27 -0
  394. package/src/internal/page-execution.ts +579 -40
  395. package/src/internal/ppr/admission.ts +300 -0
  396. package/src/internal/ppr/canary.ts +122 -0
  397. package/src/internal/ppr/compile-shell.ts +379 -0
  398. package/src/internal/ppr/contract.ts +265 -0
  399. package/src/internal/ppr/frames.ts +170 -0
  400. package/src/internal/ppr/integration.ts +137 -0
  401. package/src/internal/ppr/options.ts +221 -0
  402. package/src/internal/ppr/route-rules.ts +77 -0
  403. package/src/internal/ppr/runtime.ts +36 -0
  404. package/src/internal/ppr/server.ts +1764 -0
  405. package/src/internal/ppr/shell-store.ts +139 -0
  406. package/src/internal/prerendered-pages.ts +7 -4
  407. package/src/internal/query-digest.ts +34 -0
  408. package/src/internal/query-runtime.ts +14 -1
  409. package/src/internal/query-seam.ts +48 -0
  410. package/src/internal/render-document.ts +149 -22
  411. package/src/internal/request-abort.ts +26 -0
  412. package/src/internal/server-boundary.ts +10 -1
  413. package/src/internal/share-data.ts +136 -0
  414. package/src/internal/store-signature.ts +149 -0
  415. package/src/internal/vite-plugin-compat.ts +2 -2
  416. package/src/internal/web-app.ts +121 -3
  417. package/src/islands-swap.ts +283 -0
  418. package/src/islands.ts +169 -72
  419. package/src/isr.ts +348 -106
  420. package/src/keep-alive.ts +1315 -0
  421. package/src/live/bus.ts +77 -0
  422. package/src/live/core.ts +52 -0
  423. package/src/live/history.ts +109 -0
  424. package/src/live/hub-protocol.ts +133 -0
  425. package/src/live/hub.ts +615 -0
  426. package/src/live/keyring.ts +379 -0
  427. package/src/live/read-capped.ts +36 -0
  428. package/src/live/revocations.ts +155 -0
  429. package/src/live/settings.ts +46 -0
  430. package/src/live-client.ts +973 -0
  431. package/src/live-hub.ts +658 -0
  432. package/src/live-redis.ts +259 -0
  433. package/src/live.ts +1096 -0
  434. package/src/manifest.ts +274 -6
  435. package/src/navigation.ts +14 -2
  436. package/src/plugins/vite-zone-guard.ts +15 -2
  437. package/src/ppr.ts +153 -0
  438. package/src/query.ts +147 -20
  439. package/src/render-seam.ts +35 -0
  440. package/src/route-types.ts +3 -1
  441. package/src/router.ts +494 -101
  442. package/src/server-components/build.ts +9 -3
  443. package/src/server-components/client.ts +89 -5
  444. package/src/server-components/hole.ts +23 -0
  445. package/src/server-components/html-policy.ts +63 -4
  446. package/src/server-components/html-scan.ts +159 -0
  447. package/src/server-components/html.ts +18 -4
  448. package/src/server-components/router-integration.ts +21 -2
  449. package/src/server-components/runtime.ts +112 -30
  450. package/src/server-components/state.ts +66 -44
  451. package/src/server-components/types.ts +16 -0
  452. package/src/vite.ts +75 -6
  453. package/dist/nano.d.ts +0 -125
  454. package/dist/nano.d.ts.map +0 -1
  455. package/dist/nano.js +0 -190
  456. package/dist/nano.js.map +0 -1
  457. package/src/nano.ts +0 -246
package/dist/isr.js CHANGED
@@ -9,7 +9,11 @@
9
9
  import { isAuthenticatedRequest, writableResponse } from "@nifrajs/core/server";
10
10
  import { readBoundedBytes, TransportCodecError } from "@nifrajs/core/transport-codec";
11
11
  import { isDraftEnabled } from "./draft.js";
12
+ import { runInvalidation, settleInvalidation, } from "./internal/invalidation.js";
13
+ import { INVALIDATION } from "./internal/invalidation-contract.js";
14
+ import { isStoreSecret, MIN_STORE_SECRET_LENGTH, storeSigner, } from "./internal/store-signature.js";
12
15
  import { assertTokenSecret, timingSafeEqual } from "./internal/timing-safe-equal.js";
16
+ import { SWAP_HEADER } from "./render-seam.js";
13
17
  const ISR_TAG_PATTERN = /^[A-Za-z][A-Za-z0-9._:/-]{0,127}$/;
14
18
  const MAX_ISR_TAGS = 32;
15
19
  function normalizeTag(tag) {
@@ -73,6 +77,8 @@ export function routeTags(declared, input, routeId) {
73
77
  }
74
78
  return [...kept];
75
79
  }
80
+ // Nothing outside this process can write these, so `withISR` keeps their pages unsigned.
81
+ const inProcessStores = new WeakSet();
76
82
  /**
77
83
  * In-process ISR cache. Refuses to run in production unless explicitly allowed (mirrors the
78
84
  * rate-limit `MemoryStore` - a per-instance cache is unsafe across instances). Bounded **LRU**: a
@@ -100,6 +106,7 @@ export class MemoryCacheStore {
100
106
  if (!Number.isSafeInteger(this.max) || this.max <= 0) {
101
107
  throw new RangeError("[nifra/web] MemoryCacheStore max must be a positive safe integer");
102
108
  }
109
+ inProcessStores.add(this);
103
110
  }
104
111
  get(key) {
105
112
  const row = this.cache.get(key);
@@ -243,26 +250,14 @@ const isCachedResponse = (value) => {
243
250
  return true;
244
251
  };
245
252
  const KV_TAG_EPOCH_PREFIX = "__nifra_isr_tag_v1:";
246
- const KV_PURGED_AT = "__nifraPurgedAt";
247
- /** When a KV page key was purged, from the marker `delete` leaves; `-Infinity` for a page or nothing. */
248
- function kvPurgedAt(raw) {
249
- if (raw === null || !raw.includes(KV_PURGED_AT))
250
- return Number.NEGATIVE_INFINITY;
251
- try {
252
- const parsed = JSON.parse(raw);
253
- if (typeof parsed === "object" && parsed !== null && KV_PURGED_AT in parsed) {
254
- const at = parsed[KV_PURGED_AT];
255
- if (typeof at === "number")
256
- return at;
257
- }
258
- }
259
- catch {
260
- // Not a marker.
261
- }
262
- return Number.NEGATIVE_INFINITY;
263
- }
264
- /** When a tag epoch was written (`<time>:<uuid>`); an epoch from before epochs carried a time is older
265
- * than any render. */
253
+ const KV_PATH_EPOCH_PREFIX = "__nifra_isr_path_v1:";
254
+ const KV_PATH_DIGEST_PREFIX = "__nifra_isr_path_v1#";
255
+ /** Cloudflare KV's key limit, in UTF-8 bytes. */
256
+ const KV_KEY_LIMIT = 512;
257
+ /** How long (seconds) a path epoch outlives the store's `expirationTtl`. */
258
+ const PATH_EPOCH_SLACK = 86_400;
259
+ /** When a tag or path epoch was written (`<time>:<uuid>`); an epoch from before epochs carried a time
260
+ * is older than any render. */
266
261
  function kvEpochTime(epoch) {
267
262
  const at = epoch.indexOf(":");
268
263
  const time = at === -1 ? Number.NaN : Number(epoch.slice(0, at));
@@ -282,7 +277,8 @@ const kvClock = () => {
282
277
  * binding) - the production-grade shared/durable store ISR wants: cached pages and on-demand purges
283
278
  * hold *across* worker instances (unlike the per-instance {@link MemoryCacheStore}). Entries serialize
284
279
  * to JSON; every read is validated before it's trusted (a malformed/version-skewed entry is treated as
285
- * a miss). Construct it in your Workers `fetch` from the binding: `new KVCacheStore(env.ISR_CACHE)`.
280
+ * a miss). Construct it in your Workers `fetch` from the binding: `new KVCacheStore(env.ISR_CACHE)`,
281
+ * and give `withISR` the `storeSecret` it signs these pages with.
286
282
  *
287
283
  * Cloudflare is the default, not a requirement: {@link KVNamespaceLike} is three structural methods,
288
284
  * so a Redis, Deno KV, or Upstash binding satisfies it (pass `minExpirationTtl` for their TTL floor).
@@ -292,6 +288,7 @@ const kvClock = () => {
292
288
  export class KVCacheStore {
293
289
  kv;
294
290
  putOptions;
291
+ pathEpochOptions;
295
292
  constructor(kv, options = {}) {
296
293
  const floor = options.minExpirationTtl ?? 60;
297
294
  if (!Number.isSafeInteger(floor) || floor < 0) {
@@ -312,9 +309,16 @@ export class KVCacheStore {
312
309
  // Precompute the put options object once: omit it entirely when no TTL (KV then never expires).
313
310
  this.putOptions =
314
311
  options.expirationTtl !== undefined ? { expirationTtl: options.expirationTtl } : undefined;
312
+ // An expired path epoch would make pages stored under the one it replaced read as current again.
313
+ // Those were stored before the purge reached their location, so they expire within the TTL plus
314
+ // that delay; without a TTL they never expire, and neither may the epoch.
315
+ this.pathEpochOptions =
316
+ options.expirationTtl !== undefined
317
+ ? { expirationTtl: options.expirationTtl + PATH_EPOCH_SLACK }
318
+ : undefined;
315
319
  }
316
320
  async get(key) {
317
- const raw = await this.kv.get(key);
321
+ const [raw, pathEpoch] = await Promise.all([this.kv.get(key), this.pathEpoch(key)]);
318
322
  if (raw === null)
319
323
  return undefined;
320
324
  let parsed;
@@ -327,47 +331,54 @@ export class KVCacheStore {
327
331
  }
328
332
  if (!isCachedResponse(parsed))
329
333
  return undefined;
330
- const tags = normalizeTags(parsed.tags);
334
+ const { __nifraTagEpochs: epochs, __nifraPathEpoch: storedPathEpoch, ...value } = parsed;
335
+ // A page another location stored before this path's purge reached it keeps the older epoch.
336
+ if ((storedPathEpoch ?? "0") !== pathEpoch)
337
+ return undefined;
338
+ const tags = normalizeTags(value.tags);
331
339
  if (tags.length === 0)
332
- return parsed;
333
- const epochs = parsed.__nifraTagEpochs;
340
+ return value;
334
341
  if (epochs === undefined)
335
342
  return undefined;
336
343
  const current = await Promise.all(tags.map(async (tag) => [tag, (await this.kv.get(this.tagKey(tag))) ?? "0"]));
337
344
  if (current.some(([tag, epoch]) => epochs[tag] !== epoch))
338
345
  return undefined;
339
- const { __nifraTagEpochs: _ignored, ...value } = parsed;
340
346
  return value;
341
347
  }
342
- /** Wall-clock time, so purges on any instance order against it. KV is eventually consistent, so the
343
- * fence holds only for purges this location has already seen. */
348
+ /** Wall-clock time, so purges on any instance order against it. KV is eventually consistent, so
349
+ * `set` refuses only purges this location has already seen; a page stored before a purge reached
350
+ * its location keeps the older epoch and reads as a miss once the purge arrives. */
344
351
  fence() {
345
352
  return kvClock();
346
353
  }
347
354
  async set(key, value) {
348
355
  const { fence, ...entry } = value;
349
- if (fence !== undefined && kvPurgedAt(await this.kv.get(key)) > fence)
350
- return;
351
356
  const tags = normalizeTags(entry.tags);
352
- if (tags.length === 0) {
353
- await this.kv.put(key, JSON.stringify(entry), this.putOptions);
354
- return;
355
- }
356
- const epochs = Object.fromEntries(await Promise.all(tags.map(async (tag) => [tag, (await this.kv.get(this.tagKey(tag))) ?? "0"])));
357
- if (fence !== undefined && Object.values(epochs).some((epoch) => kvEpochTime(epoch) > fence)) {
357
+ const [pathEpoch, ...tagEpochs] = await Promise.all([
358
+ this.pathEpoch(key),
359
+ ...tags.map(async (tag) => (await this.kv.get(this.tagKey(tag))) ?? "0"),
360
+ ]);
361
+ if (fence !== undefined &&
362
+ [pathEpoch, ...tagEpochs].some((epoch) => kvEpochTime(epoch) > fence)) {
358
363
  return;
359
364
  }
360
365
  const stored = {
361
366
  ...entry,
362
- tags,
363
- __nifraTagEpochs: epochs,
367
+ ...(tags.length === 0
368
+ ? {}
369
+ : {
370
+ tags,
371
+ __nifraTagEpochs: Object.fromEntries(tags.map((tag, at) => [tag, tagEpochs[at]])),
372
+ }),
373
+ ...(pathEpoch === "0" ? {} : { __nifraPathEpoch: pathEpoch }),
364
374
  };
365
375
  await this.kv.put(key, JSON.stringify(stored), this.putOptions);
366
376
  }
367
- /** Leaves a marker rather than removing the key, so `set` can refuse a render that began before
368
- * the purge. The marker reads as a miss. */
377
+ /** Moves the path's epoch and leaves the page: a page stored under the old epoch reads as a miss,
378
+ * even one another location stored before this purge reached it, and `set` refuses a render begun
379
+ * before the purge. */
369
380
  async delete(key) {
370
- await this.kv.put(key, JSON.stringify({ [KV_PURGED_AT]: kvClock() }), this.putOptions);
381
+ await this.kv.put(await this.pathKey(key), `${kvClock()}:${crypto.randomUUID()}`, this.pathEpochOptions);
371
382
  }
372
383
  async invalidateTag(tag) {
373
384
  normalizeTag(tag);
@@ -379,6 +390,21 @@ export class KVCacheStore {
379
390
  tagKey(tag) {
380
391
  return `${KV_TAG_EPOCH_PREFIX}${encodeURIComponent(tag)}`;
381
392
  }
393
+ /** The page key under a prefix, or its digest when that would pass KV's key limit. */
394
+ async pathKey(key) {
395
+ const named = `${KV_PATH_EPOCH_PREFIX}${key}`;
396
+ // A UTF-16 unit is at most three UTF-8 bytes, so most keys skip the encode.
397
+ if (named.length * 3 <= KV_KEY_LIMIT)
398
+ return named;
399
+ const encoder = new TextEncoder();
400
+ if (encoder.encode(named).byteLength <= KV_KEY_LIMIT)
401
+ return named;
402
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(key)));
403
+ return `${KV_PATH_DIGEST_PREFIX}${Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
404
+ }
405
+ async pathEpoch(key) {
406
+ return (await this.kv.get(await this.pathKey(key))) ?? "0";
407
+ }
382
408
  }
383
409
  /** Response header marking how an ISR response was served: a cache `hit` (fresh), `stale` (served +
384
410
  * regenerating behind it), or `miss` (rendered now + stored). Useful for debugging + tests. */
@@ -412,6 +438,7 @@ export function withoutCacheChannel(res) {
412
438
  return target;
413
439
  }
414
440
  const STORE_FAILED = Symbol("nifra.isr.store-failed");
441
+ const MAC_HEADER = "x-nifra-isr-mac";
415
442
  const STORE_REPORT_INTERVAL_MS = 60_000;
416
443
  const STORE_REPORT_DETAIL_CHARS = 200;
417
444
  /** The error's own words with the cache key cut out, since a store may name the key it failed on. */
@@ -431,6 +458,68 @@ function storeErrorDetail(error, key) {
431
458
  ? `${redacted.slice(0, STORE_REPORT_DETAIL_CHARS)}...`
432
459
  : redacted;
433
460
  }
461
+ /**
462
+ * What a page's MAC covers: everything a replay serves, plus the key, so a page copied to another key
463
+ * never verifies there. Header names and tags are sorted, since a store need not keep their order.
464
+ */
465
+ function signedParts(key, entry) {
466
+ const names = Object.keys(entry.headers)
467
+ .filter((name) => name !== MAC_HEADER)
468
+ .sort();
469
+ const headers = names.map((name) => [name, entry.headers[name]]);
470
+ const tags = entry.tags === undefined ? [] : [...entry.tags].sort();
471
+ const meta = JSON.stringify([
472
+ 1,
473
+ key,
474
+ entry.status,
475
+ entry.storedAt,
476
+ entry.revalidate,
477
+ headers,
478
+ tags,
479
+ ]);
480
+ return [meta, entry.body];
481
+ }
482
+ async function signed(signer, key, entry) {
483
+ const mac = await signer.sign(signedParts(key, entry));
484
+ return { ...entry, headers: { ...entry.headers, [MAC_HEADER]: mac } };
485
+ }
486
+ async function verified(signer, key, entry) {
487
+ try {
488
+ return await signer.verify(signedParts(key, entry), entry.headers[MAC_HEADER]);
489
+ }
490
+ catch {
491
+ // A store's own malformed record.
492
+ return false;
493
+ }
494
+ }
495
+ function signerFor(store, secret) {
496
+ if (secret !== undefined && !isStoreSecret(secret)) {
497
+ throw new TypeError(`[nifra/web] ISR storeSecret must be a string of at least ${MIN_STORE_SECRET_LENGTH} characters`);
498
+ }
499
+ if (inProcessStores.has(store))
500
+ return undefined;
501
+ if (secret === undefined) {
502
+ throw new TypeError(`[nifra/web] withISR needs storeSecret, at least ${MIN_STORE_SECRET_LENGTH} characters, to sign the pages it keeps in a store other processes can write. Only a MemoryCacheStore runs without one.`);
503
+ }
504
+ return storeSigner(secret);
505
+ }
506
+ // Headers only an action or a data request carries; a stored page is always a plain document GET.
507
+ const ACTION_ONLY_HEADERS = [
508
+ "content-type",
509
+ "content-length",
510
+ "content-encoding",
511
+ "transfer-encoding",
512
+ "x-nifra-data",
513
+ "x-nifra-component",
514
+ SWAP_HEADER,
515
+ ];
516
+ /** The document GET for `path` from the client that sent `req`: what a custom key sees on a read. */
517
+ function documentGet(path, req) {
518
+ const headers = new Headers(req.headers);
519
+ for (const name of ACTION_ONLY_HEADERS)
520
+ headers.delete(name);
521
+ return new Request(originOf(req) + path, { headers });
522
+ }
434
523
  /** `createWebApp`'s marker for how its documents meet a CSP (see `DOCUMENT_POLICY` in ./csp.ts). Read
435
524
  * through the global symbol registry so a cache wrapper never imports the CSP module. */
436
525
  const DOCUMENT_POLICY = Symbol.for("nifra.web.documentPolicy");
@@ -466,6 +555,27 @@ export const urlKeyOf = (query = "bypass") => {
466
555
  };
467
556
  };
468
557
  const requestKeyOf = (urlKey) => (req) => urlKey(new URL(req.url));
558
+ /**
559
+ * The key of a request, and of a path on the request's origin. Under the default URL key a path needs
560
+ * no `Request`; a custom `key` is handed one.
561
+ */
562
+ function cacheKeysOf(options) {
563
+ const custom = options.key;
564
+ if (custom !== undefined) {
565
+ return { request: custom, path: (path, req) => custom(documentGet(path, req)) };
566
+ }
567
+ const urlKey = urlKeyOf(options.query);
568
+ return {
569
+ request: requestKeyOf(urlKey),
570
+ path: (path, req) => urlKey(new URL(originOf(req) + path)),
571
+ };
572
+ }
573
+ /** `req.url` up to its path, without a parse: the path joined to it is parsed as one URL anyway. */
574
+ const originOf = (req) => {
575
+ const url = req.url;
576
+ const end = url.indexOf("/", url.indexOf("//") + 2);
577
+ return end === -1 ? url : url.slice(0, end);
578
+ };
469
579
  function assertRevalidate(value, name) {
470
580
  if (!Number.isFinite(value) || value < 0) {
471
581
  throw new RangeError(`[nifra/web] ${name} must be a finite non-negative number`);
@@ -497,15 +607,14 @@ const cookieNamesWithin = (header, names) => {
497
607
  // the credential. The mark is set on the request the app was handed, which is why this runs after it.
498
608
  const requestCarriesPrivateState = (req) => req.headers.has("authorization") || req.headers.has("cookie") || isAuthenticatedRequest(req);
499
609
  const responseIsExplicitlyPublic = (res) => cacheControlHas(res.headers, ["public"]);
500
- // `x-nifra-data` alone is not a variation the URL key misses: a navigation data request never reads
501
- // or writes the store, so every stored entry is the document.
610
+ // Data and swap requests bypass both cache reads and writes, so these headers cannot vary stored HTML.
502
611
  const responseDeclaresVary = (res) => {
503
612
  const value = res.headers.get("vary");
504
613
  if (value === null)
505
614
  return false;
506
615
  for (const part of value.split(",")) {
507
616
  const name = part.trim().toLowerCase();
508
- if (name !== "" && name !== "x-nifra-data")
617
+ if (name !== "" && name !== "x-nifra-data" && name !== SWAP_HEADER)
509
618
  return true;
510
619
  }
511
620
  return false;
@@ -513,14 +622,14 @@ const responseDeclaresVary = (res) => {
513
622
  /**
514
623
  * Whether `res` may be stored by a shared cache and served to anyone requesting `req`'s URL: a
515
624
  * full-document `GET` 200 `text/html` with no Set-Cookie, no `private`/`no-store`, no `Vary` beyond
516
- * the data header, and, for a request carrying a cookie or Authorization or accepted by an
625
+ * the data/swap headers, and, for a request carrying a cookie or Authorization or accepted by an
517
626
  * authentication stage, an explicit `public`.
518
627
  * The one rule both `withISR` and `@nifrajs/web/cdn` cache by.
519
628
  */
520
629
  export const isCacheablePage = (req, res) => {
521
630
  if (req.method !== "GET")
522
631
  return false;
523
- if (req.headers.get("x-nifra-data") !== null)
632
+ if (req.headers.get("x-nifra-data") !== null || req.headers.has(SWAP_HEADER))
524
633
  return false;
525
634
  if (res.status !== 200)
526
635
  return false;
@@ -620,6 +729,10 @@ const isInvalidationResponse = (req, res) => {
620
729
  export function withISR(app, options) {
621
730
  const { store, now } = options;
622
731
  assertRevalidate(options.revalidate, "revalidate");
732
+ const signer = signerFor(store, options.storeSecret);
733
+ const write = (key, entry) => signer === undefined
734
+ ? store.set(key, entry)
735
+ : signed(signer, key, entry).then((value) => store.set(key, value));
623
736
  const storeTimeoutMs = options.storeTimeoutMs;
624
737
  if (storeTimeoutMs !== undefined && !(Number.isFinite(storeTimeoutMs) && storeTimeoutMs > 0)) {
625
738
  throw new RangeError("[nifra/web] ISR storeTimeoutMs must be a positive finite number");
@@ -643,29 +756,32 @@ export function withISR(app, options) {
643
756
  }
644
757
  };
645
758
  // Runs one store call; a throw, a rejection or a timeout is reported and answered with STORE_FAILED.
646
- const attempt = async (op, key, call) => {
759
+ const withinTimeout = async (op, pending) => {
760
+ if (storeTimeoutMs === undefined)
761
+ return pending;
647
762
  let timer;
763
+ const timeout = new Promise((_, reject) => {
764
+ timer = setTimeout(() => {
765
+ const error = new Error(`ISR store ${op} exceeded storeTimeoutMs (${storeTimeoutMs} ms)`);
766
+ error.name = "TimeoutError";
767
+ reject(error);
768
+ }, storeTimeoutMs);
769
+ });
648
770
  try {
649
- const pending = call();
650
- if (storeTimeoutMs === undefined)
651
- return await pending;
652
- const timeout = new Promise((_, reject) => {
653
- timer = setTimeout(() => {
654
- const error = new Error(`ISR store ${op} exceeded storeTimeoutMs (${storeTimeoutMs} ms)`);
655
- error.name = "TimeoutError";
656
- reject(error);
657
- }, storeTimeoutMs);
658
- });
659
771
  return await Promise.race([pending, timeout]);
660
772
  }
773
+ finally {
774
+ clearTimeout(timer);
775
+ }
776
+ };
777
+ const attempt = async (op, key, call) => {
778
+ try {
779
+ return await withinTimeout(op, call());
780
+ }
661
781
  catch (error) {
662
782
  reportStoreError(op, key, error);
663
783
  return STORE_FAILED;
664
784
  }
665
- finally {
666
- if (timer !== undefined)
667
- clearTimeout(timer);
668
- }
669
785
  };
670
786
  // A store without purge order, or one whose fence failed, stores the render unfenced.
671
787
  const fenceFor = async (key) => {
@@ -675,7 +791,8 @@ export function withISR(app, options) {
675
791
  const taken = await attempt("fence", key, async () => fence.call(store));
676
792
  return taken === STORE_FAILED ? undefined : taken;
677
793
  };
678
- const keyOf = options.key ?? requestKeyOf(urlKeyOf(options.query));
794
+ const keys = cacheKeysOf(options);
795
+ const keyOf = keys.request;
679
796
  const draftSecret = options.draftSecret;
680
797
  const anonymousCookies = new Set(options.anonymousCookies ?? []);
681
798
  // A request whose cookies are all anonymous is judged for storage as if it carried none.
@@ -693,6 +810,34 @@ export function withISR(app, options) {
693
810
  };
694
811
  const regenerating = new Set();
695
812
  openCacheChannel(app);
813
+ // This wrapper owns the app's invalidation pipeline from here on: an action's operation comes back
814
+ // by its request, and every stage runs in order before the action's answer leaves.
815
+ const handoff = Reflect.get(app, INVALIDATION);
816
+ const taken = isHandoff(handoff) ? handoff : undefined;
817
+ // The pipeline's purges are bounded like every other store call; their failures reach its own report.
818
+ const invalidateTag = store.invalidateTag;
819
+ const targets = {
820
+ dataCaches: options.dataCaches,
821
+ store: {
822
+ delete: (key) => withinTimeout("delete", store.delete(key)),
823
+ invalidateTag: invalidateTag === undefined
824
+ ? undefined
825
+ : (tag) => withinTimeout("invalidateTag", invalidateTag.call(store, tag)),
826
+ },
827
+ keyOf: keys.path,
828
+ publish: taken?.publish,
829
+ cdn: options.cdn,
830
+ };
831
+ const passThrough = async (req, platform) => {
832
+ const res = withoutCacheChannel(await app.fetch(req, platform));
833
+ const op = taken?.take(req);
834
+ if (op !== undefined) {
835
+ await settleInvalidation(op, targets, req, platform, {
836
+ onError: options.onInvalidationError,
837
+ });
838
+ }
839
+ return res;
840
+ };
696
841
  // An outer cache layer (`withCdn`) opens this wrapper's own channel: cached responses then carry how
697
842
  // much freshness they have left and their tags, so the CDN never holds a page longer than ISR would.
698
843
  let channelOpen = false;
@@ -756,7 +901,7 @@ export function withISR(app, options) {
756
901
  };
757
902
  // A failed write still answers with the page, and still hands it to concurrent misses: whether it
758
903
  // may be shared was decided above, not by the store.
759
- await attempt("set", key, () => store.set(key, entry));
904
+ await attempt("set", key, () => write(key, entry));
760
905
  stored?.(entry);
761
906
  return fresh(entry);
762
907
  };
@@ -811,7 +956,7 @@ export function withISR(app, options) {
811
956
  ...(tags.length === 0 ? {} : { tags }),
812
957
  fence,
813
958
  };
814
- await attempt("set", key, () => store.set(key, entry));
959
+ await attempt("set", key, () => write(key, entry));
815
960
  }
816
961
  else if (isInvalidationResponse(judged(req), res)) {
817
962
  // A withdrawal is authoritative. Keeping the old entry would make a withdrawn page stay
@@ -832,23 +977,24 @@ export function withISR(app, options) {
832
977
  }
833
978
  };
834
979
  const handler = async (req, platform) => {
835
- // A data-mode soft-nav GET bypasses the cache entirely: entries are full HTML documents keyed
836
- // by URL, and serving one to a loader-data fetch would hand the client HTML where it expects
837
- // the loader payload. (The write path already refuses to cache data-mode responses.)
980
+ // A data-mode soft-nav GET or a slot swap bypasses the cache entirely: entries are full HTML
981
+ // documents keyed by URL, and serving one to either would hand the client a document where it
982
+ // expects the loader payload or one slot. (The write path already refuses both.)
838
983
  const privateRoute = Reflect.get(app, Symbol.for("nifra.web.private-route"));
839
984
  const ownsPrivate = typeof privateRoute === "function" && privateRoute(req) === true;
840
985
  const key = req.method === "GET" &&
841
986
  req.headers.get("x-nifra-data") === null &&
842
987
  !req.headers.has("x-nifra-component") &&
988
+ !req.headers.has(SWAP_HEADER) &&
843
989
  !ownsPrivate
844
990
  ? keyOf(req)
845
991
  : null;
846
992
  if (key === null)
847
- return withoutCacheChannel(await app.fetch(req, platform));
993
+ return passThrough(req, platform);
848
994
  // Draft/preview: an editor (valid signed cookie) always renders fresh and is never cached, so
849
995
  // unpublished content can't leak into the public cache (and the editor isn't served a stale page).
850
996
  if (draftSecret !== undefined && (await isDraftEnabled(req, draftSecret))) {
851
- return withoutCacheChannel(await app.fetch(req, platform));
997
+ return passThrough(req, platform);
852
998
  }
853
999
  // A cookie or Authorization request may be personalized: under the default key it never reads
854
1000
  // the store, and its private render never marks the URL uncacheable for everyone else.
@@ -860,7 +1006,11 @@ export function withISR(app, options) {
860
1006
  uncacheable.delete(key);
861
1007
  }
862
1008
  const read = personal ? undefined : await attempt("get", key, () => store.get(key));
863
- const hit = read === STORE_FAILED ? undefined : read;
1009
+ let hit = read === STORE_FAILED ? undefined : read;
1010
+ if (hit !== undefined && signer !== undefined && !(await verified(signer, key, hit))) {
1011
+ reportStoreError("verify", key, new Error("a stored page failed its signature check"));
1012
+ hit = undefined;
1013
+ }
864
1014
  if (hit !== undefined) {
865
1015
  if (now() - hit.storedAt < hit.revalidate)
866
1016
  return served(hit, "hit");
@@ -882,6 +1032,7 @@ export function withISR(app, options) {
882
1032
  return handler;
883
1033
  }
884
1034
  const jsonError = (status, error) => Response.json({ ok: false, error }, { status });
1035
+ const isHandoff = (value) => typeof value === "object" && value !== null && typeof Reflect.get(value, "take") === "function";
885
1036
  /** Most paths and tags one revalidation request may name, so a leaked token cannot fan out. */
886
1037
  export const MAX_REVALIDATE_PATHS = 100;
887
1038
  export const MAX_REVALIDATE_TAGS = MAX_ISR_TAGS;
@@ -985,47 +1136,44 @@ export function revalidateEndpoint(options) {
985
1136
  if (tags.length > 0 && invalidateTag === undefined) {
986
1137
  return jsonError(501, "tag_invalidation_not_supported");
987
1138
  }
988
- if (tags.length > 0 && options.dataCaches !== undefined) {
989
- try {
990
- for (const cache of options.dataCaches) {
991
- for (const candidate of tags)
992
- await cache.invalidateTag(candidate);
993
- }
994
- }
995
- catch {
996
- return Response.json({ ok: false, error: "data_cache_invalidation_failed" }, { status: 500 });
997
- }
998
- }
999
- for (const key of keys)
1000
- await options.store.delete(key);
1001
- for (const candidate of tags)
1002
- await invalidateTag?.call(options.store, candidate);
1139
+ // The same pipeline an action's `revalidate()` runs: data caches, this store, live, then the CDN.
1140
+ const live = options.live;
1141
+ const report = await runInvalidation({ id: crypto.randomUUID(), paths, tags, keys: [] }, {
1142
+ dataCaches: options.dataCaches,
1143
+ store: options.store,
1144
+ keyOf: (candidate) => keys[paths.indexOf(candidate)] ?? null,
1145
+ publish: live === undefined ? undefined : (names) => live.publish(...names),
1146
+ cdn: options.cdn,
1147
+ }, req, platform);
1148
+ const stageOf = (stage) => report.stages.find((each) => each.stage === stage);
1149
+ if (stageOf("data")?.ok === false)
1150
+ return jsonError(500, "data_cache_invalidation_failed");
1151
+ if (stageOf("pages")?.ok === false)
1152
+ return jsonError(500, "page_store_invalidation_failed");
1003
1153
  const done = path !== null
1004
1154
  ? { revalidated: path }
1005
1155
  : tag !== null
1006
1156
  ? { revalidatedTag: tag }
1007
1157
  : { revalidated: paths, revalidatedTags: tags };
1008
- if (options.cdn === undefined)
1009
- return Response.json(done);
1010
- let outcome;
1011
- try {
1012
- outcome = await options.cdn.purge({ tags, paths }, platform);
1013
- }
1014
- catch {
1015
- outcome = { cdn: "failed", retryable: true, error: "cdn_purge_threw" };
1016
- }
1017
- if (outcome.cdn === "failed") {
1158
+ const published = stageOf("publish");
1159
+ const cdn = stageOf("cdn");
1160
+ if (cdn?.ok === false) {
1018
1161
  const failure = {
1019
1162
  ok: false,
1020
1163
  error: "cdn_purge_failed",
1021
1164
  origin: "done",
1022
- retryable: outcome.retryable,
1165
+ retryable: cdn.retryable,
1023
1166
  };
1024
- if (outcome.error !== undefined)
1025
- failure.reason = outcome.error;
1167
+ if (cdn.error !== undefined && cdn.error !== "cdn_failed")
1168
+ failure.reason = cdn.error;
1026
1169
  return Response.json(failure, { status: 502 });
1027
1170
  }
1028
- return Response.json({ ...done, cdn: outcome.cdn }, { status: outcome.cdn === "queued" ? 202 : 200 });
1171
+ if (published?.ok === false) {
1172
+ return Response.json({ ok: false, error: "live_publish_failed", origin: "done", retryable: true }, { status: 502 });
1173
+ }
1174
+ if (cdn === undefined)
1175
+ return Response.json(done);
1176
+ return Response.json({ ...done, cdn: cdn.queued === true ? "queued" : "accepted" }, { status: cdn.queued === true ? 202 : 200 });
1029
1177
  };
1030
1178
  }
1031
1179
  //# sourceMappingURL=isr.js.map