@nifrajs/web 4.0.2 → 5.0.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 (384) hide show
  1. package/README.md +11 -0
  2. package/dist/build-vite.d.ts +19 -0
  3. package/dist/build-vite.d.ts.map +1 -1
  4. package/dist/build-vite.js +31 -10
  5. package/dist/build-vite.js.map +1 -1
  6. package/dist/build.d.ts +8 -10
  7. package/dist/build.d.ts.map +1 -1
  8. package/dist/build.js +90 -31
  9. package/dist/build.js.map +1 -1
  10. package/dist/cdn.d.ts +8 -0
  11. package/dist/cdn.d.ts.map +1 -1
  12. package/dist/cdn.js +14 -15
  13. package/dist/cdn.js.map +1 -1
  14. package/dist/client.d.ts +1 -1
  15. package/dist/client.d.ts.map +1 -1
  16. package/dist/client.js +25 -7
  17. package/dist/client.js.map +1 -1
  18. package/dist/deferred.d.ts +2 -2
  19. package/dist/deferred.d.ts.map +1 -1
  20. package/dist/deferred.js +28 -7
  21. package/dist/deferred.js.map +1 -1
  22. package/dist/dev-feed.d.ts.map +1 -1
  23. package/dist/dev-feed.js +24 -5
  24. package/dist/dev-feed.js.map +1 -1
  25. package/dist/dev-session.d.ts.map +1 -1
  26. package/dist/dev-session.js +4 -14
  27. package/dist/dev-session.js.map +1 -1
  28. package/dist/dev.d.ts +7 -17
  29. package/dist/dev.d.ts.map +1 -1
  30. package/dist/dev.js +13 -1
  31. package/dist/dev.js.map +1 -1
  32. package/dist/draft.d.ts +8 -5
  33. package/dist/draft.d.ts.map +1 -1
  34. package/dist/draft.js +30 -13
  35. package/dist/draft.js.map +1 -1
  36. package/dist/fonts-google.d.ts.map +1 -1
  37. package/dist/fonts-google.js +7 -0
  38. package/dist/fonts-google.js.map +1 -1
  39. package/dist/index.d.ts +2 -2
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +2 -2
  42. package/dist/index.js.map +1 -1
  43. package/dist/internal/codegen.d.ts +1 -0
  44. package/dist/internal/codegen.d.ts.map +1 -1
  45. package/dist/internal/codegen.js +56 -29
  46. package/dist/internal/codegen.js.map +1 -1
  47. package/dist/internal/disposable-dir.d.ts +8 -0
  48. package/dist/internal/disposable-dir.d.ts.map +1 -0
  49. package/dist/internal/disposable-dir.js +25 -0
  50. package/dist/internal/disposable-dir.js.map +1 -0
  51. package/dist/internal/entry-chunks.d.ts +9 -0
  52. package/dist/internal/entry-chunks.d.ts.map +1 -0
  53. package/dist/internal/entry-chunks.js +15 -0
  54. package/dist/internal/entry-chunks.js.map +1 -0
  55. package/dist/internal/evidence-traversal.d.ts +12 -0
  56. package/dist/internal/evidence-traversal.d.ts.map +1 -0
  57. package/dist/internal/evidence-traversal.js +48 -0
  58. package/dist/internal/evidence-traversal.js.map +1 -0
  59. package/dist/internal/head-merge.d.ts +6 -0
  60. package/dist/internal/head-merge.d.ts.map +1 -1
  61. package/dist/internal/head-merge.js +9 -0
  62. package/dist/internal/head-merge.js.map +1 -1
  63. package/dist/internal/js-string.d.ts +2 -0
  64. package/dist/internal/js-string.d.ts.map +1 -1
  65. package/dist/internal/js-string.js +5 -1
  66. package/dist/internal/js-string.js.map +1 -1
  67. package/dist/internal/mount-shadow.js +1 -1
  68. package/dist/internal/mount-shadow.js.map +1 -1
  69. package/dist/internal/output-guard.d.ts +7 -0
  70. package/dist/internal/output-guard.d.ts.map +1 -1
  71. package/dist/internal/output-guard.js +80 -8
  72. package/dist/internal/output-guard.js.map +1 -1
  73. package/dist/internal/page-execution.d.ts +6 -0
  74. package/dist/internal/page-execution.d.ts.map +1 -1
  75. package/dist/internal/page-execution.js +377 -108
  76. package/dist/internal/page-execution.js.map +1 -1
  77. package/dist/internal/progressive-cookies.d.ts +14 -0
  78. package/dist/internal/progressive-cookies.d.ts.map +1 -0
  79. package/dist/internal/progressive-cookies.js +43 -0
  80. package/dist/internal/progressive-cookies.js.map +1 -0
  81. package/dist/internal/public-env.d.ts +2 -0
  82. package/dist/internal/public-env.d.ts.map +1 -0
  83. package/dist/internal/public-env.js +13 -0
  84. package/dist/internal/public-env.js.map +1 -0
  85. package/dist/internal/render-document.d.ts +1 -0
  86. package/dist/internal/render-document.d.ts.map +1 -1
  87. package/dist/internal/render-document.js +28 -5
  88. package/dist/internal/render-document.js.map +1 -1
  89. package/dist/internal/response-controls.d.ts +2 -0
  90. package/dist/internal/response-controls.d.ts.map +1 -1
  91. package/dist/internal/response-controls.js +26 -16
  92. package/dist/internal/response-controls.js.map +1 -1
  93. package/dist/internal/revive-dates.d.ts +16 -0
  94. package/dist/internal/revive-dates.d.ts.map +1 -0
  95. package/dist/internal/revive-dates.js +32 -0
  96. package/dist/internal/revive-dates.js.map +1 -0
  97. package/dist/internal/route-path.d.ts +8 -0
  98. package/dist/internal/route-path.d.ts.map +1 -0
  99. package/dist/internal/route-path.js +8 -0
  100. package/dist/internal/route-path.js.map +1 -0
  101. package/dist/internal/secret-scan.d.ts.map +1 -1
  102. package/dist/internal/secret-scan.js +88 -20
  103. package/dist/internal/secret-scan.js.map +1 -1
  104. package/dist/internal/server-boundary.d.ts.map +1 -1
  105. package/dist/internal/server-boundary.js +33 -5
  106. package/dist/internal/server-boundary.js.map +1 -1
  107. package/dist/internal/web-app.d.ts +21 -16
  108. package/dist/internal/web-app.d.ts.map +1 -1
  109. package/dist/internal/web-app.js +73 -21
  110. package/dist/internal/web-app.js.map +1 -1
  111. package/dist/internal/wire-date.d.ts +3 -0
  112. package/dist/internal/wire-date.d.ts.map +1 -0
  113. package/dist/internal/wire-date.js +14 -0
  114. package/dist/internal/wire-date.js.map +1 -0
  115. package/dist/internal/zone-graph.d.ts +4 -0
  116. package/dist/internal/zone-graph.d.ts.map +1 -1
  117. package/dist/internal/zone-graph.js +54 -14
  118. package/dist/internal/zone-graph.js.map +1 -1
  119. package/dist/isr.d.ts +58 -2
  120. package/dist/isr.d.ts.map +1 -1
  121. package/dist/isr.js +296 -39
  122. package/dist/isr.js.map +1 -1
  123. package/dist/llms-txt.d.ts +2 -0
  124. package/dist/llms-txt.d.ts.map +1 -1
  125. package/dist/llms-txt.js +29 -3
  126. package/dist/llms-txt.js.map +1 -1
  127. package/dist/manifest.d.ts +28 -4
  128. package/dist/manifest.d.ts.map +1 -1
  129. package/dist/manifest.js +3 -0
  130. package/dist/manifest.js.map +1 -1
  131. package/dist/open-resolved.d.ts +18 -0
  132. package/dist/open-resolved.d.ts.map +1 -0
  133. package/dist/open-resolved.js +62 -0
  134. package/dist/open-resolved.js.map +1 -0
  135. package/dist/plugins/kit.d.ts +1 -0
  136. package/dist/plugins/kit.d.ts.map +1 -1
  137. package/dist/plugins/kit.js +26 -11
  138. package/dist/plugins/kit.js.map +1 -1
  139. package/dist/plugins/vite-leak-guard.d.ts +3 -1
  140. package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
  141. package/dist/plugins/vite-leak-guard.js +4 -0
  142. package/dist/plugins/vite-leak-guard.js.map +1 -1
  143. package/dist/public-dir.d.ts +8 -1
  144. package/dist/public-dir.d.ts.map +1 -1
  145. package/dist/public-dir.js +17 -6
  146. package/dist/public-dir.js.map +1 -1
  147. package/dist/query.d.ts +13 -4
  148. package/dist/query.d.ts.map +1 -1
  149. package/dist/query.js +89 -26
  150. package/dist/query.js.map +1 -1
  151. package/dist/render-seam.d.ts +2 -0
  152. package/dist/render-seam.d.ts.map +1 -1
  153. package/dist/render-seam.js +1 -0
  154. package/dist/render-seam.js.map +1 -1
  155. package/dist/route-types.d.ts.map +1 -1
  156. package/dist/route-types.js +20 -9
  157. package/dist/route-types.js.map +1 -1
  158. package/dist/router.d.ts +9 -2
  159. package/dist/router.d.ts.map +1 -1
  160. package/dist/router.js +39 -46
  161. package/dist/router.js.map +1 -1
  162. package/dist/server-components/build-info.d.ts +3 -0
  163. package/dist/server-components/build-info.d.ts.map +1 -0
  164. package/dist/server-components/build-info.js +7 -0
  165. package/dist/server-components/build-info.js.map +1 -0
  166. package/dist/server-components/build-target.d.ts +12 -0
  167. package/dist/server-components/build-target.d.ts.map +1 -0
  168. package/dist/server-components/build-target.js +153 -0
  169. package/dist/server-components/build-target.js.map +1 -0
  170. package/dist/server-components/build.d.ts +24 -0
  171. package/dist/server-components/build.d.ts.map +1 -0
  172. package/dist/server-components/build.js +366 -0
  173. package/dist/server-components/build.js.map +1 -0
  174. package/dist/server-components/bundle-proof.d.ts +3 -0
  175. package/dist/server-components/bundle-proof.d.ts.map +1 -0
  176. package/dist/server-components/bundle-proof.js +23 -0
  177. package/dist/server-components/bundle-proof.js.map +1 -0
  178. package/dist/server-components/client.d.ts +46 -0
  179. package/dist/server-components/client.d.ts.map +1 -0
  180. package/dist/server-components/client.js +594 -0
  181. package/dist/server-components/client.js.map +1 -0
  182. package/dist/server-components/definition.d.ts +11 -0
  183. package/dist/server-components/definition.d.ts.map +1 -0
  184. package/dist/server-components/definition.js +94 -0
  185. package/dist/server-components/definition.js.map +1 -0
  186. package/dist/server-components/dev-http.d.ts +5 -0
  187. package/dist/server-components/dev-http.d.ts.map +1 -0
  188. package/dist/server-components/dev-http.js +71 -0
  189. package/dist/server-components/dev-http.js.map +1 -0
  190. package/dist/server-components/error.d.ts +9 -0
  191. package/dist/server-components/error.d.ts.map +1 -0
  192. package/dist/server-components/error.js +24 -0
  193. package/dist/server-components/error.js.map +1 -0
  194. package/dist/server-components/frames.d.ts +7 -0
  195. package/dist/server-components/frames.d.ts.map +1 -0
  196. package/dist/server-components/frames.js +169 -0
  197. package/dist/server-components/frames.js.map +1 -0
  198. package/dist/server-components/html-policy.d.ts +6 -0
  199. package/dist/server-components/html-policy.d.ts.map +1 -0
  200. package/dist/server-components/html-policy.js +75 -0
  201. package/dist/server-components/html-policy.js.map +1 -0
  202. package/dist/server-components/html.d.ts +4 -0
  203. package/dist/server-components/html.d.ts.map +1 -0
  204. package/dist/server-components/html.js +67 -0
  205. package/dist/server-components/html.js.map +1 -0
  206. package/dist/server-components/islands.d.ts +44 -0
  207. package/dist/server-components/islands.d.ts.map +1 -0
  208. package/dist/server-components/islands.js +42 -0
  209. package/dist/server-components/islands.js.map +1 -0
  210. package/dist/server-components/native.d.ts +4 -0
  211. package/dist/server-components/native.d.ts.map +1 -0
  212. package/dist/server-components/native.js +81 -0
  213. package/dist/server-components/native.js.map +1 -0
  214. package/dist/server-components/portal.d.ts +6 -0
  215. package/dist/server-components/portal.d.ts.map +1 -0
  216. package/dist/server-components/portal.js +35 -0
  217. package/dist/server-components/portal.js.map +1 -0
  218. package/dist/server-components/props.d.ts +3 -0
  219. package/dist/server-components/props.d.ts.map +1 -0
  220. package/dist/server-components/props.js +14 -0
  221. package/dist/server-components/props.js.map +1 -0
  222. package/dist/server-components/protocol.d.ts +44 -0
  223. package/dist/server-components/protocol.d.ts.map +1 -0
  224. package/dist/server-components/protocol.js +139 -0
  225. package/dist/server-components/protocol.js.map +1 -0
  226. package/dist/server-components/range.d.ts +24 -0
  227. package/dist/server-components/range.d.ts.map +1 -0
  228. package/dist/server-components/range.js +263 -0
  229. package/dist/server-components/range.js.map +1 -0
  230. package/dist/server-components/registry.d.ts +6 -0
  231. package/dist/server-components/registry.d.ts.map +1 -0
  232. package/dist/server-components/registry.js +30 -0
  233. package/dist/server-components/registry.js.map +1 -0
  234. package/dist/server-components/response.d.ts +4 -0
  235. package/dist/server-components/response.d.ts.map +1 -0
  236. package/dist/server-components/response.js +32 -0
  237. package/dist/server-components/response.js.map +1 -0
  238. package/dist/server-components/router-integration.d.ts +9 -0
  239. package/dist/server-components/router-integration.d.ts.map +1 -0
  240. package/dist/server-components/router-integration.js +29 -0
  241. package/dist/server-components/router-integration.js.map +1 -0
  242. package/dist/server-components/runtime.d.ts +14 -0
  243. package/dist/server-components/runtime.d.ts.map +1 -0
  244. package/dist/server-components/runtime.js +276 -0
  245. package/dist/server-components/runtime.js.map +1 -0
  246. package/dist/server-components/scheduler.d.ts +14 -0
  247. package/dist/server-components/scheduler.d.ts.map +1 -0
  248. package/dist/server-components/scheduler.js +74 -0
  249. package/dist/server-components/scheduler.js.map +1 -0
  250. package/dist/server-components/schema.d.ts +4 -0
  251. package/dist/server-components/schema.d.ts.map +1 -0
  252. package/dist/server-components/schema.js +77 -0
  253. package/dist/server-components/schema.js.map +1 -0
  254. package/dist/server-components/server-entry.d.ts +12 -0
  255. package/dist/server-components/server-entry.d.ts.map +1 -0
  256. package/dist/server-components/server-entry.js +90 -0
  257. package/dist/server-components/server-entry.js.map +1 -0
  258. package/dist/server-components/source.d.ts +15 -0
  259. package/dist/server-components/source.d.ts.map +1 -0
  260. package/dist/server-components/source.js +269 -0
  261. package/dist/server-components/source.js.map +1 -0
  262. package/dist/server-components/state.d.ts +4 -0
  263. package/dist/server-components/state.d.ts.map +1 -0
  264. package/dist/server-components/state.js +123 -0
  265. package/dist/server-components/state.js.map +1 -0
  266. package/dist/server-components/stream.d.ts +6 -0
  267. package/dist/server-components/stream.d.ts.map +1 -0
  268. package/dist/server-components/stream.js +70 -0
  269. package/dist/server-components/stream.js.map +1 -0
  270. package/dist/server-components/trusted-html.d.ts +3 -0
  271. package/dist/server-components/trusted-html.d.ts.map +1 -0
  272. package/dist/server-components/trusted-html.js +14 -0
  273. package/dist/server-components/trusted-html.js.map +1 -0
  274. package/dist/server-components/types.d.ts +107 -0
  275. package/dist/server-components/types.d.ts.map +1 -0
  276. package/dist/server-components/types.js +2 -0
  277. package/dist/server-components/types.js.map +1 -0
  278. package/dist/server-components/typescript.d.ts +11 -0
  279. package/dist/server-components/typescript.d.ts.map +1 -0
  280. package/dist/server-components/typescript.js +28 -0
  281. package/dist/server-components/typescript.js.map +1 -0
  282. package/dist/server-components/value.d.ts +5 -0
  283. package/dist/server-components/value.d.ts.map +1 -0
  284. package/dist/server-components/value.js +77 -0
  285. package/dist/server-components/value.js.map +1 -0
  286. package/dist/server-components/vite.d.ts +12 -0
  287. package/dist/server-components/vite.d.ts.map +1 -0
  288. package/dist/server-components/vite.js +197 -0
  289. package/dist/server-components/vite.js.map +1 -0
  290. package/dist/server-components.d.ts +5 -0
  291. package/dist/server-components.d.ts.map +1 -0
  292. package/dist/server-components.js +4 -0
  293. package/dist/server-components.js.map +1 -0
  294. package/dist/service-worker.d.ts +8 -1
  295. package/dist/service-worker.d.ts.map +1 -1
  296. package/dist/service-worker.js +20 -6
  297. package/dist/service-worker.js.map +1 -1
  298. package/dist/vite.d.ts +12 -0
  299. package/dist/vite.d.ts.map +1 -1
  300. package/dist/vite.js +7 -1
  301. package/dist/vite.js.map +1 -1
  302. package/dist/zones.d.ts +40 -0
  303. package/dist/zones.d.ts.map +1 -1
  304. package/dist/zones.js +170 -6
  305. package/dist/zones.js.map +1 -1
  306. package/package.json +97 -4
  307. package/src/build-vite.ts +31 -9
  308. package/src/build.ts +109 -31
  309. package/src/cdn.ts +18 -12
  310. package/src/client.ts +25 -6
  311. package/src/deferred.ts +27 -9
  312. package/src/dev-feed.ts +24 -5
  313. package/src/dev-session.ts +4 -13
  314. package/src/dev.ts +29 -1
  315. package/src/draft.ts +35 -14
  316. package/src/fonts-google.ts +10 -0
  317. package/src/index.ts +3 -2
  318. package/src/internal/codegen.ts +60 -29
  319. package/src/internal/disposable-dir.ts +24 -0
  320. package/src/internal/entry-chunks.ts +18 -0
  321. package/src/internal/evidence-traversal.ts +57 -0
  322. package/src/internal/head-merge.ts +16 -0
  323. package/src/internal/js-string.ts +6 -1
  324. package/src/internal/mount-shadow.ts +1 -1
  325. package/src/internal/output-guard.ts +88 -7
  326. package/src/internal/page-execution.ts +506 -108
  327. package/src/internal/progressive-cookies.ts +60 -0
  328. package/src/internal/public-env.ts +14 -0
  329. package/src/internal/render-document.ts +26 -11
  330. package/src/internal/response-controls.ts +31 -14
  331. package/src/internal/revive-dates.ts +35 -0
  332. package/src/internal/route-path.ts +8 -0
  333. package/src/internal/secret-scan.ts +90 -18
  334. package/src/internal/server-boundary.ts +29 -5
  335. package/src/internal/web-app.ts +116 -37
  336. package/src/internal/wire-date.ts +16 -0
  337. package/src/internal/zone-graph.ts +59 -19
  338. package/src/isr.ts +351 -43
  339. package/src/llms-txt.ts +32 -3
  340. package/src/manifest.ts +28 -4
  341. package/src/open-resolved.ts +64 -0
  342. package/src/plugins/kit.ts +31 -11
  343. package/src/plugins/vite-leak-guard.ts +5 -1
  344. package/src/public-dir.ts +27 -8
  345. package/src/query.ts +107 -26
  346. package/src/render-seam.ts +2 -0
  347. package/src/route-types.ts +34 -7
  348. package/src/router.ts +58 -55
  349. package/src/server-components/build-info.ts +11 -0
  350. package/src/server-components/build-target.ts +194 -0
  351. package/src/server-components/build.ts +419 -0
  352. package/src/server-components/bundle-proof.ts +33 -0
  353. package/src/server-components/client.ts +663 -0
  354. package/src/server-components/definition.ts +125 -0
  355. package/src/server-components/dev-http.ts +75 -0
  356. package/src/server-components/error.ts +24 -0
  357. package/src/server-components/frames.ts +181 -0
  358. package/src/server-components/html-policy.ts +94 -0
  359. package/src/server-components/html.ts +81 -0
  360. package/src/server-components/islands.ts +111 -0
  361. package/src/server-components/native.ts +101 -0
  362. package/src/server-components/portal.ts +42 -0
  363. package/src/server-components/props.ts +19 -0
  364. package/src/server-components/protocol.ts +167 -0
  365. package/src/server-components/range.ts +313 -0
  366. package/src/server-components/registry.ts +34 -0
  367. package/src/server-components/response.ts +44 -0
  368. package/src/server-components/router-integration.ts +40 -0
  369. package/src/server-components/runtime.ts +376 -0
  370. package/src/server-components/scheduler.ts +84 -0
  371. package/src/server-components/schema.ts +69 -0
  372. package/src/server-components/server-entry.ts +98 -0
  373. package/src/server-components/source.ts +324 -0
  374. package/src/server-components/state.ts +143 -0
  375. package/src/server-components/stream.ts +71 -0
  376. package/src/server-components/trusted-html.ts +21 -0
  377. package/src/server-components/types.ts +138 -0
  378. package/src/server-components/typescript.ts +39 -0
  379. package/src/server-components/value.ts +82 -0
  380. package/src/server-components/vite.ts +242 -0
  381. package/src/server-components.ts +32 -0
  382. package/src/service-worker.ts +24 -7
  383. package/src/vite.ts +8 -1
  384. package/src/zones.ts +208 -7
package/dist/isr.js CHANGED
@@ -6,6 +6,7 @@
6
6
  *
7
7
  * This module is the store primitive; the SWR wrapper (`withISR`) builds on it next.
8
8
  */
9
+ import { isAuthenticatedRequest, writableResponse } from "@nifrajs/core/server";
9
10
  import { isDraftEnabled } from "./draft.js";
10
11
  import { assertTokenSecret, timingSafeEqual } from "./internal/timing-safe-equal.js";
11
12
  const ISR_TAG_PATTERN = /^[A-Za-z][A-Za-z0-9._:/-]{0,127}$/;
@@ -81,6 +82,10 @@ export class MemoryCacheStore {
81
82
  cache = new Map();
82
83
  max;
83
84
  tagEpoch = new Map();
85
+ // Purge order: `fence` reads it; each purge advances it and records where, per key and per tag.
86
+ order = 0;
87
+ purgedKeys = new Map();
88
+ purgedTags = new Map();
84
89
  constructor(options = {}) {
85
90
  const isProd = typeof process !== "undefined" && process.env?.NODE_ENV === "production";
86
91
  if (options.allowInProduction !== true && isProd) {
@@ -113,8 +118,13 @@ export class MemoryCacheStore {
113
118
  }
114
119
  return Promise.resolve(value);
115
120
  }
121
+ fence() {
122
+ return this.order;
123
+ }
116
124
  set(key, value) {
117
125
  const tags = normalizeTags(value.tags);
126
+ if (value.fence !== undefined && this.purgedAfter(key, tags, value.fence))
127
+ return Promise.resolve();
118
128
  const tagEpochs = {};
119
129
  for (const tag of tags)
120
130
  tagEpochs[tag] = this.tagEpoch.get(tag) ?? "0";
@@ -131,12 +141,14 @@ export class MemoryCacheStore {
131
141
  }
132
142
  delete(key) {
133
143
  this.cache.delete(key);
144
+ this.markPurged(this.purgedKeys, key);
134
145
  this.pruneTagEpochs();
135
146
  return Promise.resolve();
136
147
  }
137
148
  invalidateTag(tag) {
138
149
  normalizeTag(tag);
139
150
  this.tagEpoch.set(tag, crypto.randomUUID());
151
+ this.markPurged(this.purgedTags, tag);
140
152
  // This store is bounded and local, so eagerly remove matching rows. It keeps invalidation
141
153
  // immediate and lets us discard epoch keys that no retained entry references anymore.
142
154
  for (const [key, row] of this.cache) {
@@ -146,6 +158,22 @@ export class MemoryCacheStore {
146
158
  this.pruneTagEpochs();
147
159
  return Promise.resolve();
148
160
  }
161
+ purgedAfter(key, tags, fence) {
162
+ if ((this.purgedKeys.get(key) ?? 0) > fence)
163
+ return true;
164
+ return tags.some((tag) => (this.purgedTags.get(tag) ?? 0) > fence);
165
+ }
166
+ markPurged(marks, name) {
167
+ marks.delete(name);
168
+ marks.set(name, ++this.order);
169
+ // Bounded like the entries: a mark matters only to a render still in flight.
170
+ while (marks.size > this.max) {
171
+ const oldest = marks.keys().next().value;
172
+ if (oldest === undefined)
173
+ break;
174
+ marks.delete(oldest);
175
+ }
176
+ }
149
177
  pruneTagEpochs() {
150
178
  if (this.tagEpoch.size === 0)
151
179
  return;
@@ -169,12 +197,21 @@ const isCachedResponse = (value) => {
169
197
  if (typeof value !== "object" || value === null)
170
198
  return false;
171
199
  const v = value;
200
+ // Everything a replay hands to `new Response` must be legal there, or a corrupt record would throw
201
+ // on every hit instead of reading as a miss.
172
202
  if (typeof v.body !== "string" ||
173
203
  typeof v.status !== "number" ||
204
+ !Number.isInteger(v.status) ||
205
+ v.status < 200 ||
206
+ v.status > 599 ||
174
207
  typeof v.storedAt !== "number" ||
208
+ !Number.isFinite(v.storedAt) ||
175
209
  typeof v.revalidate !== "number" ||
210
+ !Number.isFinite(v.revalidate) ||
211
+ v.revalidate < 0 ||
176
212
  typeof v.headers !== "object" ||
177
- v.headers === null) {
213
+ v.headers === null ||
214
+ Array.isArray(v.headers)) {
178
215
  return false;
179
216
  }
180
217
  if (v.tags !== undefined) {
@@ -187,13 +224,55 @@ const isCachedResponse = (value) => {
187
224
  return false;
188
225
  }
189
226
  }
190
- for (const headerValue of Object.values(v.headers)) {
227
+ const headers = {};
228
+ for (const [name, headerValue] of Object.entries(v.headers)) {
191
229
  if (typeof headerValue !== "string")
192
230
  return false;
231
+ headers[name] = headerValue;
232
+ }
233
+ try {
234
+ new Headers(headers);
235
+ }
236
+ catch {
237
+ return false;
193
238
  }
194
239
  return true;
195
240
  };
196
241
  const KV_TAG_EPOCH_PREFIX = "__nifra_isr_tag_v1:";
242
+ const KV_PURGED_AT = "__nifraPurgedAt";
243
+ /** When a KV page key was purged, from the marker `delete` leaves; `-Infinity` for a page or nothing. */
244
+ function kvPurgedAt(raw) {
245
+ if (raw === null || !raw.includes(KV_PURGED_AT))
246
+ return Number.NEGATIVE_INFINITY;
247
+ try {
248
+ const parsed = JSON.parse(raw);
249
+ if (typeof parsed === "object" && parsed !== null && KV_PURGED_AT in parsed) {
250
+ const at = parsed[KV_PURGED_AT];
251
+ if (typeof at === "number")
252
+ return at;
253
+ }
254
+ }
255
+ catch {
256
+ // Not a marker.
257
+ }
258
+ return Number.NEGATIVE_INFINITY;
259
+ }
260
+ /** When a tag epoch was written (`<time>:<uuid>`); an epoch from before epochs carried a time is older
261
+ * than any render. */
262
+ function kvEpochTime(epoch) {
263
+ const at = epoch.indexOf(":");
264
+ const time = at === -1 ? Number.NaN : Number(epoch.slice(0, at));
265
+ return Number.isFinite(time) ? time : Number.NEGATIVE_INFINITY;
266
+ }
267
+ let lastKvTime = 0;
268
+ /** Wall-clock ms, strictly increasing per process so a purge orders after a render it follows. Not
269
+ * `timeOrigin + performance.now()`: that clock stops while the host sleeps, so a purge from a process
270
+ * that slept would order before renders other instances began earlier. */
271
+ const kvClock = () => {
272
+ const now = Date.now();
273
+ lastKvTime = now > lastKvTime ? now : lastKvTime + 0.001;
274
+ return lastKvTime;
275
+ };
197
276
  /**
198
277
  * A {@link CacheStore} backed by a **Cloudflare Workers KV** namespace (or any {@link KVNamespaceLike}
199
278
  * binding) - the production-grade shared/durable store ISR wants: cached pages and on-demand purges
@@ -256,29 +335,42 @@ export class KVCacheStore {
256
335
  const { __nifraTagEpochs: _ignored, ...value } = parsed;
257
336
  return value;
258
337
  }
338
+ /** Wall-clock time, so purges on any instance order against it. KV is eventually consistent, so the
339
+ * fence holds only for purges this location has already seen. */
340
+ fence() {
341
+ return kvClock();
342
+ }
259
343
  async set(key, value) {
260
- const tags = normalizeTags(value.tags);
344
+ const { fence, ...entry } = value;
345
+ if (fence !== undefined && kvPurgedAt(await this.kv.get(key)) > fence)
346
+ return;
347
+ const tags = normalizeTags(entry.tags);
261
348
  if (tags.length === 0) {
262
- await this.kv.put(key, JSON.stringify(value), this.putOptions);
349
+ await this.kv.put(key, JSON.stringify(entry), this.putOptions);
263
350
  return;
264
351
  }
265
352
  const epochs = Object.fromEntries(await Promise.all(tags.map(async (tag) => [tag, (await this.kv.get(this.tagKey(tag))) ?? "0"])));
353
+ if (fence !== undefined && Object.values(epochs).some((epoch) => kvEpochTime(epoch) > fence)) {
354
+ return;
355
+ }
266
356
  const stored = {
267
- ...value,
357
+ ...entry,
268
358
  tags,
269
359
  __nifraTagEpochs: epochs,
270
360
  };
271
361
  await this.kv.put(key, JSON.stringify(stored), this.putOptions);
272
362
  }
363
+ /** Leaves a marker rather than removing the key, so `set` can refuse a render that began before
364
+ * the purge. The marker reads as a miss. */
273
365
  async delete(key) {
274
- await this.kv.delete(key);
366
+ await this.kv.put(key, JSON.stringify({ [KV_PURGED_AT]: kvClock() }), this.putOptions);
275
367
  }
276
368
  async invalidateTag(tag) {
277
369
  normalizeTag(tag);
278
370
  // Epoch invalidation avoids KV key enumeration and remains bounded for a tag with millions of
279
371
  // pages. Epoch keys intentionally have no expiration: an expired epoch would resurrect entries
280
- // written before the invalidation when their page key still exists.
281
- await this.kv.put(this.tagKey(tag), crypto.randomUUID());
372
+ // written before the invalidation when their page key still exists. The time orders it for `set`.
373
+ await this.kv.put(this.tagKey(tag), `${kvClock()}:${crypto.randomUUID()}`);
282
374
  }
283
375
  tagKey(tag) {
284
376
  return `${KV_TAG_EPOCH_PREFIX}${encodeURIComponent(tag)}`;
@@ -310,18 +402,30 @@ export function withoutCacheChannel(res) {
310
402
  if (!res.headers.has(ISR_REVALIDATE_HEADER) && !res.headers.has(ISR_REVALIDATE_TAGS_HEADER)) {
311
403
  return res;
312
404
  }
405
+ const target = writableResponse(res);
406
+ target.headers.delete(ISR_REVALIDATE_HEADER);
407
+ target.headers.delete(ISR_REVALIDATE_TAGS_HEADER);
408
+ return target;
409
+ }
410
+ const STORE_FAILED = Symbol("nifra.isr.store-failed");
411
+ const STORE_REPORT_INTERVAL_MS = 60_000;
412
+ const STORE_REPORT_DETAIL_CHARS = 200;
413
+ /** The error's own words with the cache key cut out, since a store may name the key it failed on. */
414
+ function storeErrorDetail(error, key) {
415
+ const text = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
416
+ let path;
313
417
  try {
314
- res.headers.delete(ISR_REVALIDATE_HEADER);
315
- res.headers.delete(ISR_REVALIDATE_TAGS_HEADER);
316
- return res;
418
+ path = new URL(key).pathname;
317
419
  }
318
420
  catch {
319
- // A `fetch()` response has immutable headers: rebuild it around the same body instead.
320
- const headers = new Headers(res.headers);
321
- headers.delete(ISR_REVALIDATE_HEADER);
322
- headers.delete(ISR_REVALIDATE_TAGS_HEADER);
323
- return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
421
+ path = undefined;
324
422
  }
423
+ let redacted = text.split(key).join("<key>");
424
+ if (path !== undefined && path.length > 1)
425
+ redacted = redacted.split(path).join("<key>");
426
+ return redacted.length > STORE_REPORT_DETAIL_CHARS
427
+ ? `${redacted.slice(0, STORE_REPORT_DETAIL_CHARS)}...`
428
+ : redacted;
325
429
  }
326
430
  /** `createWebApp`'s marker for how its documents meet a CSP (see `DOCUMENT_POLICY` in ./csp.ts). Read
327
431
  * through the global symbol registry so a cache wrapper never imports the CSP module. */
@@ -376,7 +480,18 @@ const cacheControlHas = (headers, names) => {
376
480
  return names.some((name) => directives.includes(name));
377
481
  };
378
482
  const hasSetCookie = (headers) => headers.has("set-cookie") || (headers.getSetCookie?.().length ?? 0) > 0;
379
- const requestCarriesPrivateState = (req) => req.headers.has("authorization") || req.headers.has("cookie");
483
+ /** Whether every cookie in a `Cookie` header is named in `names`. */
484
+ const cookieNamesWithin = (header, names) => {
485
+ for (const pair of header.split(";")) {
486
+ const name = pair.split("=", 1)[0]?.trim() ?? "";
487
+ if (name !== "" && !names.has(name))
488
+ return false;
489
+ }
490
+ return true;
491
+ };
492
+ // An authentication stage may read any header, so a request it accepted is private whatever carried
493
+ // the credential. The mark is set on the request the app was handed, which is why this runs after it.
494
+ const requestCarriesPrivateState = (req) => req.headers.has("authorization") || req.headers.has("cookie") || isAuthenticatedRequest(req);
380
495
  const responseIsExplicitlyPublic = (res) => cacheControlHas(res.headers, ["public"]);
381
496
  // `x-nifra-data` alone is not a variation the URL key misses: a navigation data request never reads
382
497
  // or writes the store, so every stored entry is the document.
@@ -394,7 +509,8 @@ const responseDeclaresVary = (res) => {
394
509
  /**
395
510
  * Whether `res` may be stored by a shared cache and served to anyone requesting `req`'s URL: a
396
511
  * full-document `GET` 200 `text/html` with no Set-Cookie, no `private`/`no-store`, no `Vary` beyond
397
- * the data header, and, for a request carrying a cookie or Authorization, an explicit `public`.
512
+ * the data header, and, for a request carrying a cookie or Authorization or accepted by an
513
+ * authentication stage, an explicit `public`.
398
514
  * The one rule both `withISR` and `@nifrajs/web/cdn` cache by.
399
515
  */
400
516
  export const isCacheablePage = (req, res) => {
@@ -471,7 +587,21 @@ function sanitizeCachedHeaders(headers) {
471
587
  }
472
588
  return out;
473
589
  }
474
- const isInvalidationResponse = (res) => res.status === 404 || res.status === 410 || (res.status >= 300 && res.status < 400);
590
+ /**
591
+ * Whether a regeneration's answer withdraws the stored page: it is gone or moved, or, rendered for a
592
+ * request with no cookie or Authorization, refused (401, 403) or declared `private`/`no-store`. A
593
+ * signed-in visitor's own 403 or private copy says nothing about the shared page. A 5xx or 429 keeps
594
+ * serving the stale copy.
595
+ */
596
+ const isInvalidationResponse = (req, res) => {
597
+ if (res.status === 404 || res.status === 410 || (res.status >= 300 && res.status < 400))
598
+ return true;
599
+ if (requestCarriesPrivateState(req))
600
+ return false;
601
+ return (res.status === 401 ||
602
+ res.status === 403 ||
603
+ (res.status >= 200 && res.status < 300 && cacheControlHas(res.headers, ["private", "no-store"])));
604
+ };
475
605
  /**
476
606
  * Wrap a nifra app with **Incremental Static Regeneration**: a cacheable page is served from
477
607
  * {@link CacheStore} when fresh, served **stale while a fresh copy regenerates in the background**
@@ -486,8 +616,77 @@ const isInvalidationResponse = (res) => res.status === 404 || res.status === 410
486
616
  export function withISR(app, options) {
487
617
  const { store, now } = options;
488
618
  assertRevalidate(options.revalidate, "revalidate");
619
+ const storeTimeoutMs = options.storeTimeoutMs;
620
+ if (storeTimeoutMs !== undefined && !(Number.isFinite(storeTimeoutMs) && storeTimeoutMs > 0)) {
621
+ throw new RangeError("[nifra/web] ISR storeTimeoutMs must be a positive finite number");
622
+ }
623
+ const lastReport = new Map();
624
+ const reportStoreError = (op, key, error) => {
625
+ try {
626
+ if (options.onStoreError !== undefined) {
627
+ options.onStoreError(error, { op });
628
+ return;
629
+ }
630
+ const last = lastReport.get(op);
631
+ const at = now();
632
+ if (last !== undefined && at - last < STORE_REPORT_INTERVAL_MS)
633
+ return;
634
+ lastReport.set(op, at);
635
+ console.error(`[nifra/web] ISR store ${op} failed; serving without the cache (${storeErrorDetail(error, key)})`);
636
+ }
637
+ catch {
638
+ // A failing reporter must not turn a served page into an error.
639
+ }
640
+ };
641
+ // Runs one store call; a throw, a rejection or a timeout is reported and answered with STORE_FAILED.
642
+ const attempt = async (op, key, call) => {
643
+ let timer;
644
+ try {
645
+ const pending = call();
646
+ if (storeTimeoutMs === undefined)
647
+ return await pending;
648
+ const timeout = new Promise((_, reject) => {
649
+ timer = setTimeout(() => {
650
+ const error = new Error(`ISR store ${op} exceeded storeTimeoutMs (${storeTimeoutMs} ms)`);
651
+ error.name = "TimeoutError";
652
+ reject(error);
653
+ }, storeTimeoutMs);
654
+ });
655
+ return await Promise.race([pending, timeout]);
656
+ }
657
+ catch (error) {
658
+ reportStoreError(op, key, error);
659
+ return STORE_FAILED;
660
+ }
661
+ finally {
662
+ if (timer !== undefined)
663
+ clearTimeout(timer);
664
+ }
665
+ };
666
+ // A store without purge order, or one whose fence failed, stores the render unfenced.
667
+ const fenceFor = async (key) => {
668
+ const { fence } = store;
669
+ if (fence === undefined)
670
+ return undefined;
671
+ const taken = await attempt("fence", key, async () => fence.call(store));
672
+ return taken === STORE_FAILED ? undefined : taken;
673
+ };
489
674
  const keyOf = options.key ?? requestKeyOf(urlKeyOf(options.query));
490
675
  const draftSecret = options.draftSecret;
676
+ const anonymousCookies = new Set(options.anonymousCookies ?? []);
677
+ // A request whose cookies are all anonymous is judged for storage as if it carried none.
678
+ const judged = (req) => {
679
+ if (anonymousCookies.size === 0 ||
680
+ req.headers.has("authorization") ||
681
+ isAuthenticatedRequest(req))
682
+ return req;
683
+ const cookie = req.headers.get("cookie");
684
+ if (cookie === null || !cookieNamesWithin(cookie, anonymousCookies))
685
+ return req;
686
+ const headers = new Headers(req.headers);
687
+ headers.delete("cookie");
688
+ return new Request(req.url, { method: req.method, headers });
689
+ };
491
690
  const regenerating = new Set();
492
691
  openCacheChannel(app);
493
692
  // An outer cache layer (`withCdn`) opens this wrapper's own channel: cached responses then carry how
@@ -531,10 +730,12 @@ export function withISR(app, options) {
531
730
  const safeSeconds = Number.isFinite(seconds) && seconds >= 0 ? seconds : options.revalidate;
532
731
  return safeSeconds * 1000;
533
732
  };
534
- const render = async (req, platform, key) => {
733
+ const render = async (req, platform, key, remember, stored) => {
734
+ const fence = await fenceFor(key);
535
735
  const res = await app.fetch(req, platform);
536
- if (!isCacheablePage(req, res)) {
537
- rememberUncacheable(key, res);
736
+ if (!isCacheablePage(judged(req), res)) {
737
+ if (remember)
738
+ rememberUncacheable(key, res);
538
739
  return withoutCacheChannel(res);
539
740
  }
540
741
  uncacheable.delete(key);
@@ -547,14 +748,44 @@ export function withISR(app, options) {
547
748
  storedAt: now(),
548
749
  revalidate: ttlMs(res),
549
750
  ...(tags.length === 0 ? {} : { tags }),
751
+ fence,
550
752
  };
551
- await store.set(key, entry);
552
- const fresh = new Response(body, {
553
- status: res.status,
753
+ // A failed write still answers with the page, and still hands it to concurrent misses: whether it
754
+ // may be shared was decided above, not by the store.
755
+ await attempt("set", key, () => store.set(key, entry));
756
+ stored?.(entry);
757
+ return fresh(entry);
758
+ };
759
+ const fresh = (entry) => {
760
+ const res = new Response(entry.body, {
761
+ status: entry.status,
554
762
  headers: { ...entry.headers, [ISR_STATUS_HEADER]: "miss" },
555
763
  });
556
- advertise(fresh.headers, entry.revalidate, tags);
557
- return fresh;
764
+ advertise(res.headers, entry.revalidate, entry.tags ?? []);
765
+ return res;
766
+ };
767
+ // Cold misses in flight per key. Concurrent misses wait for the first one's stored entry instead
768
+ // of each rendering the page; a render that is not stored (private, failed) sends each waiter to
769
+ // render for itself, so a waiter never receives a page that was not safe to share.
770
+ const coldRenders = new Map();
771
+ const renderCold = async (req, platform, key) => {
772
+ const pending = coldRenders.get(key);
773
+ if (pending !== undefined) {
774
+ const entry = await pending;
775
+ return entry === undefined ? render(req, platform, key, true) : fresh(entry);
776
+ }
777
+ const shared = Promise.withResolvers();
778
+ coldRenders.set(key, shared.promise);
779
+ let entry;
780
+ try {
781
+ return await render(req, platform, key, true, (stored) => {
782
+ entry = stored;
783
+ });
784
+ }
785
+ finally {
786
+ coldRenders.delete(key);
787
+ shared.resolve(entry);
788
+ }
558
789
  };
559
790
  // Background regeneration (single-flight per key) - a failed regen keeps the stale entry; the next
560
791
  // stale hit retries. Never throws (it runs detached / under waitUntil).
@@ -563,23 +794,27 @@ export function withISR(app, options) {
563
794
  return;
564
795
  regenerating.add(key);
565
796
  try {
797
+ const fence = await fenceFor(key);
566
798
  const res = await app.fetch(req, platform);
567
- if (isCacheablePage(req, res)) {
799
+ if (isCacheablePage(judged(req), res)) {
568
800
  const tags = tagsOf(res);
569
- await store.set(key, {
801
+ const entry = {
570
802
  body: await res.text(),
571
803
  status: res.status,
572
804
  headers: headersOf(res),
573
805
  storedAt: now(),
574
806
  revalidate: ttlMs(res),
575
807
  ...(tags.length === 0 ? {} : { tags }),
576
- });
808
+ fence,
809
+ };
810
+ await attempt("set", key, () => store.set(key, entry));
577
811
  }
578
- else if (isInvalidationResponse(res)) {
579
- // A successful deletion or redirect is authoritative. Keeping the old entry would make a
580
- // withdrawn page stay publicly visible forever because every stale request would retry the same
581
- // non-cacheable response while continuing to serve the old body.
582
- await store.delete(key);
812
+ else if (isInvalidationResponse(judged(req), res)) {
813
+ // A withdrawal is authoritative. Keeping the old entry would make a withdrawn page stay
814
+ // publicly visible forever, because every stale request would retry the same non-cacheable
815
+ // response while continuing to serve the old body.
816
+ await res.body?.cancel();
817
+ await attempt("delete", key, () => store.delete(key));
583
818
  }
584
819
  else {
585
820
  await res.body?.cancel();
@@ -596,7 +831,14 @@ export function withISR(app, options) {
596
831
  // A data-mode soft-nav GET bypasses the cache entirely: entries are full HTML documents keyed
597
832
  // by URL, and serving one to a loader-data fetch would hand the client HTML where it expects
598
833
  // the loader payload. (The write path already refuses to cache data-mode responses.)
599
- const key = req.method === "GET" && req.headers.get("x-nifra-data") === null ? keyOf(req) : null;
834
+ const privateRoute = Reflect.get(app, Symbol.for("nifra.web.private-route"));
835
+ const ownsPrivate = typeof privateRoute === "function" && privateRoute(req) === true;
836
+ const key = req.method === "GET" &&
837
+ req.headers.get("x-nifra-data") === null &&
838
+ !req.headers.has("x-nifra-component") &&
839
+ !ownsPrivate
840
+ ? keyOf(req)
841
+ : null;
600
842
  if (key === null)
601
843
  return withoutCacheChannel(await app.fetch(req, platform));
602
844
  // Draft/preview: an editor (valid signed cookie) always renders fresh and is never cached, so
@@ -604,13 +846,17 @@ export function withISR(app, options) {
604
846
  if (draftSecret !== undefined && (await isDraftEnabled(req, draftSecret))) {
605
847
  return withoutCacheChannel(await app.fetch(req, platform));
606
848
  }
849
+ // A cookie or Authorization request may be personalized: under the default key it never reads
850
+ // the store, and its private render never marks the URL uncacheable for everyone else.
851
+ const personal = options.key === undefined && requestCarriesPrivateState(judged(req));
607
852
  const until = uncacheable.get(key);
608
853
  if (until !== undefined) {
609
854
  if (now() < until)
610
- return render(req, platform, key);
855
+ return render(req, platform, key, !personal);
611
856
  uncacheable.delete(key);
612
857
  }
613
- const hit = await store.get(key);
858
+ const read = personal ? undefined : await attempt("get", key, () => store.get(key));
859
+ const hit = read === STORE_FAILED ? undefined : read;
614
860
  if (hit !== undefined) {
615
861
  if (now() - hit.storedAt < hit.revalidate)
616
862
  return served(hit, "hit");
@@ -622,7 +868,7 @@ export function withISR(app, options) {
622
868
  void task.catch(() => { });
623
869
  return served(hit, "stale");
624
870
  }
625
- return render(req, platform, key);
871
+ return personal ? render(req, platform, key, false) : renderCold(req, platform, key);
626
872
  };
627
873
  Object.defineProperty(handler, CACHE_CHANNEL, {
628
874
  value: () => {
@@ -723,6 +969,17 @@ export function revalidateEndpoint(options) {
723
969
  if (tags.length > 0 && invalidateTag === undefined) {
724
970
  return jsonError(501, "tag_invalidation_not_supported");
725
971
  }
972
+ if (tags.length > 0 && options.dataCaches !== undefined) {
973
+ try {
974
+ for (const cache of options.dataCaches) {
975
+ for (const candidate of tags)
976
+ await cache.invalidateTag(candidate);
977
+ }
978
+ }
979
+ catch {
980
+ return Response.json({ ok: false, error: "data_cache_invalidation_failed" }, { status: 500 });
981
+ }
982
+ }
726
983
  for (const key of keys)
727
984
  await options.store.delete(key);
728
985
  for (const candidate of tags)