@nifrajs/web 5.5.1 → 5.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (457) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/dist/boundary.d.ts +67 -7
  3. package/dist/boundary.d.ts.map +1 -1
  4. package/dist/boundary.js +6 -4
  5. package/dist/boundary.js.map +1 -1
  6. package/dist/build-plan.d.ts +3 -0
  7. package/dist/build-plan.d.ts.map +1 -1
  8. package/dist/build-plan.js.map +1 -1
  9. package/dist/build-vite.d.ts.map +1 -1
  10. package/dist/build-vite.js +2 -0
  11. package/dist/build-vite.js.map +1 -1
  12. package/dist/build.d.ts +9 -2
  13. package/dist/build.d.ts.map +1 -1
  14. package/dist/build.js +9 -0
  15. package/dist/build.js.map +1 -1
  16. package/dist/client.d.ts +2 -1
  17. package/dist/client.d.ts.map +1 -1
  18. package/dist/client.js +140 -12
  19. package/dist/client.js.map +1 -1
  20. package/dist/csp.d.ts +8 -0
  21. package/dist/csp.d.ts.map +1 -1
  22. package/dist/csp.js +11 -2
  23. package/dist/csp.js.map +1 -1
  24. package/dist/deferred.d.ts +6 -4
  25. package/dist/deferred.d.ts.map +1 -1
  26. package/dist/deferred.js +10 -6
  27. package/dist/deferred.js.map +1 -1
  28. package/dist/dev-feed.d.ts.map +1 -1
  29. package/dist/dev-feed.js +11 -2
  30. package/dist/dev-feed.js.map +1 -1
  31. package/dist/dev-session.d.ts.map +1 -1
  32. package/dist/dev-session.js +13 -6
  33. package/dist/dev-session.js.map +1 -1
  34. package/dist/dev.d.ts +4 -1
  35. package/dist/dev.d.ts.map +1 -1
  36. package/dist/dev.js +12 -5
  37. package/dist/dev.js.map +1 -1
  38. package/dist/diagnostic-catalog.d.ts +4 -0
  39. package/dist/diagnostic-catalog.d.ts.map +1 -1
  40. package/dist/diagnostic-catalog.js +48 -0
  41. package/dist/diagnostic-catalog.js.map +1 -1
  42. package/dist/diagnostic.d.ts +1 -1
  43. package/dist/diagnostic.d.ts.map +1 -1
  44. package/dist/diagnostic.js +1 -1
  45. package/dist/diagnostic.js.map +1 -1
  46. package/dist/fn.d.ts +5 -3
  47. package/dist/fn.d.ts.map +1 -1
  48. package/dist/fn.js +20 -5
  49. package/dist/fn.js.map +1 -1
  50. package/dist/fs.d.ts.map +1 -1
  51. package/dist/fs.js +13 -3
  52. package/dist/fs.js.map +1 -1
  53. package/dist/identity.d.ts +22 -0
  54. package/dist/identity.d.ts.map +1 -0
  55. package/dist/identity.js +60 -0
  56. package/dist/identity.js.map +1 -0
  57. package/dist/index.d.ts +7 -6
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +4 -2
  60. package/dist/index.js.map +1 -1
  61. package/dist/internal/cdn/cloudflare.d.ts +2 -1
  62. package/dist/internal/cdn/cloudflare.d.ts.map +1 -1
  63. package/dist/internal/cdn/cloudflare.js +2 -1
  64. package/dist/internal/cdn/cloudflare.js.map +1 -1
  65. package/dist/internal/certified-components.d.ts +1 -1
  66. package/dist/internal/certified-components.js +1 -1
  67. package/dist/internal/codegen.d.ts +11 -10
  68. package/dist/internal/codegen.d.ts.map +1 -1
  69. package/dist/internal/codegen.js +65 -8
  70. package/dist/internal/codegen.js.map +1 -1
  71. package/dist/internal/dev-websocket.d.ts +73 -0
  72. package/dist/internal/dev-websocket.d.ts.map +1 -0
  73. package/dist/internal/dev-websocket.js +145 -0
  74. package/dist/internal/dev-websocket.js.map +1 -0
  75. package/dist/internal/hole-runtime.d.ts +66 -0
  76. package/dist/internal/hole-runtime.d.ts.map +1 -0
  77. package/dist/internal/hole-runtime.js +211 -0
  78. package/dist/internal/hole-runtime.js.map +1 -0
  79. package/dist/internal/identity-epoch.d.ts +50 -0
  80. package/dist/internal/identity-epoch.d.ts.map +1 -0
  81. package/dist/internal/identity-epoch.js +179 -0
  82. package/dist/internal/identity-epoch.js.map +1 -0
  83. package/dist/internal/invalidation-contract.d.ts +23 -0
  84. package/dist/internal/invalidation-contract.d.ts.map +1 -0
  85. package/dist/internal/invalidation-contract.js +11 -0
  86. package/dist/internal/invalidation-contract.js.map +1 -0
  87. package/dist/internal/invalidation.d.ts +62 -0
  88. package/dist/internal/invalidation.d.ts.map +1 -0
  89. package/dist/internal/invalidation.js +149 -0
  90. package/dist/internal/invalidation.js.map +1 -0
  91. package/dist/internal/island-bindings.d.ts +51 -0
  92. package/dist/internal/island-bindings.d.ts.map +1 -0
  93. package/dist/internal/island-bindings.js +147 -0
  94. package/dist/internal/island-bindings.js.map +1 -0
  95. package/dist/internal/island-list.d.ts +26 -0
  96. package/dist/internal/island-list.d.ts.map +1 -0
  97. package/dist/internal/island-list.js +78 -0
  98. package/dist/internal/island-list.js.map +1 -0
  99. package/dist/internal/island-mount.d.ts +4 -0
  100. package/dist/internal/island-mount.d.ts.map +1 -0
  101. package/dist/internal/island-mount.js +66 -0
  102. package/dist/internal/island-mount.js.map +1 -0
  103. package/dist/internal/island-owner.d.ts +5 -0
  104. package/dist/internal/island-owner.d.ts.map +1 -0
  105. package/dist/internal/island-owner.js +20 -0
  106. package/dist/internal/island-owner.js.map +1 -0
  107. package/dist/internal/island-resource.d.ts +30 -0
  108. package/dist/internal/island-resource.d.ts.map +1 -0
  109. package/dist/internal/island-resource.js +57 -0
  110. package/dist/internal/island-resource.js.map +1 -0
  111. package/dist/internal/island-signals.d.ts +51 -0
  112. package/dist/internal/island-signals.d.ts.map +1 -0
  113. package/dist/internal/island-signals.js +220 -0
  114. package/dist/internal/island-signals.js.map +1 -0
  115. package/dist/internal/keep-alive-table.d.ts +23 -0
  116. package/dist/internal/keep-alive-table.d.ts.map +1 -0
  117. package/dist/internal/keep-alive-table.js +53 -0
  118. package/dist/internal/keep-alive-table.js.map +1 -0
  119. package/dist/internal/live-contract.d.ts +96 -0
  120. package/dist/internal/live-contract.d.ts.map +1 -0
  121. package/dist/internal/live-contract.js +5 -0
  122. package/dist/internal/live-contract.js.map +1 -0
  123. package/dist/internal/live-queries.d.ts +22 -0
  124. package/dist/internal/live-queries.d.ts.map +1 -0
  125. package/dist/internal/live-queries.js +54 -0
  126. package/dist/internal/live-queries.js.map +1 -0
  127. package/dist/internal/page-events.d.ts +10 -0
  128. package/dist/internal/page-events.d.ts.map +1 -0
  129. package/dist/internal/page-events.js +26 -0
  130. package/dist/internal/page-events.js.map +1 -0
  131. package/dist/internal/page-execution.d.ts +14 -1
  132. package/dist/internal/page-execution.d.ts.map +1 -1
  133. package/dist/internal/page-execution.js +415 -31
  134. package/dist/internal/page-execution.js.map +1 -1
  135. package/dist/internal/ppr/admission.d.ts +83 -0
  136. package/dist/internal/ppr/admission.d.ts.map +1 -0
  137. package/dist/internal/ppr/admission.js +221 -0
  138. package/dist/internal/ppr/admission.js.map +1 -0
  139. package/dist/internal/ppr/canary.d.ts +24 -0
  140. package/dist/internal/ppr/canary.d.ts.map +1 -0
  141. package/dist/internal/ppr/canary.js +102 -0
  142. package/dist/internal/ppr/canary.js.map +1 -0
  143. package/dist/internal/ppr/compile-shell.d.ts +39 -0
  144. package/dist/internal/ppr/compile-shell.d.ts.map +1 -0
  145. package/dist/internal/ppr/compile-shell.js +298 -0
  146. package/dist/internal/ppr/compile-shell.js.map +1 -0
  147. package/dist/internal/ppr/contract.d.ts +177 -0
  148. package/dist/internal/ppr/contract.d.ts.map +1 -0
  149. package/dist/internal/ppr/contract.js +80 -0
  150. package/dist/internal/ppr/contract.js.map +1 -0
  151. package/dist/internal/ppr/frames.d.ts +71 -0
  152. package/dist/internal/ppr/frames.d.ts.map +1 -0
  153. package/dist/internal/ppr/frames.js +119 -0
  154. package/dist/internal/ppr/frames.js.map +1 -0
  155. package/dist/internal/ppr/integration.d.ts +19 -0
  156. package/dist/internal/ppr/integration.d.ts.map +1 -0
  157. package/dist/internal/ppr/integration.js +106 -0
  158. package/dist/internal/ppr/integration.js.map +1 -0
  159. package/dist/internal/ppr/options.d.ts +96 -0
  160. package/dist/internal/ppr/options.d.ts.map +1 -0
  161. package/dist/internal/ppr/options.js +125 -0
  162. package/dist/internal/ppr/options.js.map +1 -0
  163. package/dist/internal/ppr/route-rules.d.ts +12 -0
  164. package/dist/internal/ppr/route-rules.d.ts.map +1 -0
  165. package/dist/internal/ppr/route-rules.js +53 -0
  166. package/dist/internal/ppr/route-rules.js.map +1 -0
  167. package/dist/internal/ppr/runtime.d.ts +29 -0
  168. package/dist/internal/ppr/runtime.d.ts.map +1 -0
  169. package/dist/internal/ppr/runtime.js +32 -0
  170. package/dist/internal/ppr/runtime.js.map +1 -0
  171. package/dist/internal/ppr/server.d.ts +29 -0
  172. package/dist/internal/ppr/server.d.ts.map +1 -0
  173. package/dist/internal/ppr/server.js +1407 -0
  174. package/dist/internal/ppr/server.js.map +1 -0
  175. package/dist/internal/ppr/shell-store.d.ts +38 -0
  176. package/dist/internal/ppr/shell-store.d.ts.map +1 -0
  177. package/dist/internal/ppr/shell-store.js +90 -0
  178. package/dist/internal/ppr/shell-store.js.map +1 -0
  179. package/dist/internal/prerendered-pages.d.ts +1 -1
  180. package/dist/internal/prerendered-pages.d.ts.map +1 -1
  181. package/dist/internal/prerendered-pages.js +7 -4
  182. package/dist/internal/prerendered-pages.js.map +1 -1
  183. package/dist/internal/query-digest.d.ts +9 -0
  184. package/dist/internal/query-digest.d.ts.map +1 -0
  185. package/dist/internal/query-digest.js +23 -0
  186. package/dist/internal/query-digest.js.map +1 -0
  187. package/dist/internal/query-runtime.d.ts +6 -1
  188. package/dist/internal/query-runtime.d.ts.map +1 -1
  189. package/dist/internal/query-runtime.js +9 -0
  190. package/dist/internal/query-runtime.js.map +1 -1
  191. package/dist/internal/query-seam.d.ts +29 -0
  192. package/dist/internal/query-seam.d.ts.map +1 -0
  193. package/dist/internal/query-seam.js +23 -0
  194. package/dist/internal/query-seam.js.map +1 -0
  195. package/dist/internal/render-document.d.ts +28 -1
  196. package/dist/internal/render-document.d.ts.map +1 -1
  197. package/dist/internal/render-document.js +83 -18
  198. package/dist/internal/render-document.js.map +1 -1
  199. package/dist/internal/request-abort.d.ts +8 -0
  200. package/dist/internal/request-abort.d.ts.map +1 -0
  201. package/dist/internal/request-abort.js +22 -0
  202. package/dist/internal/request-abort.js.map +1 -0
  203. package/dist/internal/server-boundary.d.ts +2 -0
  204. package/dist/internal/server-boundary.d.ts.map +1 -1
  205. package/dist/internal/server-boundary.js +8 -1
  206. package/dist/internal/server-boundary.js.map +1 -1
  207. package/dist/internal/share-data.d.ts +10 -0
  208. package/dist/internal/share-data.d.ts.map +1 -0
  209. package/dist/internal/share-data.js +135 -0
  210. package/dist/internal/share-data.js.map +1 -0
  211. package/dist/internal/store-signature.d.ts +15 -0
  212. package/dist/internal/store-signature.d.ts.map +1 -0
  213. package/dist/internal/store-signature.js +114 -0
  214. package/dist/internal/store-signature.js.map +1 -0
  215. package/dist/internal/vite-plugin-compat.d.ts +2 -2
  216. package/dist/internal/vite-plugin-compat.js +2 -2
  217. package/dist/internal/web-app.d.ts +25 -0
  218. package/dist/internal/web-app.d.ts.map +1 -1
  219. package/dist/internal/web-app.js +78 -3
  220. package/dist/internal/web-app.js.map +1 -1
  221. package/dist/islands-swap.d.ts +41 -0
  222. package/dist/islands-swap.d.ts.map +1 -0
  223. package/dist/islands-swap.js +270 -0
  224. package/dist/islands-swap.js.map +1 -0
  225. package/dist/islands.d.ts +86 -24
  226. package/dist/islands.d.ts.map +1 -1
  227. package/dist/islands.js +138 -78
  228. package/dist/islands.js.map +1 -1
  229. package/dist/isr.d.ts +67 -13
  230. package/dist/isr.d.ts.map +1 -1
  231. package/dist/isr.js +247 -99
  232. package/dist/isr.js.map +1 -1
  233. package/dist/keep-alive.d.ts +147 -0
  234. package/dist/keep-alive.d.ts.map +1 -0
  235. package/dist/keep-alive.js +1069 -0
  236. package/dist/keep-alive.js.map +1 -0
  237. package/dist/live/bus.d.ts +56 -0
  238. package/dist/live/bus.d.ts.map +1 -0
  239. package/dist/live/bus.js +32 -0
  240. package/dist/live/bus.js.map +1 -0
  241. package/dist/live/core.d.ts +25 -0
  242. package/dist/live/core.d.ts.map +1 -0
  243. package/dist/live/core.js +29 -0
  244. package/dist/live/core.js.map +1 -0
  245. package/dist/live/history.d.ts +38 -0
  246. package/dist/live/history.d.ts.map +1 -0
  247. package/dist/live/history.js +104 -0
  248. package/dist/live/history.js.map +1 -0
  249. package/dist/live/hub-protocol.d.ts +46 -0
  250. package/dist/live/hub-protocol.d.ts.map +1 -0
  251. package/dist/live/hub-protocol.js +108 -0
  252. package/dist/live/hub-protocol.js.map +1 -0
  253. package/dist/live/hub.d.ts +181 -0
  254. package/dist/live/hub.d.ts.map +1 -0
  255. package/dist/live/hub.js +539 -0
  256. package/dist/live/hub.js.map +1 -0
  257. package/dist/live/keyring.d.ts +87 -0
  258. package/dist/live/keyring.d.ts.map +1 -0
  259. package/dist/live/keyring.js +260 -0
  260. package/dist/live/keyring.js.map +1 -0
  261. package/dist/live/read-capped.d.ts +6 -0
  262. package/dist/live/read-capped.d.ts.map +1 -0
  263. package/dist/live/read-capped.js +42 -0
  264. package/dist/live/read-capped.js.map +1 -0
  265. package/dist/live/revocations.d.ts +52 -0
  266. package/dist/live/revocations.d.ts.map +1 -0
  267. package/dist/live/revocations.js +116 -0
  268. package/dist/live/revocations.js.map +1 -0
  269. package/dist/live/settings.d.ts +14 -0
  270. package/dist/live/settings.d.ts.map +1 -0
  271. package/dist/live/settings.js +38 -0
  272. package/dist/live/settings.js.map +1 -0
  273. package/dist/live-client.d.ts +34 -0
  274. package/dist/live-client.d.ts.map +1 -0
  275. package/dist/live-client.js +840 -0
  276. package/dist/live-client.js.map +1 -0
  277. package/dist/live-hub.d.ts +130 -0
  278. package/dist/live-hub.d.ts.map +1 -0
  279. package/dist/live-hub.js +507 -0
  280. package/dist/live-hub.js.map +1 -0
  281. package/dist/live-redis.d.ts +48 -0
  282. package/dist/live-redis.d.ts.map +1 -0
  283. package/dist/live-redis.js +201 -0
  284. package/dist/live-redis.js.map +1 -0
  285. package/dist/live.d.ts +94 -0
  286. package/dist/live.d.ts.map +1 -0
  287. package/dist/live.js +869 -0
  288. package/dist/live.js.map +1 -0
  289. package/dist/manifest.d.ts +113 -2
  290. package/dist/manifest.d.ts.map +1 -1
  291. package/dist/manifest.js +109 -5
  292. package/dist/manifest.js.map +1 -1
  293. package/dist/navigation.d.ts +9 -3
  294. package/dist/navigation.d.ts.map +1 -1
  295. package/dist/navigation.js +13 -3
  296. package/dist/navigation.js.map +1 -1
  297. package/dist/plugins/vite-zone-guard.d.ts +6 -0
  298. package/dist/plugins/vite-zone-guard.d.ts.map +1 -1
  299. package/dist/plugins/vite-zone-guard.js +8 -2
  300. package/dist/plugins/vite-zone-guard.js.map +1 -1
  301. package/dist/ppr.d.ts +73 -0
  302. package/dist/ppr.d.ts.map +1 -0
  303. package/dist/ppr.js +76 -0
  304. package/dist/ppr.js.map +1 -0
  305. package/dist/query.d.ts +8 -13
  306. package/dist/query.d.ts.map +1 -1
  307. package/dist/query.js +112 -18
  308. package/dist/query.js.map +1 -1
  309. package/dist/render-seam.d.ts +30 -0
  310. package/dist/render-seam.d.ts.map +1 -1
  311. package/dist/render-seam.js +5 -0
  312. package/dist/render-seam.js.map +1 -1
  313. package/dist/route-types.d.ts.map +1 -1
  314. package/dist/route-types.js +2 -2
  315. package/dist/route-types.js.map +1 -1
  316. package/dist/router.d.ts +72 -4
  317. package/dist/router.d.ts.map +1 -1
  318. package/dist/router.js +374 -99
  319. package/dist/router.js.map +1 -1
  320. package/dist/server-components/build.d.ts.map +1 -1
  321. package/dist/server-components/build.js +3 -3
  322. package/dist/server-components/build.js.map +1 -1
  323. package/dist/server-components/client.d.ts +23 -1
  324. package/dist/server-components/client.d.ts.map +1 -1
  325. package/dist/server-components/client.js +92 -5
  326. package/dist/server-components/client.js.map +1 -1
  327. package/dist/server-components/hole.d.ts +8 -0
  328. package/dist/server-components/hole.d.ts.map +1 -0
  329. package/dist/server-components/hole.js +20 -0
  330. package/dist/server-components/hole.js.map +1 -0
  331. package/dist/server-components/html-policy.d.ts.map +1 -1
  332. package/dist/server-components/html-policy.js +62 -4
  333. package/dist/server-components/html-policy.js.map +1 -1
  334. package/dist/server-components/html-scan.d.ts +17 -0
  335. package/dist/server-components/html-scan.d.ts.map +1 -0
  336. package/dist/server-components/html-scan.js +180 -0
  337. package/dist/server-components/html-scan.js.map +1 -0
  338. package/dist/server-components/html.d.ts.map +1 -1
  339. package/dist/server-components/html.js +19 -4
  340. package/dist/server-components/html.js.map +1 -1
  341. package/dist/server-components/router-integration.d.ts +4 -1
  342. package/dist/server-components/router-integration.d.ts.map +1 -1
  343. package/dist/server-components/router-integration.js +23 -2
  344. package/dist/server-components/router-integration.js.map +1 -1
  345. package/dist/server-components/runtime.d.ts +6 -1
  346. package/dist/server-components/runtime.d.ts.map +1 -1
  347. package/dist/server-components/runtime.js +86 -28
  348. package/dist/server-components/runtime.js.map +1 -1
  349. package/dist/server-components/state.d.ts +3 -1
  350. package/dist/server-components/state.d.ts.map +1 -1
  351. package/dist/server-components/state.js +50 -38
  352. package/dist/server-components/state.js.map +1 -1
  353. package/dist/server-components/types.d.ts +9 -0
  354. package/dist/server-components/types.d.ts.map +1 -1
  355. package/dist/vite.d.ts +5 -1
  356. package/dist/vite.d.ts.map +1 -1
  357. package/dist/vite.js +58 -4
  358. package/dist/vite.js.map +1 -1
  359. package/package.json +48 -8
  360. package/src/boundary.ts +77 -5
  361. package/src/build-plan.ts +3 -0
  362. package/src/build-vite.ts +2 -0
  363. package/src/build.ts +19 -1
  364. package/src/client.ts +147 -10
  365. package/src/csp.ts +16 -2
  366. package/src/deferred.ts +11 -5
  367. package/src/dev-feed.ts +11 -2
  368. package/src/dev-session.ts +22 -11
  369. package/src/dev.ts +29 -8
  370. package/src/diagnostic-catalog.ts +53 -0
  371. package/src/diagnostic.ts +1 -0
  372. package/src/fn.ts +24 -7
  373. package/src/fs.ts +22 -8
  374. package/src/identity.ts +61 -0
  375. package/src/index.ts +19 -0
  376. package/src/internal/cdn/cloudflare.ts +2 -1
  377. package/src/internal/certified-components.ts +1 -1
  378. package/src/internal/codegen.ts +87 -8
  379. package/src/internal/dev-websocket.ts +230 -0
  380. package/src/internal/hole-runtime.ts +286 -0
  381. package/src/internal/identity-epoch.ts +235 -0
  382. package/src/internal/invalidation-contract.ts +35 -0
  383. package/src/internal/invalidation.ts +213 -0
  384. package/src/internal/island-bindings.ts +191 -0
  385. package/src/internal/island-list.ts +92 -0
  386. package/src/internal/island-mount.ts +68 -0
  387. package/src/internal/island-owner.ts +20 -0
  388. package/src/internal/island-resource.ts +76 -0
  389. package/src/internal/island-signals.ts +249 -0
  390. package/src/internal/keep-alive-table.ts +72 -0
  391. package/src/internal/live-contract.ts +106 -0
  392. package/src/internal/live-queries.ts +60 -0
  393. package/src/internal/page-events.ts +27 -0
  394. package/src/internal/page-execution.ts +579 -40
  395. package/src/internal/ppr/admission.ts +300 -0
  396. package/src/internal/ppr/canary.ts +122 -0
  397. package/src/internal/ppr/compile-shell.ts +379 -0
  398. package/src/internal/ppr/contract.ts +265 -0
  399. package/src/internal/ppr/frames.ts +170 -0
  400. package/src/internal/ppr/integration.ts +137 -0
  401. package/src/internal/ppr/options.ts +221 -0
  402. package/src/internal/ppr/route-rules.ts +77 -0
  403. package/src/internal/ppr/runtime.ts +36 -0
  404. package/src/internal/ppr/server.ts +1764 -0
  405. package/src/internal/ppr/shell-store.ts +139 -0
  406. package/src/internal/prerendered-pages.ts +7 -4
  407. package/src/internal/query-digest.ts +34 -0
  408. package/src/internal/query-runtime.ts +14 -1
  409. package/src/internal/query-seam.ts +48 -0
  410. package/src/internal/render-document.ts +149 -22
  411. package/src/internal/request-abort.ts +26 -0
  412. package/src/internal/server-boundary.ts +10 -1
  413. package/src/internal/share-data.ts +136 -0
  414. package/src/internal/store-signature.ts +149 -0
  415. package/src/internal/vite-plugin-compat.ts +2 -2
  416. package/src/internal/web-app.ts +121 -3
  417. package/src/islands-swap.ts +283 -0
  418. package/src/islands.ts +169 -72
  419. package/src/isr.ts +348 -106
  420. package/src/keep-alive.ts +1315 -0
  421. package/src/live/bus.ts +77 -0
  422. package/src/live/core.ts +52 -0
  423. package/src/live/history.ts +109 -0
  424. package/src/live/hub-protocol.ts +133 -0
  425. package/src/live/hub.ts +615 -0
  426. package/src/live/keyring.ts +379 -0
  427. package/src/live/read-capped.ts +36 -0
  428. package/src/live/revocations.ts +155 -0
  429. package/src/live/settings.ts +46 -0
  430. package/src/live-client.ts +973 -0
  431. package/src/live-hub.ts +658 -0
  432. package/src/live-redis.ts +259 -0
  433. package/src/live.ts +1096 -0
  434. package/src/manifest.ts +274 -6
  435. package/src/navigation.ts +14 -2
  436. package/src/plugins/vite-zone-guard.ts +15 -2
  437. package/src/ppr.ts +153 -0
  438. package/src/query.ts +147 -20
  439. package/src/render-seam.ts +35 -0
  440. package/src/route-types.ts +3 -1
  441. package/src/router.ts +494 -101
  442. package/src/server-components/build.ts +9 -3
  443. package/src/server-components/client.ts +89 -5
  444. package/src/server-components/hole.ts +23 -0
  445. package/src/server-components/html-policy.ts +63 -4
  446. package/src/server-components/html-scan.ts +159 -0
  447. package/src/server-components/html.ts +18 -4
  448. package/src/server-components/router-integration.ts +21 -2
  449. package/src/server-components/runtime.ts +112 -30
  450. package/src/server-components/state.ts +66 -44
  451. package/src/server-components/types.ts +16 -0
  452. package/src/vite.ts +75 -6
  453. package/dist/nano.d.ts +0 -125
  454. package/dist/nano.d.ts.map +0 -1
  455. package/dist/nano.js +0 -190
  456. package/dist/nano.js.map +0 -1
  457. package/src/nano.ts +0 -246
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Shells and shared hole frames in a store shared by processes (Workers KV, Redis): a second level
3
+ * behind the in-process map. Whoever can write the store could put script into every visitor's page,
4
+ * so each entry is signed with the app's secret over its key, deployment and contents; anything that
5
+ * fails the check is a miss.
6
+ */
7
+ import type { CachedResponse, CacheStore } from "../../isr.ts"
8
+ import { storeSigner } from "../store-signature.ts"
9
+
10
+ /** A shell or a shared hole's frame as the shared store holds it: everything a process needs to
11
+ * serve it. */
12
+ export interface StoredEntry {
13
+ readonly routeId: string
14
+ /** A shell's document up to the request script's end, with room for the token at `tokenAt`
15
+ * (bytes); or a hole's frame. */
16
+ readonly html: string
17
+ readonly tokenAt: number
18
+ readonly data: unknown
19
+ readonly builtAt: number
20
+ readonly tags: readonly string[]
21
+ /** A shared hole's live keys. */
22
+ readonly keys?: readonly string[]
23
+ /** The shell loader's `notFound()`. */
24
+ readonly notFound: boolean
25
+ }
26
+
27
+ export interface SharedShellStore {
28
+ get(key: string): Promise<StoredEntry | undefined>
29
+ /** Skipped by the store when the key or one of its tags was purged after `fence`. */
30
+ set(
31
+ key: string,
32
+ entry: StoredEntry,
33
+ keepSeconds: number,
34
+ fence: number | undefined,
35
+ ): Promise<void>
36
+ fence(): Promise<number | undefined>
37
+ /** Absent when the store cannot drop entries by tag. */
38
+ readonly invalidateTag: ((tag: string) => Promise<void>) | undefined
39
+ }
40
+
41
+ const MAC_HEADER = "x-nifra-ppr-mac"
42
+
43
+ const isStrings = (value: unknown): value is readonly string[] =>
44
+ Array.isArray(value) && value.every((item) => typeof item === "string")
45
+
46
+ const field = (value: object, name: string): unknown => Reflect.get(value, name)
47
+
48
+ const isStoredEntry = (value: unknown): value is StoredEntry => {
49
+ if (typeof value !== "object" || value === null) return false
50
+ const tokenAt = field(value, "tokenAt")
51
+ const keys = field(value, "keys")
52
+ return (
53
+ typeof field(value, "routeId") === "string" &&
54
+ typeof field(value, "html") === "string" &&
55
+ typeof tokenAt === "number" &&
56
+ Number.isSafeInteger(tokenAt) &&
57
+ typeof field(value, "builtAt") === "number" &&
58
+ isStrings(field(value, "tags")) &&
59
+ (keys === undefined || isStrings(keys)) &&
60
+ typeof field(value, "notFound") === "boolean"
61
+ )
62
+ }
63
+
64
+ export function sharedShellStore(
65
+ store: CacheStore,
66
+ secret: string,
67
+ deployment: string,
68
+ ): SharedShellStore {
69
+ const signer = storeSigner(secret)
70
+ const invalidateTag = store.invalidateTag
71
+ return {
72
+ async get(entryKey) {
73
+ const cached = await store.get(entryKey)
74
+ if (cached === undefined) return undefined
75
+ // One part, where ISR signs two: under a shared secret neither entry verifies as the other.
76
+ if (!(await signer.verify([cached.body], cached.headers[MAC_HEADER]))) {
77
+ console.error("[nifra/web] ppr: a shared store entry failed its signature check", {
78
+ key: entryKey,
79
+ })
80
+ return undefined
81
+ }
82
+ let payload: unknown
83
+ try {
84
+ payload = JSON.parse(cached.body)
85
+ } catch {
86
+ return undefined
87
+ }
88
+ if (typeof payload !== "object" || payload === null) return undefined
89
+ // The key and deployment are signed too, so an entry copied to another key or kept from
90
+ // another deployment never verifies as this one.
91
+ if (
92
+ field(payload, "v") !== 1 ||
93
+ field(payload, "key") !== entryKey ||
94
+ field(payload, "deployment") !== deployment
95
+ ) {
96
+ return undefined
97
+ }
98
+ const entry = field(payload, "entry")
99
+ return isStoredEntry(entry) ? entry : undefined
100
+ },
101
+ async set(entryKey, entry, keepSeconds, fence) {
102
+ const body = JSON.stringify({ v: 1, key: entryKey, deployment, entry })
103
+ const mac = await signer.sign([body])
104
+ const value: CachedResponse = {
105
+ body,
106
+ status: entry.notFound ? 404 : 200,
107
+ headers: { [MAC_HEADER]: mac },
108
+ storedAt: entry.builtAt,
109
+ revalidate: keepSeconds * 1000,
110
+ tags: entry.tags,
111
+ fence,
112
+ }
113
+ await store.set(entryKey, value)
114
+ },
115
+ async fence() {
116
+ return store.fence === undefined ? undefined : await store.fence()
117
+ },
118
+ invalidateTag:
119
+ invalidateTag === undefined ? undefined : (tag) => invalidateTag.call(store, tag),
120
+ }
121
+ }
122
+
123
+ /**
124
+ * A short, stable digest of `text` (FNV-1a, 64 bits as hex). It names a deployment inside cache keys;
125
+ * nothing an attacker controls goes into it, so it needs no collision resistance.
126
+ */
127
+ export function digest(text: string): string {
128
+ let high = 0xcbf29ce4
129
+ let low = 0x84222325
130
+ for (let at = 0; at < text.length; at++) {
131
+ low = (low ^ text.charCodeAt(at)) >>> 0
132
+ // Multiply the 64-bit value by the FNV prime 0x100000001b3 in two 32-bit halves.
133
+ const lowProduct = low * 0x1b3
134
+ const carry = Math.floor(lowProduct / 0x100000000)
135
+ high = (high * 0x1b3 + low * 0x100 + carry) >>> 0
136
+ low = lowProduct >>> 0
137
+ }
138
+ return high.toString(16).padStart(8, "0") + low.toString(16).padStart(8, "0")
139
+ }
@@ -1,5 +1,6 @@
1
1
  import { pathnameOf } from "@nifrajs/core/server"
2
2
  import { isDraftEnabled } from "../draft.ts"
3
+ import { SWAP_HEADER } from "../render-seam.ts"
3
4
  import { DATA_HEADER } from "../router.ts"
4
5
 
5
6
  /**
@@ -21,10 +22,11 @@ const REPLAYED = [
21
22
  "content-language",
22
23
  "content-security-policy",
23
24
  "content-security-policy-report-only",
25
+ "vary",
24
26
  ]
25
- // Vary values a file still answers correctly: compression is negotiated by the host, and a data request
26
- // never reaches the file.
27
- const STATIC_VARY = new Set(["accept-encoding", DATA_HEADER])
27
+ // Vary values a file still answers correctly: compression is negotiated by the host, and a data or
28
+ // slot-swap request never reaches the file.
29
+ const STATIC_VARY = new Set(["accept-encoding", DATA_HEADER, SWAP_HEADER])
28
30
 
29
31
  /**
30
32
  * The headers a self-hosted server replays with a prerendered page, or `undefined` when its build-time
@@ -84,7 +86,7 @@ export function servablePrerenderedPages(
84
86
  /**
85
87
  * Wrap a page route's GET handler so a plain document request for a prerendered path is answered from
86
88
  * its file. Anything a file cannot answer the way a render would renders on demand: a query string, a
87
- * navigation data or component request, a draft-mode visitor, or a file the reader cannot return.
89
+ * navigation data, component or slot-swap request, a draft-mode visitor, or a file the reader cannot return.
88
90
  */
89
91
  export function servePrerenderedPages(
90
92
  prerendered: PrerenderedPages,
@@ -102,6 +104,7 @@ export function servePrerenderedPages(
102
104
  req.url.includes("?") ||
103
105
  req.headers.has(DATA_HEADER) ||
104
106
  req.headers.has("x-nifra-component") ||
107
+ req.headers.has(SWAP_HEADER) ||
105
108
  (draftSecret !== undefined && (await isDraftEnabled(req, draftSecret)))
106
109
  ) {
107
110
  return render(c)
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The digest a live load gives each granted key for query hooks: HMAC-SHA-256 under a salt fresh per
3
+ * load, the first 16 bytes in base64url. A page's queries name the keys they read and find the load's
4
+ * tag for each one without the load ever sending key text. Shared by the server and the browser.
5
+ */
6
+
7
+ const TEXT = new TextEncoder()
8
+ const DIGEST_BYTES = 16
9
+ const SALT_BYTES = 16
10
+
11
+ export function querySalt(): string {
12
+ return base64url(crypto.getRandomValues(new Uint8Array(SALT_BYTES)))
13
+ }
14
+
15
+ /** Digests keys under one salt, importing the salt once. */
16
+ export async function queryDigester(salt: string): Promise<(key: string) => Promise<string>> {
17
+ const secret = await crypto.subtle.importKey(
18
+ "raw",
19
+ TEXT.encode(salt),
20
+ { name: "HMAC", hash: "SHA-256" },
21
+ false,
22
+ ["sign"],
23
+ )
24
+ return async (key) =>
25
+ base64url(
26
+ new Uint8Array(await crypto.subtle.sign("HMAC", secret, TEXT.encode(key)), 0, DIGEST_BYTES),
27
+ )
28
+ }
29
+
30
+ function base64url(bytes: Uint8Array): string {
31
+ let binary = ""
32
+ for (const byte of bytes) binary += String.fromCharCode(byte)
33
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")
34
+ }
@@ -1,4 +1,4 @@
1
- import type { QueryClient, QueryState } from "../query.ts"
1
+ import type { QueryClient, QueryHandle, QueryState } from "../query.ts"
2
2
  import { createQueryClient } from "../query.ts"
3
3
 
4
4
  /** Stable idle state shared by every framework query binding. */
@@ -23,6 +23,19 @@ export function getQueryClientSingleton(): QueryClient | undefined {
23
23
  return singleton
24
24
  }
25
25
 
26
+ export const noop = (): void => {}
27
+
28
+ /** Stable no-op subscription for a binding with no client (server, pre-hydration). */
29
+ export const noopSubscribe = (): (() => void) => noop
30
+
31
+ /** A binding's fetch on mount. A failed fetch shows as the query's `error`, so its promise is let go. */
32
+ export const fetchOnMount = (handle: QueryHandle<unknown> | undefined): (() => void) =>
33
+ handle === undefined
34
+ ? noop
35
+ : () => {
36
+ handle.fetch().catch(noop)
37
+ }
38
+
26
39
  /** Create the refetch fallback used when a binding has no client-side query cache. */
27
40
  export function createNoClientRefetch<T>(tag: string, operation: string): () => Promise<T> {
28
41
  return async () => {
@@ -0,0 +1,48 @@
1
+ /**
2
+ * What a query client tells the live client. `@nifrajs/web/live-client` installs the seam; a page
3
+ * without live refresh never does, so its queries keep no live state and ship none of it.
4
+ */
5
+ import type { QueryClient } from "../query.ts"
6
+
7
+ export interface LiveQuery {
8
+ /** The live keys its data depends on, as the page's loaders declare them. */
9
+ keys(): readonly string[]
10
+ /** The {@link QuerySeam.tick} its data was asked for at: the start of the fetch in flight, else of
11
+ * the fetch that produced it. */
12
+ tick(): number
13
+ /** Drop its data's freshness and refetch, superseding a fetch in flight. */
14
+ refresh(): void
15
+ }
16
+
17
+ export interface QuerySeam {
18
+ /** The live tick now, read as each fetch starts. */
19
+ tick(): number
20
+ /** A query was mounted or unmounted; only a mounted one with live keys is watched. */
21
+ track(query: LiveQuery, mounted: boolean): void
22
+ }
23
+
24
+ // On the global, so a query engine and a live client from two copies of the package still meet.
25
+ const SEAM = Symbol.for("nifra.web.querySeam")
26
+ const CLIENTS = Symbol.for("nifra.web.queryClients")
27
+
28
+ export const querySeam = (): QuerySeam | undefined => Reflect.get(globalThis, SEAM)
29
+
30
+ export function installQuerySeam(seam: QuerySeam): void {
31
+ if (querySeam() === undefined) Reflect.set(globalThis, SEAM, seam)
32
+ }
33
+
34
+ /**
35
+ * The browser query clients the page made, held weakly: a viewer switch clears each one still alive,
36
+ * whether it was made before or after the live client loaded.
37
+ */
38
+ export const queryClients = (): readonly WeakRef<QueryClient>[] =>
39
+ Reflect.get(globalThis, CLIENTS) ?? []
40
+
41
+ export function adoptClient(client: QueryClient): void {
42
+ // A server builds a client per request; none of them outlives it or sees a viewer switch.
43
+ if (typeof Reflect.get(globalThis, "window") === "undefined") return
44
+ // Each new client sheds the ones the page let go, so the list never outgrows the live clients.
45
+ const clients = queryClients().filter((ref) => ref.deref() !== undefined)
46
+ clients.push(new WeakRef(client))
47
+ Reflect.set(globalThis, CLIENTS, clients)
48
+ }
@@ -24,6 +24,7 @@ import type {
24
24
  PageLocales,
25
25
  RenderAdapter,
26
26
  RenderProps,
27
+ RenderStreamOptions,
27
28
  } from "../render-seam.ts"
28
29
  import {
29
30
  ACTION_GLOBAL,
@@ -36,10 +37,12 @@ import {
36
37
  LOCALES_GLOBAL,
37
38
  ROOT_ATTRIBUTE,
38
39
  ROUTE_GLOBAL,
40
+ SWAP_HEADER,
39
41
  } from "../render-seam.ts"
40
42
  import { PRERENDERED_GLOBAL } from "../router.ts"
41
43
  import { trustedHeadAttributes } from "./head-attributes.ts"
42
44
  import { isStaticMeta, mergeHeads } from "./head-merge.ts"
45
+ import { reportDeferredRejection } from "./request-abort.ts"
43
46
  import { PRE_HYDRATION_GUARD } from "./runtime-contract.ts"
44
47
  import { EXECUTABLE_SCRIPT_TYPES, INERT_SCRIPT_TYPES } from "./script-types.ts"
45
48
 
@@ -171,6 +174,14 @@ export interface RenderPageOptions {
171
174
  readonly matchChain?: MatchChain
172
175
  /** HTTP status for the response (default 200; e.g. 404 for a not-found page). */
173
176
  readonly status?: number
177
+ /**
178
+ * The slot an `x-nifra-swap` request names. When the adapter renders slots and nothing is
179
+ * deferred, the response is that slot's HTML alone; otherwise the whole document.
180
+ */
181
+ readonly swapSlot?: string
182
+ /** Chain indices the adapter renders inside the slot wrappers of a kept page (`keepAlive`), so the
183
+ * browser can keep the page this document shows without remounting it. */
184
+ readonly slots?: readonly number[]
174
185
  /** Extra response headers - e.g. the `cache-control` a terminal status page wants. `content-type`
175
186
  * is ignored (the document is always HTML) and the ISR freshness header is applied after these,
176
187
  * so neither can be overridden from here. */
@@ -205,6 +216,11 @@ export interface RenderPageOptions {
205
216
  /** CSP nonce applied to every framework-owned executable script in this document. Nonce-bearing
206
217
  * documents are marked `private, no-store` so a request-specific nonce is never replayed. */
207
218
  readonly nonce?: string
219
+ /** The request's abort signal. A deferred value that rejects because it aborted is the client
220
+ * leaving, not a failure, so the server log skips it. Omit ⇒ every rejection is logged. */
221
+ readonly signal?: AbortSignal
222
+ /** Where a rejected deferred value is logged: the app's error log. Omit ⇒ the console. */
223
+ readonly logError?: ((error: unknown) => void) | undefined
208
224
  /**
209
225
  * A hash-based Content-Security-Policy from `createCspPolicy`. The document then carries `nonce`
210
226
  * only when it needs one (it defers a value, or a head tag names the nonce); otherwise it renders
@@ -262,6 +278,18 @@ export function renderPage(options: RenderPageInput): MaybePromise<Response> {
262
278
  return page instanceof Promise ? page.then((p) => p.toResponse()) : page.toResponse()
263
279
  }
264
280
 
281
+ // Every option of an adapter call, for a render with slot wrappers (only a keep-alive app's).
282
+ const streamOptions = (
283
+ nonce: string | undefined,
284
+ parts: DocumentParts | undefined,
285
+ slots: readonly number[],
286
+ ): RenderStreamOptions => {
287
+ const options: { -readonly [K in keyof RenderStreamOptions]: RenderStreamOptions[K] } = { slots }
288
+ if (nonce !== undefined) options.nonce = nonce
289
+ if (parts !== undefined) options.parts = parts
290
+ return options
291
+ }
292
+
265
293
  export function renderPageResult(options: RenderPageInput): MaybePromise<RenderedPage> {
266
294
  const { csp } = options
267
295
  if (csp !== undefined) {
@@ -512,6 +540,8 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
512
540
  if (name !== "content-type") headers[name] = value
513
541
  }
514
542
  }
543
+ if (adapter.renderSlot !== undefined)
544
+ headers.vary = headers.vary === undefined ? SWAP_HEADER : `${headers.vary}, ${SWAP_HEADER}`
515
545
  // ISR: advertise the route's freshness so a `withISR` wrapper can set this page's cache TTL. A
516
546
  // dedicated header (not the action-revalidation `x-nifra-revalidate`) so the TTL channel never aliases
517
547
  // the client's path-list channel.
@@ -551,6 +581,17 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
551
581
  ...(locale === undefined ? {} : { locale }),
552
582
  }
553
583
 
584
+ const { swapSlot, ...documentOptions } = options
585
+ const slots = options.slots
586
+ if (swapSlot !== undefined && allDeferred.length === 0 && adapter.renderSlot !== undefined) {
587
+ const slotPage = (slotHtml: string | undefined): MaybePromise<RenderedPage> =>
588
+ slotHtml === undefined
589
+ ? renderPageResult(documentOptions)
590
+ : new BufferedRenderedPage(slotHtml, status, swapHeaders(headers, swapSlot))
591
+ const out = adapter.renderSlot(chain, renderProps, swapSlot)
592
+ return out instanceof Promise ? out.then(slotPage) : slotPage(out)
593
+ }
594
+
554
595
  // Fast path: nothing `defer()`s and the adapter can render synchronously to a string → buffer the
555
596
  // whole document in one pass. Progressive streaming only benefits pages with deferred boundaries
556
597
  // (those take the streaming path below); for a plain page the streaming pipeline + the framework's
@@ -571,6 +612,9 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
571
612
  headers,
572
613
  streamNonce,
573
614
  streamNonce === nonce ? nonceAttr : ` nonce="${escapeAttr(streamNonce ?? "")}"`,
615
+ options.signal,
616
+ options.slots,
617
+ options.logError,
574
618
  ).then((response) => new ResponseRenderedPage(response))
575
619
  const last = chain[chain.length - 1]
576
620
  const leaf = isWeakKey(last) ? last : undefined
@@ -596,15 +640,17 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
596
640
  return streamed(renderNonce)
597
641
  }
598
642
  const out =
599
- parts !== undefined
600
- ? adapter.renderToString(
601
- chain,
602
- renderProps,
603
- renderNonce === undefined ? { parts } : { nonce: renderNonce, parts },
604
- )
605
- : renderNonce === undefined
606
- ? adapter.renderToString(chain, renderProps)
607
- : adapter.renderToString(chain, renderProps, { nonce: renderNonce })
643
+ slots !== undefined
644
+ ? adapter.renderToString(chain, renderProps, streamOptions(renderNonce, parts, slots))
645
+ : parts !== undefined
646
+ ? adapter.renderToString(
647
+ chain,
648
+ renderProps,
649
+ renderNonce === undefined ? { parts } : { nonce: renderNonce, parts },
650
+ )
651
+ : renderNonce === undefined
652
+ ? adapter.renderToString(chain, renderProps)
653
+ : adapter.renderToString(chain, renderProps, { nonce: renderNonce })
608
654
  const page = (bodyHtml: string | undefined): MaybePromise<RenderedPage> => {
609
655
  if (bodyHtml === undefined) {
610
656
  // The render suspended on something only the streaming renderer waits for (React's `use`
@@ -643,6 +689,9 @@ async function renderStreamedPage(
643
689
  headers: Record<string, string>,
644
690
  nonce: string | undefined,
645
691
  nonceAttr: string,
692
+ signal: AbortSignal | undefined,
693
+ slots: readonly number[] | undefined,
694
+ logError: ((error: unknown) => void) | undefined,
646
695
  ): Promise<Response> {
647
696
  // Streaming path - required for `defer()` (progressive `<Await>` resolution) and used by any adapter
648
697
  // that doesn't implement `renderToString`. Awaiting `renderToStream` resolves on shell-readiness
@@ -652,21 +701,54 @@ async function renderStreamedPage(
652
701
  const closeRoot = enc.encode(closeRootHtml)
653
702
  const tail = enc.encode(tailHtml)
654
703
  const appStream =
655
- parts !== undefined
656
- ? await adapter.renderToStream(
657
- chain,
658
- renderProps,
659
- nonce === undefined ? { parts } : { nonce, parts },
660
- )
661
- : nonce === undefined
662
- ? await adapter.renderToStream(chain, renderProps)
663
- : await adapter.renderToStream(chain, renderProps, { nonce })
704
+ slots !== undefined
705
+ ? await adapter.renderToStream(chain, renderProps, streamOptions(nonce, parts, slots))
706
+ : parts !== undefined
707
+ ? await adapter.renderToStream(
708
+ chain,
709
+ renderProps,
710
+ nonce === undefined ? { parts } : { nonce, parts },
711
+ )
712
+ : nonce === undefined
713
+ ? await adapter.renderToStream(chain, renderProps)
714
+ : await adapter.renderToStream(chain, renderProps, { nonce })
664
715
  // After the render: an adapter fills its document parts before handing back the stream.
665
716
  const shell = enc.encode(shellOf())
666
- const body = streamDocument(shell, appStream, closeRoot, allDeferred, tail, enc, nonceAttr)
717
+ const body = streamDocument(
718
+ shell,
719
+ appStream,
720
+ closeRoot,
721
+ allDeferred,
722
+ tail,
723
+ enc,
724
+ nonceAttr,
725
+ signal,
726
+ logError,
727
+ )
667
728
  return new Response(body, { status, headers })
668
729
  }
669
730
 
731
+ // A swap response shares its URL with the document, so like a data response it is never stored: a
732
+ // cache keyed on the URL alone would serve one slot in place of the page. A slot joins a page that
733
+ // already runs under its own policy, so the CSP header belongs to the document alone.
734
+ const DOCUMENT_ONLY_HEADERS = new Set([
735
+ ISR_REVALIDATE_HEADER,
736
+ ISR_REVALIDATE_TAGS_HEADER,
737
+ "content-security-policy",
738
+ ])
739
+
740
+ function swapHeaders(
741
+ document: Readonly<Record<string, string>>,
742
+ slot: string,
743
+ ): Record<string, string> {
744
+ const headers: Record<string, string> = {}
745
+ for (const [name, value] of Object.entries(document))
746
+ if (!DOCUMENT_ONLY_HEADERS.has(name)) headers[name] = value
747
+ headers["cache-control"] = "private, no-store"
748
+ headers[SWAP_HEADER] = slot
749
+ return headers
750
+ }
751
+
670
752
  class BufferedRenderedPage implements RenderedPage {
671
753
  readonly [RESPONSE_RESULT] = true
672
754
  private readonly body: string
@@ -732,6 +814,8 @@ function streamDocument(
732
814
  tail: Uint8Array,
733
815
  enc: TextEncoder,
734
816
  nonceAttr: string,
817
+ signal: AbortSignal | undefined,
818
+ logError: ((error: unknown) => void) | undefined,
735
819
  ): ReadableStream<Uint8Array> {
736
820
  const reader = stream.getReader()
737
821
  // A client that goes away cancels the body: the renderer stops, and nothing more is written.
@@ -756,7 +840,7 @@ function streamDocument(
756
840
  // A rejected (or unserializable) deferred streams __nifraReject (the client `<Await>`
757
841
  // surfaces it) - it must not break the whole body. Redact: stream a stable opaque code,
758
842
  // never the raw error text; log the real reason server-side.
759
- console.error("[nifra/web] deferred value rejected:", err)
843
+ reportDeferredRejection(signal, err, logError)
760
844
  return `<script${nonceAttr}>window.__nifraReject(${d.id},${serializeData(DEFERRED_ERROR_CODE)})</script>`
761
845
  }),
762
846
  )
@@ -1071,6 +1155,42 @@ export function statusPage(status: number, options: StatusPageOptions = {}): nev
1071
1155
  export interface RevalidateResult<T> {
1072
1156
  readonly __nifraRevalidate: readonly string[]
1073
1157
  readonly data: T
1158
+ /** What else the write made stale: cache tags and live keys. */
1159
+ readonly __nifraInvalidate?: {
1160
+ readonly tags: readonly string[]
1161
+ readonly keys: readonly string[]
1162
+ }
1163
+ }
1164
+
1165
+ /** `value` is what {@link revalidate} returned. */
1166
+ export function isRevalidateResult(value: unknown): value is RevalidateResult<unknown> {
1167
+ return typeof value === "object" && value !== null && "__nifraRevalidate" in value
1168
+ }
1169
+
1170
+ /** Cache tags and live keys an action's write made stale, beside its paths. */
1171
+ export interface RevalidateOptions {
1172
+ /** Cache tags: data caches, the page store and the CDN drop entries carrying them; live pages watching a tag as a key reload. */
1173
+ readonly tags?: readonly string[]
1174
+ /** Live keys (`ctx.live`): every open page watching one reloads it. */
1175
+ readonly keys?: readonly string[]
1176
+ }
1177
+
1178
+ const INVALIDATION_NAME = /^[A-Za-z][A-Za-z0-9._:/-]{0,127}$/
1179
+ const MAX_INVALIDATION_NAMES = 64
1180
+
1181
+ function invalidationNames(names: readonly string[] | undefined, kind: string): readonly string[] {
1182
+ if (names === undefined) return []
1183
+ if (!Array.isArray(names) || names.length > MAX_INVALIDATION_NAMES) {
1184
+ throw new TypeError(`[nifra/web] revalidate() takes at most ${MAX_INVALIDATION_NAMES} ${kind}`)
1185
+ }
1186
+ for (const name of names) {
1187
+ if (typeof name !== "string" || !INVALIDATION_NAME.test(name)) {
1188
+ throw new TypeError(
1189
+ `[nifra/web] revalidate() ${kind} are a letter followed by up to 127 of A-Z a-z 0-9 . _ : / -`,
1190
+ )
1191
+ }
1192
+ }
1193
+ return [...new Set(names)]
1074
1194
  }
1075
1195
 
1076
1196
  /**
@@ -1080,8 +1200,15 @@ export interface RevalidateResult<T> {
1080
1200
  * so a mutation can refresh views beyond the one that was submitted. `data` is still surfaced to the
1081
1201
  * component as `actionData` (the wrapper is transparent to `ActionData<typeof action>`).
1082
1202
  */
1083
- export function revalidate<T>(paths: readonly string[], data: T): RevalidateResult<T> {
1084
- return { __nifraRevalidate: paths, data }
1203
+ export function revalidate<T>(
1204
+ paths: readonly string[],
1205
+ data: T,
1206
+ options?: RevalidateOptions,
1207
+ ): RevalidateResult<T> {
1208
+ if (options === undefined) return { __nifraRevalidate: paths, data }
1209
+ const tags = invalidationNames(options.tags, "tags")
1210
+ const keys = invalidationNames(options.keys, "keys")
1211
+ return { __nifraRevalidate: paths, data, __nifraInvalidate: { tags, keys } }
1085
1212
  }
1086
1213
 
1087
1214
  /**
@@ -0,0 +1,26 @@
1
+ /** Whether `error` is a load giving up because its request was aborted - the client went away - rather
2
+ * than a failure: nobody reads the outcome, so there is nothing to report. */
3
+ export function isRequestAbort(signal: AbortSignal | undefined, error: unknown): boolean {
4
+ return (
5
+ signal?.aborted === true &&
6
+ (error === signal.reason || (error instanceof Error && error.name === "AbortError"))
7
+ )
8
+ }
9
+
10
+ /** Report a deferred value's rejection to `log`, the app's error log, or to the console without
11
+ * one or when it throws: both callers stream an opaque error code after this, which a failing
12
+ * logger must not cut off. A rejection the client's leaving caused is not reported. */
13
+ export function reportDeferredRejection(
14
+ signal: AbortSignal | undefined,
15
+ error: unknown,
16
+ log: ((error: unknown) => void) | undefined,
17
+ ): void {
18
+ if (isRequestAbort(signal, error)) return
19
+ if (log !== undefined) {
20
+ try {
21
+ log(error)
22
+ return
23
+ } catch {}
24
+ }
25
+ console.error("[nifra/web] deferred value rejected:", error)
26
+ }
@@ -12,6 +12,8 @@
12
12
 
13
13
  /** Modules whose exports become client stubs: `todos.fn.ts`, `x.fn.mjs`, or the extensionless `foo.fn`. */
14
14
  export const SERVER_FN_MODULE = /\.fn(\.[cm]?[jt]sx?)?$/
15
+ /** A server-function namespace: lowercase, dot/dash separated, so a mount cannot invent a path. */
16
+ export const SERVER_FN_NAMESPACE = /^[a-z0-9]+(?:[-.][a-z0-9]+)*$/
15
17
 
16
18
  import { jsStringLiteral } from "./js-string.ts"
17
19
  import { DATE_TAG, REVIVE_DATES_SOURCE } from "./revive-dates.ts"
@@ -105,7 +107,14 @@ function stringEnd(source: string, start: number): number {
105
107
  */
106
108
  export function serverFnNamespace(filePath: string): string {
107
109
  const base = filePath.replaceAll("\\", "/").split("/").pop() ?? ""
108
- return base.replace(SERVER_FN_MODULE, "")
110
+ const namespace = base.replace(SERVER_FN_MODULE, "")
111
+ // `serverFunctions()` refuses this namespace, so a stub calling it could never reach a function.
112
+ if (!SERVER_FN_NAMESPACE.test(namespace)) {
113
+ throw new Error(
114
+ `[nifra/fn] ${base} names the server-function namespace ${JSON.stringify(namespace)}, which serverFunctions() refuses - name the file in lowercase dot/dash segments (todo-items.fn.ts).`,
115
+ )
116
+ }
117
+ return namespace
109
118
  }
110
119
 
111
120
  /** The server functions a module declares, in source order. */