@nifrajs/web 5.5.1 → 5.7.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 (481) hide show
  1. package/CHANGELOG.md +243 -0
  2. package/README.md +21 -11
  3. package/dist/boundary.d.ts +67 -7
  4. package/dist/boundary.d.ts.map +1 -1
  5. package/dist/boundary.js +6 -4
  6. package/dist/boundary.js.map +1 -1
  7. package/dist/build-plan.d.ts +3 -0
  8. package/dist/build-plan.d.ts.map +1 -1
  9. package/dist/build-plan.js.map +1 -1
  10. package/dist/build-vite.d.ts.map +1 -1
  11. package/dist/build-vite.js +2 -0
  12. package/dist/build-vite.js.map +1 -1
  13. package/dist/build.d.ts +9 -2
  14. package/dist/build.d.ts.map +1 -1
  15. package/dist/build.js +10 -0
  16. package/dist/build.js.map +1 -1
  17. package/dist/client.d.ts +3 -1
  18. package/dist/client.d.ts.map +1 -1
  19. package/dist/client.js +174 -23
  20. package/dist/client.js.map +1 -1
  21. package/dist/csp.d.ts +8 -0
  22. package/dist/csp.d.ts.map +1 -1
  23. package/dist/csp.js +11 -2
  24. package/dist/csp.js.map +1 -1
  25. package/dist/deferred.d.ts +6 -4
  26. package/dist/deferred.d.ts.map +1 -1
  27. package/dist/deferred.js +10 -6
  28. package/dist/deferred.js.map +1 -1
  29. package/dist/dev-feed.d.ts.map +1 -1
  30. package/dist/dev-feed.js +11 -2
  31. package/dist/dev-feed.js.map +1 -1
  32. package/dist/dev-session.d.ts.map +1 -1
  33. package/dist/dev-session.js +13 -6
  34. package/dist/dev-session.js.map +1 -1
  35. package/dist/dev.d.ts +4 -1
  36. package/dist/dev.d.ts.map +1 -1
  37. package/dist/dev.js +12 -5
  38. package/dist/dev.js.map +1 -1
  39. package/dist/diagnostic-catalog.d.ts +4 -0
  40. package/dist/diagnostic-catalog.d.ts.map +1 -1
  41. package/dist/diagnostic-catalog.js +48 -0
  42. package/dist/diagnostic-catalog.js.map +1 -1
  43. package/dist/diagnostic.d.ts +1 -1
  44. package/dist/diagnostic.d.ts.map +1 -1
  45. package/dist/diagnostic.js +1 -1
  46. package/dist/diagnostic.js.map +1 -1
  47. package/dist/fn.d.ts +5 -3
  48. package/dist/fn.d.ts.map +1 -1
  49. package/dist/fn.js +20 -5
  50. package/dist/fn.js.map +1 -1
  51. package/dist/fs.d.ts.map +1 -1
  52. package/dist/fs.js +13 -3
  53. package/dist/fs.js.map +1 -1
  54. package/dist/identity.d.ts +22 -0
  55. package/dist/identity.d.ts.map +1 -0
  56. package/dist/identity.js +60 -0
  57. package/dist/identity.js.map +1 -0
  58. package/dist/index.d.ts +8 -6
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +6 -3
  61. package/dist/index.js.map +1 -1
  62. package/dist/internal/cdn/cloudflare.d.ts +2 -1
  63. package/dist/internal/cdn/cloudflare.d.ts.map +1 -1
  64. package/dist/internal/cdn/cloudflare.js +2 -1
  65. package/dist/internal/cdn/cloudflare.js.map +1 -1
  66. package/dist/internal/certified-components.d.ts +1 -1
  67. package/dist/internal/certified-components.js +1 -1
  68. package/dist/internal/codegen.d.ts +11 -10
  69. package/dist/internal/codegen.d.ts.map +1 -1
  70. package/dist/internal/codegen.js +73 -10
  71. package/dist/internal/codegen.js.map +1 -1
  72. package/dist/internal/dev-websocket.d.ts +73 -0
  73. package/dist/internal/dev-websocket.d.ts.map +1 -0
  74. package/dist/internal/dev-websocket.js +145 -0
  75. package/dist/internal/dev-websocket.js.map +1 -0
  76. package/dist/internal/hole-runtime.d.ts +66 -0
  77. package/dist/internal/hole-runtime.d.ts.map +1 -0
  78. package/dist/internal/hole-runtime.js +211 -0
  79. package/dist/internal/hole-runtime.js.map +1 -0
  80. package/dist/internal/identity-epoch.d.ts +50 -0
  81. package/dist/internal/identity-epoch.d.ts.map +1 -0
  82. package/dist/internal/identity-epoch.js +180 -0
  83. package/dist/internal/identity-epoch.js.map +1 -0
  84. package/dist/internal/invalidation-contract.d.ts +23 -0
  85. package/dist/internal/invalidation-contract.d.ts.map +1 -0
  86. package/dist/internal/invalidation-contract.js +11 -0
  87. package/dist/internal/invalidation-contract.js.map +1 -0
  88. package/dist/internal/invalidation.d.ts +62 -0
  89. package/dist/internal/invalidation.d.ts.map +1 -0
  90. package/dist/internal/invalidation.js +149 -0
  91. package/dist/internal/invalidation.js.map +1 -0
  92. package/dist/internal/island-bindings.d.ts +51 -0
  93. package/dist/internal/island-bindings.d.ts.map +1 -0
  94. package/dist/internal/island-bindings.js +147 -0
  95. package/dist/internal/island-bindings.js.map +1 -0
  96. package/dist/internal/island-list.d.ts +26 -0
  97. package/dist/internal/island-list.d.ts.map +1 -0
  98. package/dist/internal/island-list.js +78 -0
  99. package/dist/internal/island-list.js.map +1 -0
  100. package/dist/internal/island-mount.d.ts +4 -0
  101. package/dist/internal/island-mount.d.ts.map +1 -0
  102. package/dist/internal/island-mount.js +66 -0
  103. package/dist/internal/island-mount.js.map +1 -0
  104. package/dist/internal/island-owner.d.ts +5 -0
  105. package/dist/internal/island-owner.d.ts.map +1 -0
  106. package/dist/internal/island-owner.js +20 -0
  107. package/dist/internal/island-owner.js.map +1 -0
  108. package/dist/internal/island-resource.d.ts +30 -0
  109. package/dist/internal/island-resource.d.ts.map +1 -0
  110. package/dist/internal/island-resource.js +57 -0
  111. package/dist/internal/island-resource.js.map +1 -0
  112. package/dist/internal/island-signals.d.ts +51 -0
  113. package/dist/internal/island-signals.d.ts.map +1 -0
  114. package/dist/internal/island-signals.js +220 -0
  115. package/dist/internal/island-signals.js.map +1 -0
  116. package/dist/internal/keep-alive-table.d.ts +23 -0
  117. package/dist/internal/keep-alive-table.d.ts.map +1 -0
  118. package/dist/internal/keep-alive-table.js +53 -0
  119. package/dist/internal/keep-alive-table.js.map +1 -0
  120. package/dist/internal/live-contract.d.ts +101 -0
  121. package/dist/internal/live-contract.d.ts.map +1 -0
  122. package/dist/internal/live-contract.js +5 -0
  123. package/dist/internal/live-contract.js.map +1 -0
  124. package/dist/internal/live-queries.d.ts +22 -0
  125. package/dist/internal/live-queries.d.ts.map +1 -0
  126. package/dist/internal/live-queries.js +54 -0
  127. package/dist/internal/live-queries.js.map +1 -0
  128. package/dist/internal/page-events.d.ts +10 -0
  129. package/dist/internal/page-events.d.ts.map +1 -0
  130. package/dist/internal/page-events.js +26 -0
  131. package/dist/internal/page-events.js.map +1 -0
  132. package/dist/internal/page-execution.d.ts +16 -1
  133. package/dist/internal/page-execution.d.ts.map +1 -1
  134. package/dist/internal/page-execution.js +432 -31
  135. package/dist/internal/page-execution.js.map +1 -1
  136. package/dist/internal/parity.d.ts +3 -0
  137. package/dist/internal/parity.d.ts.map +1 -1
  138. package/dist/internal/parity.js +28 -5
  139. package/dist/internal/parity.js.map +1 -1
  140. package/dist/internal/ppr/admission.d.ts +83 -0
  141. package/dist/internal/ppr/admission.d.ts.map +1 -0
  142. package/dist/internal/ppr/admission.js +221 -0
  143. package/dist/internal/ppr/admission.js.map +1 -0
  144. package/dist/internal/ppr/canary.d.ts +24 -0
  145. package/dist/internal/ppr/canary.d.ts.map +1 -0
  146. package/dist/internal/ppr/canary.js +102 -0
  147. package/dist/internal/ppr/canary.js.map +1 -0
  148. package/dist/internal/ppr/compile-shell.d.ts +39 -0
  149. package/dist/internal/ppr/compile-shell.d.ts.map +1 -0
  150. package/dist/internal/ppr/compile-shell.js +298 -0
  151. package/dist/internal/ppr/compile-shell.js.map +1 -0
  152. package/dist/internal/ppr/contract.d.ts +177 -0
  153. package/dist/internal/ppr/contract.d.ts.map +1 -0
  154. package/dist/internal/ppr/contract.js +80 -0
  155. package/dist/internal/ppr/contract.js.map +1 -0
  156. package/dist/internal/ppr/frames.d.ts +71 -0
  157. package/dist/internal/ppr/frames.d.ts.map +1 -0
  158. package/dist/internal/ppr/frames.js +119 -0
  159. package/dist/internal/ppr/frames.js.map +1 -0
  160. package/dist/internal/ppr/integration.d.ts +19 -0
  161. package/dist/internal/ppr/integration.d.ts.map +1 -0
  162. package/dist/internal/ppr/integration.js +106 -0
  163. package/dist/internal/ppr/integration.js.map +1 -0
  164. package/dist/internal/ppr/options.d.ts +96 -0
  165. package/dist/internal/ppr/options.d.ts.map +1 -0
  166. package/dist/internal/ppr/options.js +125 -0
  167. package/dist/internal/ppr/options.js.map +1 -0
  168. package/dist/internal/ppr/route-rules.d.ts +12 -0
  169. package/dist/internal/ppr/route-rules.d.ts.map +1 -0
  170. package/dist/internal/ppr/route-rules.js +53 -0
  171. package/dist/internal/ppr/route-rules.js.map +1 -0
  172. package/dist/internal/ppr/runtime.d.ts +29 -0
  173. package/dist/internal/ppr/runtime.d.ts.map +1 -0
  174. package/dist/internal/ppr/runtime.js +35 -0
  175. package/dist/internal/ppr/runtime.js.map +1 -0
  176. package/dist/internal/ppr/server.d.ts +29 -0
  177. package/dist/internal/ppr/server.d.ts.map +1 -0
  178. package/dist/internal/ppr/server.js +1409 -0
  179. package/dist/internal/ppr/server.js.map +1 -0
  180. package/dist/internal/ppr/shell-store.d.ts +39 -0
  181. package/dist/internal/ppr/shell-store.d.ts.map +1 -0
  182. package/dist/internal/ppr/shell-store.js +106 -0
  183. package/dist/internal/ppr/shell-store.js.map +1 -0
  184. package/dist/internal/prerendered-pages.d.ts +1 -1
  185. package/dist/internal/prerendered-pages.d.ts.map +1 -1
  186. package/dist/internal/prerendered-pages.js +7 -4
  187. package/dist/internal/prerendered-pages.js.map +1 -1
  188. package/dist/internal/present.d.ts +11 -0
  189. package/dist/internal/present.d.ts.map +1 -0
  190. package/dist/internal/present.js +13 -0
  191. package/dist/internal/present.js.map +1 -0
  192. package/dist/internal/query-digest.d.ts +9 -0
  193. package/dist/internal/query-digest.d.ts.map +1 -0
  194. package/dist/internal/query-digest.js +23 -0
  195. package/dist/internal/query-digest.js.map +1 -0
  196. package/dist/internal/query-runtime.d.ts +6 -1
  197. package/dist/internal/query-runtime.d.ts.map +1 -1
  198. package/dist/internal/query-runtime.js +9 -0
  199. package/dist/internal/query-runtime.js.map +1 -1
  200. package/dist/internal/query-seam.d.ts +29 -0
  201. package/dist/internal/query-seam.d.ts.map +1 -0
  202. package/dist/internal/query-seam.js +23 -0
  203. package/dist/internal/query-seam.js.map +1 -0
  204. package/dist/internal/render-document.d.ts +34 -64
  205. package/dist/internal/render-document.d.ts.map +1 -1
  206. package/dist/internal/render-document.js +82 -64
  207. package/dist/internal/render-document.js.map +1 -1
  208. package/dist/internal/request-abort.d.ts +8 -0
  209. package/dist/internal/request-abort.d.ts.map +1 -0
  210. package/dist/internal/request-abort.js +22 -0
  211. package/dist/internal/request-abort.js.map +1 -0
  212. package/dist/internal/server-boundary.d.ts +2 -0
  213. package/dist/internal/server-boundary.d.ts.map +1 -1
  214. package/dist/internal/server-boundary.js +8 -1
  215. package/dist/internal/server-boundary.js.map +1 -1
  216. package/dist/internal/share-data.d.ts +10 -0
  217. package/dist/internal/share-data.d.ts.map +1 -0
  218. package/dist/internal/share-data.js +135 -0
  219. package/dist/internal/share-data.js.map +1 -0
  220. package/dist/internal/status-page.d.ts +68 -0
  221. package/dist/internal/status-page.d.ts.map +1 -0
  222. package/dist/internal/status-page.js +66 -0
  223. package/dist/internal/status-page.js.map +1 -0
  224. package/dist/internal/store-signature.d.ts +15 -0
  225. package/dist/internal/store-signature.d.ts.map +1 -0
  226. package/dist/internal/store-signature.js +114 -0
  227. package/dist/internal/store-signature.js.map +1 -0
  228. package/dist/internal/vite-plugin-compat.d.ts +2 -2
  229. package/dist/internal/vite-plugin-compat.js +2 -2
  230. package/dist/internal/web-app.d.ts +26 -0
  231. package/dist/internal/web-app.d.ts.map +1 -1
  232. package/dist/internal/web-app.js +79 -3
  233. package/dist/internal/web-app.js.map +1 -1
  234. package/dist/islands-swap.d.ts +41 -0
  235. package/dist/islands-swap.d.ts.map +1 -0
  236. package/dist/islands-swap.js +270 -0
  237. package/dist/islands-swap.js.map +1 -0
  238. package/dist/islands.d.ts +86 -24
  239. package/dist/islands.d.ts.map +1 -1
  240. package/dist/islands.js +138 -78
  241. package/dist/islands.js.map +1 -1
  242. package/dist/isr.d.ts +67 -13
  243. package/dist/isr.d.ts.map +1 -1
  244. package/dist/isr.js +247 -99
  245. package/dist/isr.js.map +1 -1
  246. package/dist/keep-alive.d.ts +147 -0
  247. package/dist/keep-alive.d.ts.map +1 -0
  248. package/dist/keep-alive.js +1102 -0
  249. package/dist/keep-alive.js.map +1 -0
  250. package/dist/live/bus.d.ts +70 -0
  251. package/dist/live/bus.d.ts.map +1 -0
  252. package/dist/live/bus.js +55 -0
  253. package/dist/live/bus.js.map +1 -0
  254. package/dist/live/core.d.ts +25 -0
  255. package/dist/live/core.d.ts.map +1 -0
  256. package/dist/live/core.js +29 -0
  257. package/dist/live/core.js.map +1 -0
  258. package/dist/live/history.d.ts +38 -0
  259. package/dist/live/history.d.ts.map +1 -0
  260. package/dist/live/history.js +104 -0
  261. package/dist/live/history.js.map +1 -0
  262. package/dist/live/hub-protocol.d.ts +46 -0
  263. package/dist/live/hub-protocol.d.ts.map +1 -0
  264. package/dist/live/hub-protocol.js +108 -0
  265. package/dist/live/hub-protocol.js.map +1 -0
  266. package/dist/live/hub.d.ts +185 -0
  267. package/dist/live/hub.d.ts.map +1 -0
  268. package/dist/live/hub.js +544 -0
  269. package/dist/live/hub.js.map +1 -0
  270. package/dist/live/keyring.d.ts +92 -0
  271. package/dist/live/keyring.d.ts.map +1 -0
  272. package/dist/live/keyring.js +265 -0
  273. package/dist/live/keyring.js.map +1 -0
  274. package/dist/live/read-capped.d.ts +6 -0
  275. package/dist/live/read-capped.d.ts.map +1 -0
  276. package/dist/live/read-capped.js +42 -0
  277. package/dist/live/read-capped.js.map +1 -0
  278. package/dist/live/revocations.d.ts +52 -0
  279. package/dist/live/revocations.d.ts.map +1 -0
  280. package/dist/live/revocations.js +116 -0
  281. package/dist/live/revocations.js.map +1 -0
  282. package/dist/live/settings.d.ts +14 -0
  283. package/dist/live/settings.d.ts.map +1 -0
  284. package/dist/live/settings.js +38 -0
  285. package/dist/live/settings.js.map +1 -0
  286. package/dist/live-client.d.ts +34 -0
  287. package/dist/live-client.d.ts.map +1 -0
  288. package/dist/live-client.js +878 -0
  289. package/dist/live-client.js.map +1 -0
  290. package/dist/live-hub.d.ts +130 -0
  291. package/dist/live-hub.d.ts.map +1 -0
  292. package/dist/live-hub.js +507 -0
  293. package/dist/live-hub.js.map +1 -0
  294. package/dist/live-redis.d.ts +60 -0
  295. package/dist/live-redis.d.ts.map +1 -0
  296. package/dist/live-redis.js +212 -0
  297. package/dist/live-redis.js.map +1 -0
  298. package/dist/live.d.ts +150 -0
  299. package/dist/live.d.ts.map +1 -0
  300. package/dist/live.js +972 -0
  301. package/dist/live.js.map +1 -0
  302. package/dist/manifest.d.ts +113 -2
  303. package/dist/manifest.d.ts.map +1 -1
  304. package/dist/manifest.js +109 -5
  305. package/dist/manifest.js.map +1 -1
  306. package/dist/navigation.d.ts +9 -3
  307. package/dist/navigation.d.ts.map +1 -1
  308. package/dist/navigation.js +13 -3
  309. package/dist/navigation.js.map +1 -1
  310. package/dist/plugins/vite-zone-guard.d.ts +6 -0
  311. package/dist/plugins/vite-zone-guard.d.ts.map +1 -1
  312. package/dist/plugins/vite-zone-guard.js +8 -2
  313. package/dist/plugins/vite-zone-guard.js.map +1 -1
  314. package/dist/ppr.d.ts +73 -0
  315. package/dist/ppr.d.ts.map +1 -0
  316. package/dist/ppr.js +76 -0
  317. package/dist/ppr.js.map +1 -0
  318. package/dist/query.d.ts +8 -13
  319. package/dist/query.d.ts.map +1 -1
  320. package/dist/query.js +112 -18
  321. package/dist/query.js.map +1 -1
  322. package/dist/render-seam.d.ts +32 -0
  323. package/dist/render-seam.d.ts.map +1 -1
  324. package/dist/render-seam.js +7 -0
  325. package/dist/render-seam.js.map +1 -1
  326. package/dist/route-types.d.ts.map +1 -1
  327. package/dist/route-types.js +2 -2
  328. package/dist/route-types.js.map +1 -1
  329. package/dist/router.d.ts +72 -4
  330. package/dist/router.d.ts.map +1 -1
  331. package/dist/router.js +416 -129
  332. package/dist/router.js.map +1 -1
  333. package/dist/server-components/build.d.ts.map +1 -1
  334. package/dist/server-components/build.js +3 -3
  335. package/dist/server-components/build.js.map +1 -1
  336. package/dist/server-components/client.d.ts +23 -1
  337. package/dist/server-components/client.d.ts.map +1 -1
  338. package/dist/server-components/client.js +92 -5
  339. package/dist/server-components/client.js.map +1 -1
  340. package/dist/server-components/definition.d.ts.map +1 -1
  341. package/dist/server-components/definition.js +2 -1
  342. package/dist/server-components/definition.js.map +1 -1
  343. package/dist/server-components/hole.d.ts +8 -0
  344. package/dist/server-components/hole.d.ts.map +1 -0
  345. package/dist/server-components/hole.js +20 -0
  346. package/dist/server-components/hole.js.map +1 -0
  347. package/dist/server-components/html-policy.d.ts.map +1 -1
  348. package/dist/server-components/html-policy.js +62 -4
  349. package/dist/server-components/html-policy.js.map +1 -1
  350. package/dist/server-components/html-scan.d.ts +17 -0
  351. package/dist/server-components/html-scan.d.ts.map +1 -0
  352. package/dist/server-components/html-scan.js +180 -0
  353. package/dist/server-components/html-scan.js.map +1 -0
  354. package/dist/server-components/html.d.ts.map +1 -1
  355. package/dist/server-components/html.js +19 -4
  356. package/dist/server-components/html.js.map +1 -1
  357. package/dist/server-components/router-integration.d.ts +4 -1
  358. package/dist/server-components/router-integration.d.ts.map +1 -1
  359. package/dist/server-components/router-integration.js +23 -2
  360. package/dist/server-components/router-integration.js.map +1 -1
  361. package/dist/server-components/runtime.d.ts +6 -1
  362. package/dist/server-components/runtime.d.ts.map +1 -1
  363. package/dist/server-components/runtime.js +88 -29
  364. package/dist/server-components/runtime.js.map +1 -1
  365. package/dist/server-components/state.d.ts +3 -1
  366. package/dist/server-components/state.d.ts.map +1 -1
  367. package/dist/server-components/state.js +50 -38
  368. package/dist/server-components/state.js.map +1 -1
  369. package/dist/server-components/types.d.ts +9 -0
  370. package/dist/server-components/types.d.ts.map +1 -1
  371. package/dist/server-components/vite.d.ts.map +1 -1
  372. package/dist/server-components/vite.js +26 -9
  373. package/dist/server-components/vite.js.map +1 -1
  374. package/dist/vite.d.ts +5 -1
  375. package/dist/vite.d.ts.map +1 -1
  376. package/dist/vite.js +58 -4
  377. package/dist/vite.js.map +1 -1
  378. package/package.json +48 -8
  379. package/src/boundary.ts +77 -5
  380. package/src/build-plan.ts +3 -0
  381. package/src/build-vite.ts +2 -0
  382. package/src/build.ts +20 -1
  383. package/src/client.ts +183 -23
  384. package/src/csp.ts +16 -2
  385. package/src/deferred.ts +11 -5
  386. package/src/dev-feed.ts +11 -2
  387. package/src/dev-session.ts +22 -11
  388. package/src/dev.ts +29 -8
  389. package/src/diagnostic-catalog.ts +53 -0
  390. package/src/diagnostic.ts +1 -0
  391. package/src/fn.ts +24 -7
  392. package/src/fs.ts +22 -8
  393. package/src/identity.ts +61 -0
  394. package/src/index.ts +20 -4
  395. package/src/internal/cdn/cloudflare.ts +2 -1
  396. package/src/internal/certified-components.ts +1 -1
  397. package/src/internal/codegen.ts +95 -9
  398. package/src/internal/dev-websocket.ts +230 -0
  399. package/src/internal/hole-runtime.ts +286 -0
  400. package/src/internal/identity-epoch.ts +239 -0
  401. package/src/internal/invalidation-contract.ts +35 -0
  402. package/src/internal/invalidation.ts +213 -0
  403. package/src/internal/island-bindings.ts +191 -0
  404. package/src/internal/island-list.ts +92 -0
  405. package/src/internal/island-mount.ts +68 -0
  406. package/src/internal/island-owner.ts +20 -0
  407. package/src/internal/island-resource.ts +76 -0
  408. package/src/internal/island-signals.ts +249 -0
  409. package/src/internal/keep-alive-table.ts +72 -0
  410. package/src/internal/live-contract.ts +111 -0
  411. package/src/internal/live-queries.ts +60 -0
  412. package/src/internal/page-events.ts +27 -0
  413. package/src/internal/page-execution.ts +605 -45
  414. package/src/internal/parity.ts +35 -6
  415. package/src/internal/ppr/admission.ts +300 -0
  416. package/src/internal/ppr/canary.ts +122 -0
  417. package/src/internal/ppr/compile-shell.ts +379 -0
  418. package/src/internal/ppr/contract.ts +265 -0
  419. package/src/internal/ppr/frames.ts +170 -0
  420. package/src/internal/ppr/integration.ts +137 -0
  421. package/src/internal/ppr/options.ts +221 -0
  422. package/src/internal/ppr/route-rules.ts +77 -0
  423. package/src/internal/ppr/runtime.ts +39 -0
  424. package/src/internal/ppr/server.ts +1766 -0
  425. package/src/internal/ppr/shell-store.ts +152 -0
  426. package/src/internal/prerendered-pages.ts +7 -4
  427. package/src/internal/present.ts +20 -0
  428. package/src/internal/query-digest.ts +34 -0
  429. package/src/internal/query-runtime.ts +14 -1
  430. package/src/internal/query-seam.ts +48 -0
  431. package/src/internal/render-document.ts +158 -113
  432. package/src/internal/request-abort.ts +26 -0
  433. package/src/internal/server-boundary.ts +10 -1
  434. package/src/internal/share-data.ts +136 -0
  435. package/src/internal/status-page.ts +95 -0
  436. package/src/internal/store-signature.ts +149 -0
  437. package/src/internal/vite-plugin-compat.ts +2 -2
  438. package/src/internal/web-app.ts +123 -3
  439. package/src/islands-swap.ts +283 -0
  440. package/src/islands.ts +169 -72
  441. package/src/isr.ts +348 -106
  442. package/src/keep-alive.ts +1354 -0
  443. package/src/live/bus.ts +104 -0
  444. package/src/live/core.ts +52 -0
  445. package/src/live/history.ts +109 -0
  446. package/src/live/hub-protocol.ts +133 -0
  447. package/src/live/hub.ts +620 -0
  448. package/src/live/keyring.ts +384 -0
  449. package/src/live/read-capped.ts +36 -0
  450. package/src/live/revocations.ts +155 -0
  451. package/src/live/settings.ts +46 -0
  452. package/src/live-client.ts +1011 -0
  453. package/src/live-hub.ts +658 -0
  454. package/src/live-redis.ts +284 -0
  455. package/src/live.ts +1257 -0
  456. package/src/manifest.ts +274 -6
  457. package/src/navigation.ts +14 -2
  458. package/src/plugins/vite-zone-guard.ts +15 -2
  459. package/src/ppr.ts +153 -0
  460. package/src/query.ts +147 -20
  461. package/src/render-seam.ts +37 -0
  462. package/src/route-types.ts +3 -1
  463. package/src/router.ts +534 -132
  464. package/src/server-components/build.ts +9 -3
  465. package/src/server-components/client.ts +89 -5
  466. package/src/server-components/definition.ts +2 -1
  467. package/src/server-components/hole.ts +23 -0
  468. package/src/server-components/html-policy.ts +63 -4
  469. package/src/server-components/html-scan.ts +159 -0
  470. package/src/server-components/html.ts +18 -4
  471. package/src/server-components/router-integration.ts +21 -2
  472. package/src/server-components/runtime.ts +114 -31
  473. package/src/server-components/state.ts +66 -44
  474. package/src/server-components/types.ts +16 -0
  475. package/src/server-components/vite.ts +33 -14
  476. package/src/vite.ts +75 -6
  477. package/dist/nano.d.ts +0 -125
  478. package/dist/nano.d.ts.map +0 -1
  479. package/dist/nano.js +0 -190
  480. package/dist/nano.js.map +0 -1
  481. package/src/nano.ts +0 -246
@@ -0,0 +1,286 @@
1
+ /**
2
+ * The browser side of a partially prerendered page's holes, shared by every adapter's `Hole`: the
3
+ * host's first fill from the document, and each refill a data request brings when the page on screen
4
+ * reloads. An adapter supplies only how an interactive hole's own root hydrates, updates and goes. A
5
+ * hole that shows a server component mounts the client islands inside it through the component
6
+ * client the app registered with `holeComponents` (`@nifrajs/web/server-components/client`).
7
+ */
8
+ import type { ComponentClientAdapter, ComponentClientRoot } from "../server-components/client.ts"
9
+ import type { ComponentRecord } from "../server-components/types.ts"
10
+ import { onPageRelease, PAGE_HIDE_EVENT, PAGE_SHOW_EVENT } from "./page-events.ts"
11
+
12
+ /** One hole's fill, as the PPR runtime keeps it. */
13
+ export interface HoleFill {
14
+ readonly props?: unknown
15
+ readonly error: boolean
16
+ /** The record of the server component the hole shows, without its HTML. */
17
+ readonly component?: unknown
18
+ /** That component's HTML. */
19
+ readonly html?: string
20
+ }
21
+
22
+ /** The shell's inline runtime (internal/ppr/runtime.ts). */
23
+ interface PprRuntime {
24
+ get(name: string): HoleFill | undefined
25
+ subscribe(name: string, callback: (fill: HoleFill) => void): () => void
26
+ put(name: string, frame: unknown, host: Element): void
27
+ /** Stop filling holes from the document, and cancel the ones still loading on the server. */
28
+ leave?(): void
29
+ }
30
+
31
+ /** The error slot a failed, timed-out or refused hole shows. Constant: no thrown message reaches it. */
32
+ export const HOLE_ERROR_HTML = '<p data-nifra-hole-error="">This section is unavailable.</p>'
33
+
34
+ const ERROR_FRAME = { v: 1, error: true, html: HOLE_ERROR_HTML } as const
35
+
36
+ /** The runtime the shell's inline script defines, when this document has one. */
37
+ const pprRuntime = (): PprRuntime | undefined => {
38
+ const runtime: unknown = Reflect.get(globalThis, "__nifraPpr")
39
+ return typeof runtime === "function" &&
40
+ "get" in runtime &&
41
+ "subscribe" in runtime &&
42
+ "put" in runtime
43
+ ? // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: the shell's constant inline runtime defines exactly this shape.
44
+ (runtime as PprRuntime)
45
+ : undefined
46
+ }
47
+
48
+ /** How the islands inside a hole that shows a server component mount: the app's component client,
49
+ * and the protocol check the component's record passes first. */
50
+ export interface HoleComponents {
51
+ readonly client: ComponentClientAdapter
52
+ record(value: unknown): ComponentRecord
53
+ }
54
+
55
+ let components: HoleComponents | undefined
56
+
57
+ /** What `holeComponents` registers; `undefined` forgets it. */
58
+ export function setHoleComponents(next: HoleComponents | undefined): void {
59
+ components = next
60
+ }
61
+
62
+ /** An interactive hole's own root, as its adapter made it. */
63
+ export interface HoleRoot {
64
+ /** Render new props into the root, keeping its state; `false` when it has to be made again. */
65
+ update(props: unknown): boolean
66
+ /** `leaving`: the page is unmounting the host, so the adapter may defer the work. A promise means
67
+ * the host is not free until it settles. */
68
+ dispose(leaving: boolean): void | Promise<void>
69
+ /** The kept page holding the host is hidden (`true`) or shown again: a root whose framework can
70
+ * pause what it runs keeps its state either way. */
71
+ hide?(hidden: boolean): void
72
+ }
73
+
74
+ export interface HoleHost {
75
+ /** Apply a data request's fill once it arrives, unless a newer one is given first. Without the
76
+ * shell's runtime, only a server component's fill or the error slot can be put. A first fill the
77
+ * host was rendered with (`initialHtml`) hydrates its islands over that HTML. */
78
+ refill(fill: unknown): void
79
+ dispose(): void
80
+ /**
81
+ * For an adapter whose hidden page cleans its effects up (React's `Activity`): while a kept page
82
+ * hides the host, keep it, root and all, for the page's return, and dispose it only if the page is
83
+ * dropped. `false` when nothing hides the host, and disposing is the adapter's.
84
+ */
85
+ park(): boolean
86
+ /** The parked host is the adapter's again. */
87
+ unpark(): void
88
+ }
89
+
90
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
91
+ typeof value === "object" && value !== null
92
+
93
+ /** A fill the server deferred: the NDJSON stream settles its promise. */
94
+ const settledFill = (fill: unknown): Promise<unknown> =>
95
+ isRecord(fill) && fill.__nifra_deferred === true && fill.promise instanceof Promise
96
+ ? fill.promise
97
+ : Promise.resolve(fill)
98
+
99
+ /**
100
+ * What a host renders with: a blocking render's settled fill puts the server's HTML in it, which a
101
+ * crawler reads with no script; any other state leaves it empty. A host reads it once, so no later
102
+ * render changes what the runtime put there.
103
+ */
104
+ export const initialHtml = (fill: unknown): string =>
105
+ isRecord(fill) && fill.__nifra_deferred !== true && typeof fill.html === "string" ? fill.html : ""
106
+
107
+ /** The record of the server component a fill shows, its HTML put back, not yet checked. */
108
+ const recordOf = (fill: Record<string, unknown>): Record<string, unknown> | undefined =>
109
+ isRecord(fill.component) && typeof fill.html === "string"
110
+ ? { ...fill.component, html: fill.html }
111
+ : undefined
112
+
113
+ /** The error slot, built without HTML parsing, for a document with no runtime to put it. */
114
+ const errorSlot = (host: Element): void => {
115
+ const slot = host.ownerDocument.createElement("p")
116
+ slot.setAttribute("data-nifra-hole-error", "")
117
+ slot.textContent = "This section is unavailable."
118
+ host.replaceChildren(slot)
119
+ host.setAttribute("data-nifra-hole-error", "")
120
+ }
121
+
122
+ /**
123
+ * The islands of the server component in `host`: hydrated over the server's HTML, or with `fresh`,
124
+ * put into the host from the record. `failed` runs when they cannot mount, as when the record fails
125
+ * the protocol check.
126
+ */
127
+ function componentRoot(
128
+ host: Element,
129
+ unchecked: Record<string, unknown>,
130
+ fresh: boolean,
131
+ failed: () => void,
132
+ ): HoleRoot {
133
+ const registered = components
134
+ let current = true
135
+ const valid = (): boolean => current
136
+ const mounted: Promise<ComponentClientRoot | undefined> = (async () => {
137
+ if (registered === undefined) {
138
+ throw new Error(
139
+ "[nifra/web] ppr: a hole shows a server component, but no holeComponents client",
140
+ )
141
+ }
142
+ const { client } = registered
143
+ const record = registered.record(unchecked)
144
+ const mount = fresh ? client.create : client.hydrate
145
+ if (mount === undefined) throw new Error("[nifra/web] ppr: the component client cannot create")
146
+ const prepared = await client.prepare(record, valid, host, { records: [record] })
147
+ if (!current) return undefined
148
+ const root = await mount.call(client, host, record, prepared, valid)
149
+ if (!current) {
150
+ await root.dispose()
151
+ return undefined
152
+ }
153
+ host.setAttribute("aria-busy", "false")
154
+ return root
155
+ })().catch((error: unknown) => {
156
+ if (current) {
157
+ console.error("[nifra/web] ppr: a server component hole's islands did not mount", error)
158
+ failed()
159
+ }
160
+ return undefined
161
+ })
162
+ return {
163
+ update: () => false,
164
+ dispose() {
165
+ current = false
166
+ return mounted.then((root) => root?.dispose())
167
+ },
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Drive the host of hole `name`. `hydrate` makes an interactive hole's root over the server HTML in
173
+ * `host`; a hole without one only ever shows server HTML, or a server component's.
174
+ */
175
+ export function holeHost(
176
+ name: string,
177
+ host: Element,
178
+ hydrate: ((host: Element, props: unknown) => HoleRoot) | undefined,
179
+ ): HoleHost {
180
+ const runtime = pprRuntime()
181
+ let root: HoleRoot | undefined
182
+ let disposed = false
183
+ let latest = 0
184
+ // The kept page hiding the host, and what disposes the host if that page is dropped.
185
+ let hiddenBy: Element | undefined
186
+ let release: (() => void) | undefined
187
+ const put = (frame: Record<string, unknown>): void => {
188
+ if (runtime !== undefined) runtime.put(name, frame, host)
189
+ else errorSlot(host)
190
+ host.setAttribute("aria-busy", "false")
191
+ }
192
+ const showError = (): void => {
193
+ root = undefined
194
+ put(ERROR_FRAME)
195
+ }
196
+ const start = (fill: HoleFill): void => {
197
+ if (disposed || root !== undefined || fill.error) return
198
+ const record = recordOf({ component: fill.component, html: fill.html })
199
+ if (record !== undefined) root = componentRoot(host, record, false, showError)
200
+ else if (hydrate !== undefined) root = hydrate(host, fill.props)
201
+ }
202
+ const now = runtime?.get(name)
203
+ const unsubscribe =
204
+ now === undefined && (hydrate !== undefined || components !== undefined)
205
+ ? runtime?.subscribe(name, start)
206
+ : undefined
207
+ if (now !== undefined) start(now)
208
+ // A `keepAlive` page hidden behind another has been left: the document's holes stop filling and
209
+ // the ones still loading are cancelled, and a refill already on its way is dropped. The page's
210
+ // reload when it is shown again refills the host.
211
+ const hidden = (event: Event): void => {
212
+ if (!(event.target instanceof Element) || !event.target.contains(host)) return
213
+ hiddenBy = event.target
214
+ latest++
215
+ runtime?.leave?.()
216
+ root?.hide?.(true)
217
+ }
218
+ const shown = (event: Event): void => {
219
+ if (event.target !== hiddenBy) return
220
+ hiddenBy = undefined
221
+ root?.hide?.(false)
222
+ }
223
+ host.ownerDocument.addEventListener(PAGE_HIDE_EVENT, hidden)
224
+ host.ownerDocument.addEventListener(PAGE_SHOW_EVENT, shown)
225
+ const apply = (frame: unknown, mine: number, inPlace: boolean): void => {
226
+ if (!isRecord(frame)) return
227
+ const failed = frame.error === true
228
+ const record = failed ? undefined : recordOf(frame)
229
+ if (!failed && record === undefined && root?.update(frame.props) === true) {
230
+ host.setAttribute("aria-busy", "false")
231
+ return
232
+ }
233
+ // An error shows the error slot, never content the action that reloaded it may have changed.
234
+ const gone = root?.dispose(false)
235
+ root = undefined
236
+ const fill = (): void => {
237
+ if (disposed || mine !== latest) return
238
+ host.removeAttribute("data-nifra-hole-error")
239
+ if (record !== undefined) {
240
+ host.setAttribute("data-nifra-filled", "")
241
+ root = componentRoot(host, record, !inPlace, showError)
242
+ return
243
+ }
244
+ put(frame)
245
+ if (!failed && hydrate !== undefined) root = hydrate(host, frame.props)
246
+ }
247
+ if (gone instanceof Promise) gone.then(fill, fill)
248
+ else fill()
249
+ }
250
+ const dispose = (): void => {
251
+ disposed = true
252
+ release?.()
253
+ release = undefined
254
+ host.ownerDocument.removeEventListener(PAGE_HIDE_EVENT, hidden)
255
+ host.ownerDocument.removeEventListener(PAGE_SHOW_EVENT, shown)
256
+ unsubscribe?.()
257
+ Promise.resolve(root?.dispose(true)).catch(() => {})
258
+ root = undefined
259
+ }
260
+ return {
261
+ refill(fill) {
262
+ // A blocking render's settled fill is the server HTML the host already shows.
263
+ const inPlace = latest === 0 && root === undefined && initialHtml(fill) !== ""
264
+ const mine = ++latest
265
+ host.setAttribute("aria-busy", "true")
266
+ settledFill(fill).then(
267
+ (frame) => {
268
+ if (!disposed && mine === latest) apply(frame, mine, inPlace)
269
+ },
270
+ () => {
271
+ if (!disposed && mine === latest) apply(ERROR_FRAME, mine, false)
272
+ },
273
+ )
274
+ },
275
+ dispose,
276
+ park() {
277
+ if (disposed || hiddenBy === undefined) return false
278
+ release ??= onPageRelease(hiddenBy, dispose)
279
+ return true
280
+ },
281
+ unpark() {
282
+ release?.()
283
+ release = undefined
284
+ },
285
+ }
286
+ }
@@ -0,0 +1,239 @@
1
+ /**
2
+ * The server half of the identity epoch: a response that signs a visitor in or out also rotates the
3
+ * readable epoch cookie, so every tab drops the pages it kept alive for the previous visitor.
4
+ *
5
+ * A header-only hook reads each response's `Set-Cookie`: when an identity cookie's value differs from
6
+ * the request's, or the cookie is deleted, the response also sets a fresh epoch with the same lifetime,
7
+ * so the epoch expires with an idle session. A rolling session re-sending the same value with a new
8
+ * lifetime re-sends the epoch it holds with that lifetime, so the two keep expiring together.
9
+ */
10
+ import { type CookieOptions, parseCookies } from "@nifrajs/core/server"
11
+ import { EPOCH_COOKIE, LOCAL_EPOCH_COOKIE } from "../identity.ts"
12
+
13
+ /** `createWebApp`'s `keepAlive` option: which cookies say who is signed in. */
14
+ export interface KeepAliveServerOptions {
15
+ /**
16
+ * Cookies whose change means a different visitor, on top of the session cookies of
17
+ * `@nifrajs/auth` and Auth.js under their default names and Better Auth's under any `cookiePrefix`.
18
+ * Name a custom session cookie here, or kept pages survive a sign-in or sign-out through it.
19
+ */
20
+ readonly identityCookies?: readonly string[]
21
+ /** `HttpOnly` cookies that say nothing about who is signed in; development stops asking about them. */
22
+ readonly ignoreCookies?: readonly string[]
23
+ }
24
+
25
+ /** The request and response a header hook sees. */
26
+ interface HeaderView {
27
+ get(name: string): string | null
28
+ append(name: string, value: string): void
29
+ }
30
+ interface RequestView {
31
+ readonly url: string
32
+ header(name: string): string | null | undefined
33
+ }
34
+
35
+ // Session cookies by their default names, without a `__Secure-` / `__Host-` prefix.
36
+ const IDENTITY_DEFAULTS = ["nifra_session", "authjs.session-token", "next-auth.session-token"]
37
+ // `HttpOnly` cookies the same integrations write that carry no identity of their own.
38
+ const IGNORE_DEFAULTS = [
39
+ "authjs.csrf-token",
40
+ "authjs.callback-url",
41
+ "authjs.pkce.code_verifier",
42
+ "authjs.state",
43
+ "authjs.nonce",
44
+ "next-auth.csrf-token",
45
+ "next-auth.callback-url",
46
+ "next-auth.pkce.code_verifier",
47
+ "next-auth.state",
48
+ "next-auth.nonce",
49
+ "__nifra_draft",
50
+ ]
51
+
52
+ // Better Auth names its cookies `<advanced.cookiePrefix>.<name>`, "better-auth" unless the app sets
53
+ // one, so these count under any prefix.
54
+ const BETTER_AUTH_SESSION = ".session_token"
55
+ const BETTER_AUTH_IGNORED = [".session_data", ".dont_remember"]
56
+
57
+ // `__Secure-a.0` and `__Host-a` are the cookie `a`: a prefix, then Auth.js's chunk index.
58
+ const baseName = (name: string): string =>
59
+ name.replace(/^__(?:secure|host)-/i, "").replace(/\.\d+$/, "")
60
+
61
+ interface SetCookie {
62
+ readonly name: string
63
+ readonly value: string
64
+ readonly deleted: boolean
65
+ readonly httpOnly: boolean
66
+ readonly secure: boolean
67
+ readonly maxAge: string | undefined
68
+ readonly expires: string | undefined
69
+ }
70
+
71
+ // Splits a `Set-Cookie` list joined with ", ": only a comma followed by `name=` starts a cookie, so
72
+ // the comma inside an `Expires` date stays where it is.
73
+ const SPLIT = /,\s*(?=[!#$%&'*+\-.^_`|~0-9A-Za-z]+=)/
74
+
75
+ const parseSetCookies = (joined: string, now: number): SetCookie[] => {
76
+ const cookies: SetCookie[] = []
77
+ for (const line of joined.split(SPLIT)) {
78
+ const [pair = "", ...attributes] = line.split(";")
79
+ const at = pair.indexOf("=")
80
+ if (at <= 0) continue
81
+ let maxAge: string | undefined
82
+ let expires: string | undefined
83
+ let httpOnly = false
84
+ let secure = false
85
+ for (const attribute of attributes) {
86
+ const eq = attribute.indexOf("=")
87
+ const key = (eq === -1 ? attribute : attribute.slice(0, eq)).trim().toLowerCase()
88
+ const value = eq === -1 ? "" : attribute.slice(eq + 1).trim()
89
+ if (key === "max-age") maxAge = value
90
+ else if (key === "expires") expires = value
91
+ else if (key === "httponly") httpOnly = true
92
+ else if (key === "secure") secure = true
93
+ }
94
+ const name = pair.slice(0, at).trim()
95
+ const age = maxAge === undefined ? Number.NaN : Number(maxAge)
96
+ const until = expires === undefined ? Number.NaN : Date.parse(expires)
97
+ cookies.push({
98
+ name,
99
+ // Decoded as core decodes the request's cookies, so an encoded value compares equal to itself.
100
+ value: parseCookies(pair.trim())[name] ?? "",
101
+ deleted: age <= 0 || until <= now,
102
+ httpOnly,
103
+ secure: secure || /^__(?:secure|host)-/i.test(name),
104
+ maxAge,
105
+ expires,
106
+ })
107
+ }
108
+ return cookies
109
+ }
110
+
111
+ const randomEpoch = (): string => {
112
+ const bytes = crypto.getRandomValues(new Uint8Array(16))
113
+ let hex = ""
114
+ for (const byte of bytes) hex += byte.toString(16).padStart(2, "0")
115
+ return hex
116
+ }
117
+
118
+ const EPOCH_VALUE = /^[0-9a-f]{32}$/
119
+
120
+ // A forwarded protocol only picks the cookie's name: a client that lies about it loses its own epoch.
121
+ // A browser sends a `__Host-` cookie only over https, so carrying one says so behind a TLS proxy that
122
+ // forwards nothing, and the client, which reads that one first, must see the new value there.
123
+ const servedOverHttps = (request: RequestView, sent: Readonly<Record<string, string>>): boolean => {
124
+ if (request.url.startsWith("https:") || sent[EPOCH_COOKIE] !== undefined) return true
125
+ const forwarded = request.header("x-forwarded-proto")
126
+ return forwarded?.split(",")[0]?.trim().toLowerCase() === "https"
127
+ }
128
+
129
+ const epochCookie = (
130
+ secure: boolean,
131
+ value: string,
132
+ lifetime: { readonly maxAge?: string | undefined; readonly expires?: string | undefined },
133
+ ): string => {
134
+ let cookie = `${secure ? EPOCH_COOKIE : LOCAL_EPOCH_COOKIE}=${value}; Path=/; SameSite=Lax`
135
+ if (secure) cookie += "; Secure"
136
+ if (lifetime.maxAge !== undefined) cookie += `; Max-Age=${lifetime.maxAge}`
137
+ if (lifetime.expires !== undefined) cookie += `; Expires=${lifetime.expires}`
138
+ return cookie
139
+ }
140
+
141
+ /**
142
+ * The response hook `createWebApp` installs for an app with a `keepAlive` route. With `development`,
143
+ * it also says once per cookie when an `HttpOnly` cookie it does not know changes.
144
+ */
145
+ export function identityEpochHook(
146
+ options: KeepAliveServerOptions,
147
+ development: boolean,
148
+ ): (headers: HeaderView, request: RequestView) => void {
149
+ const named = new Set([...IDENTITY_DEFAULTS, ...(options.identityCookies ?? []).map(baseName)])
150
+ const told = new Set((options.ignoreCookies ?? []).map(baseName))
151
+ const ignored = new Set([...IGNORE_DEFAULTS, ...told])
152
+ // An app's own `ignoreCookies` entry outranks the Better Auth suffix, never a name it listed.
153
+ const isIdentity = (base: string): boolean =>
154
+ named.has(base) || (base.endsWith(BETTER_AUTH_SESSION) && !told.has(base))
155
+ const isIgnored = (base: string): boolean =>
156
+ ignored.has(base) || BETTER_AUTH_IGNORED.some((suffix) => base.endsWith(suffix))
157
+ const reported = new Set<string>()
158
+ return (headers, request) => {
159
+ const joined = headers.get("set-cookie")
160
+ if (joined === null || joined === "") return
161
+ const sent = parseCookies(request.header("cookie") ?? null)
162
+ let changed: SetCookie | undefined
163
+ let renewed: SetCookie | undefined
164
+ for (const cookie of parseSetCookies(joined, Date.now())) {
165
+ if (cookie.name === EPOCH_COOKIE || cookie.name === LOCAL_EPOCH_COOKIE) return
166
+ const now = cookie.deleted ? "" : cookie.value
167
+ const base = baseName(cookie.name)
168
+ if (now === (sent[cookie.name] ?? "")) {
169
+ if (
170
+ renewed === undefined &&
171
+ !cookie.deleted &&
172
+ (cookie.maxAge !== undefined || cookie.expires !== undefined) &&
173
+ isIdentity(base)
174
+ )
175
+ renewed = cookie
176
+ continue
177
+ }
178
+ if (isIdentity(base)) {
179
+ // The first identity cookie still set gives the lifetime; a sign-out deletes them all.
180
+ if (changed === undefined || (changed.deleted && !cookie.deleted)) changed = cookie
181
+ continue
182
+ }
183
+ if (!development || !cookie.httpOnly || isIgnored(base) || reported.has(base)) continue
184
+ reported.add(base)
185
+ console.warn(
186
+ `[nifra/web] NIFRA_KEEPALIVE_IDENTITY_COOKIE: this app keeps pages alive, and a response changed the HttpOnly cookie "${cookie.name}", which nifra does not know as a sign-in cookie. If it says who is signed in, name it in keepAlive: { identityCookies: ["${base}"] } so a sign-in or sign-out drops the pages kept for the previous visitor; if it does not, in keepAlive.ignoreCookies. An app run by \`nifra dev\` and \`nifra build\` exports keepAlive from backend/framework.ts; a server you write passes it to createWebApp.`,
187
+ )
188
+ }
189
+ if (changed !== undefined) {
190
+ headers.append(
191
+ "set-cookie",
192
+ epochCookie(
193
+ changed.secure || servedOverHttps(request, sent),
194
+ randomEpoch(),
195
+ changed.deleted ? {} : { maxAge: changed.maxAge, expires: changed.expires },
196
+ ),
197
+ )
198
+ return
199
+ }
200
+ if (renewed === undefined) return
201
+ const lifetime = { maxAge: renewed.maxAge, expires: renewed.expires }
202
+ for (const secure of [true, false]) {
203
+ const held = sent[secure ? EPOCH_COOKIE : LOCAL_EPOCH_COOKIE]
204
+ if (held === undefined || !EPOCH_VALUE.test(held)) continue
205
+ headers.append("set-cookie", epochCookie(secure, held, lifetime))
206
+ return
207
+ }
208
+ // An epoch that already expired, or was never set: a fresh one, so pages kept without one drop.
209
+ headers.append(
210
+ "set-cookie",
211
+ epochCookie(renewed.secure || servedOverHttps(request, sent), randomEpoch(), lifetime),
212
+ )
213
+ }
214
+ }
215
+
216
+ /** The response-cookie surface {@link rotateIdentityEpoch} needs: nifra's handler context. */
217
+ export interface IdentityEpochContext {
218
+ readonly request: Request
219
+ readonly set: { cookie(name: string, value: string, options?: CookieOptions): void }
220
+ }
221
+
222
+ /**
223
+ * Rotate the identity epoch from a flow that changes who is signed in without changing a cookie
224
+ * `createWebApp` watches - say, a session kept server-side under a cookie whose value stays the
225
+ * same. Every tab then drops the pages it kept alive for the previous visitor.
226
+ */
227
+ export function rotateIdentityEpoch(c: IdentityEpochContext): void {
228
+ const request = c.request
229
+ const secure = servedOverHttps(
230
+ { url: request.url, header: (name) => request.headers.get(name) },
231
+ parseCookies(request.headers.get("cookie")),
232
+ )
233
+ c.set.cookie(secure ? EPOCH_COOKIE : LOCAL_EPOCH_COOKIE, randomEpoch(), {
234
+ path: "/",
235
+ sameSite: "lax",
236
+ secure,
237
+ httpOnly: false,
238
+ })
239
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The seam between `createWebApp` and a cache wrapper for an action's invalidation: types and one
3
+ * symbol, so an app pays for the pipeline only once something runs it.
4
+ */
5
+ import type { RevalidateResult } from "./render-document.ts"
6
+
7
+ export interface InvalidationOp {
8
+ /** Names the operation in logs; never key text. */
9
+ readonly id: string
10
+ readonly paths: readonly string[]
11
+ readonly tags: readonly string[]
12
+ readonly keys: readonly string[]
13
+ }
14
+
15
+ /** What a `createWebApp` app hands a cache wrapper that owns its invalidation. */
16
+ export interface InvalidationHandoff {
17
+ /** The operation the action behind `request` recorded, once. */
18
+ take(request: Request): InvalidationOp | undefined
19
+ /** The app's live publish, when it configures `live`. */
20
+ readonly publish: ((keys: readonly string[]) => Promise<void>) | undefined
21
+ }
22
+
23
+ export const INVALIDATION: unique symbol = Symbol.for("nifra.web.invalidation")
24
+
25
+ /** The operation an action's `revalidate()` names: its paths, cache tags and live keys. */
26
+ export function invalidationOf(revalidation: RevalidateResult<unknown>): InvalidationOp {
27
+ return {
28
+ id: crypto.randomUUID(),
29
+ paths: revalidation.__nifraRevalidate.filter(
30
+ (path) => typeof path === "string" && path.startsWith("/"),
31
+ ),
32
+ tags: revalidation.__nifraInvalidate?.tags ?? [],
33
+ keys: revalidation.__nifraInvalidate?.keys ?? [],
34
+ }
35
+ }