@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
@@ -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,
@@ -33,15 +34,19 @@ import {
33
34
  DATA_GLOBAL,
34
35
  HANDOVER_ID,
35
36
  LAYOUT_DATA_GLOBAL,
37
+ LIVE_GLOBAL,
36
38
  LOCALES_GLOBAL,
37
39
  ROOT_ATTRIBUTE,
38
40
  ROUTE_GLOBAL,
41
+ SWAP_HEADER,
39
42
  } from "../render-seam.ts"
40
43
  import { PRERENDERED_GLOBAL } from "../router.ts"
41
44
  import { trustedHeadAttributes } from "./head-attributes.ts"
42
45
  import { isStaticMeta, mergeHeads } from "./head-merge.ts"
46
+ import { reportDeferredRejection } from "./request-abort.ts"
43
47
  import { PRE_HYDRATION_GUARD } from "./runtime-contract.ts"
44
48
  import { EXECUTABLE_SCRIPT_TYPES, INERT_SCRIPT_TYPES } from "./script-types.ts"
49
+ import type { HeadersLike } from "./status-page.ts"
45
50
 
46
51
  const TEXT_ENCODER = new TextEncoder()
47
52
 
@@ -105,6 +110,11 @@ export interface RenderPageOptions {
105
110
  /** An action's data return (POST only) - surfaced to the page as `actionData` + serialized
106
111
  * for the client so post-POST hydration matches. Omit on GETs. */
107
112
  readonly actionData?: unknown
113
+ /**
114
+ * A live runtime's seed for this page (`createWebApp({ live })` sets it): handed to the client
115
+ * entry, which exchanges it for a grant instead of loading the page again.
116
+ */
117
+ readonly live?: unknown
108
118
  /**
109
119
  * URL of the built client entry (loaded as a module script).
110
120
  *
@@ -171,6 +181,14 @@ export interface RenderPageOptions {
171
181
  readonly matchChain?: MatchChain
172
182
  /** HTTP status for the response (default 200; e.g. 404 for a not-found page). */
173
183
  readonly status?: number
184
+ /**
185
+ * The slot an `x-nifra-swap` request names. When the adapter renders slots and nothing is
186
+ * deferred, the response is that slot's HTML alone; otherwise the whole document.
187
+ */
188
+ readonly swapSlot?: string
189
+ /** Chain indices the adapter renders inside the slot wrappers of a kept page (`keepAlive`), so the
190
+ * browser can keep the page this document shows without remounting it. */
191
+ readonly slots?: readonly number[]
174
192
  /** Extra response headers - e.g. the `cache-control` a terminal status page wants. `content-type`
175
193
  * is ignored (the document is always HTML) and the ISR freshness header is applied after these,
176
194
  * so neither can be overridden from here. */
@@ -205,6 +223,11 @@ export interface RenderPageOptions {
205
223
  /** CSP nonce applied to every framework-owned executable script in this document. Nonce-bearing
206
224
  * documents are marked `private, no-store` so a request-specific nonce is never replayed. */
207
225
  readonly nonce?: string
226
+ /** The request's abort signal. A deferred value that rejects because it aborted is the client
227
+ * leaving, not a failure, so the server log skips it. Omit ⇒ every rejection is logged. */
228
+ readonly signal?: AbortSignal
229
+ /** Where a rejected deferred value is logged: the app's error log. Omit ⇒ the console. */
230
+ readonly logError?: ((error: unknown) => void) | undefined
208
231
  /**
209
232
  * A hash-based Content-Security-Policy from `createCspPolicy`. The document then carries `nonce`
210
233
  * only when it needs one (it defers a value, or a head tag names the nonce); otherwise it renders
@@ -262,6 +285,18 @@ export function renderPage(options: RenderPageInput): MaybePromise<Response> {
262
285
  return page instanceof Promise ? page.then((p) => p.toResponse()) : page.toResponse()
263
286
  }
264
287
 
288
+ // Every option of an adapter call, for a render with slot wrappers (only a keep-alive app's).
289
+ const streamOptions = (
290
+ nonce: string | undefined,
291
+ parts: DocumentParts | undefined,
292
+ slots: readonly number[],
293
+ ): RenderStreamOptions => {
294
+ const options: { -readonly [K in keyof RenderStreamOptions]: RenderStreamOptions[K] } = { slots }
295
+ if (nonce !== undefined) options.nonce = nonce
296
+ if (parts !== undefined) options.parts = parts
297
+ return options
298
+ }
299
+
265
300
  export function renderPageResult(options: RenderPageInput): MaybePromise<RenderedPage> {
266
301
  const { csp } = options
267
302
  if (csp !== undefined) {
@@ -388,6 +423,8 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
388
423
  options.serverComponents === undefined
389
424
  ? ""
390
425
  : `,"__NIFRA_COMPONENTS__":${serializeData(options.serverComponents)}`
426
+ const liveTail =
427
+ options.live === undefined ? "" : `,"${LIVE_GLOBAL}":${serializeData(options.live)}`
391
428
  const deferredRuntime =
392
429
  allDeferred.length > 0 ? `<script${nonceAttr}>${DEFERRED_RUNTIME}</script>` : ""
393
430
  // The regex only injects the CSP nonce into the (constant) hydration head; with no nonce it's a
@@ -500,7 +537,7 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
500
537
  // Tail - the page-state handover + the client module. Module scripts defer (run after parse), so
501
538
  // the handover + every streamed deferred resolution are in the document before the entry hydrates.
502
539
  const tailHtml = hydrate
503
- ? `${slot.tailPre}${serializeData(forClient)}${action}${layoutTail}${boundaryTail}${componentTail}${slot.tailPost}`
540
+ ? `${slot.tailPre}${serializeData(forClient)}${action}${layoutTail}${boundaryTail}${componentTail}${liveTail}${slot.tailPost}`
504
541
  : (slot.tailPost as string)
505
542
  const headers: Record<string, string> = { "content-type": "text/html; charset=utf-8" }
506
543
  if (locale !== undefined) headers["content-language"] = lang
@@ -512,6 +549,8 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
512
549
  if (name !== "content-type") headers[name] = value
513
550
  }
514
551
  }
552
+ if (adapter.renderSlot !== undefined)
553
+ headers.vary = headers.vary === undefined ? SWAP_HEADER : `${headers.vary}, ${SWAP_HEADER}`
515
554
  // ISR: advertise the route's freshness so a `withISR` wrapper can set this page's cache TTL. A
516
555
  // dedicated header (not the action-revalidation `x-nifra-revalidate`) so the TTL channel never aliases
517
556
  // the client's path-list channel.
@@ -551,6 +590,17 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
551
590
  ...(locale === undefined ? {} : { locale }),
552
591
  }
553
592
 
593
+ const { swapSlot, ...documentOptions } = options
594
+ const slots = options.slots
595
+ if (swapSlot !== undefined && allDeferred.length === 0 && adapter.renderSlot !== undefined) {
596
+ const slotPage = (slotHtml: string | undefined): MaybePromise<RenderedPage> =>
597
+ slotHtml === undefined
598
+ ? renderPageResult(documentOptions)
599
+ : new BufferedRenderedPage(slotHtml, status, swapHeaders(headers, swapSlot))
600
+ const out = adapter.renderSlot(chain, renderProps, swapSlot)
601
+ return out instanceof Promise ? out.then(slotPage) : slotPage(out)
602
+ }
603
+
554
604
  // Fast path: nothing `defer()`s and the adapter can render synchronously to a string → buffer the
555
605
  // whole document in one pass. Progressive streaming only benefits pages with deferred boundaries
556
606
  // (those take the streaming path below); for a plain page the streaming pipeline + the framework's
@@ -571,6 +621,9 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
571
621
  headers,
572
622
  streamNonce,
573
623
  streamNonce === nonce ? nonceAttr : ` nonce="${escapeAttr(streamNonce ?? "")}"`,
624
+ options.signal,
625
+ options.slots,
626
+ options.logError,
574
627
  ).then((response) => new ResponseRenderedPage(response))
575
628
  const last = chain[chain.length - 1]
576
629
  const leaf = isWeakKey(last) ? last : undefined
@@ -596,15 +649,17 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
596
649
  return streamed(renderNonce)
597
650
  }
598
651
  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 })
652
+ slots !== undefined
653
+ ? adapter.renderToString(chain, renderProps, streamOptions(renderNonce, parts, slots))
654
+ : parts !== undefined
655
+ ? adapter.renderToString(
656
+ chain,
657
+ renderProps,
658
+ renderNonce === undefined ? { parts } : { nonce: renderNonce, parts },
659
+ )
660
+ : renderNonce === undefined
661
+ ? adapter.renderToString(chain, renderProps)
662
+ : adapter.renderToString(chain, renderProps, { nonce: renderNonce })
608
663
  const page = (bodyHtml: string | undefined): MaybePromise<RenderedPage> => {
609
664
  if (bodyHtml === undefined) {
610
665
  // The render suspended on something only the streaming renderer waits for (React's `use`
@@ -643,6 +698,9 @@ async function renderStreamedPage(
643
698
  headers: Record<string, string>,
644
699
  nonce: string | undefined,
645
700
  nonceAttr: string,
701
+ signal: AbortSignal | undefined,
702
+ slots: readonly number[] | undefined,
703
+ logError: ((error: unknown) => void) | undefined,
646
704
  ): Promise<Response> {
647
705
  // Streaming path - required for `defer()` (progressive `<Await>` resolution) and used by any adapter
648
706
  // that doesn't implement `renderToString`. Awaiting `renderToStream` resolves on shell-readiness
@@ -652,21 +710,54 @@ async function renderStreamedPage(
652
710
  const closeRoot = enc.encode(closeRootHtml)
653
711
  const tail = enc.encode(tailHtml)
654
712
  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 })
713
+ slots !== undefined
714
+ ? await adapter.renderToStream(chain, renderProps, streamOptions(nonce, parts, slots))
715
+ : parts !== undefined
716
+ ? await adapter.renderToStream(
717
+ chain,
718
+ renderProps,
719
+ nonce === undefined ? { parts } : { nonce, parts },
720
+ )
721
+ : nonce === undefined
722
+ ? await adapter.renderToStream(chain, renderProps)
723
+ : await adapter.renderToStream(chain, renderProps, { nonce })
664
724
  // After the render: an adapter fills its document parts before handing back the stream.
665
725
  const shell = enc.encode(shellOf())
666
- const body = streamDocument(shell, appStream, closeRoot, allDeferred, tail, enc, nonceAttr)
726
+ const body = streamDocument(
727
+ shell,
728
+ appStream,
729
+ closeRoot,
730
+ allDeferred,
731
+ tail,
732
+ enc,
733
+ nonceAttr,
734
+ signal,
735
+ logError,
736
+ )
667
737
  return new Response(body, { status, headers })
668
738
  }
669
739
 
740
+ // A swap response shares its URL with the document, so like a data response it is never stored: a
741
+ // cache keyed on the URL alone would serve one slot in place of the page. A slot joins a page that
742
+ // already runs under its own policy, so the CSP header belongs to the document alone.
743
+ const DOCUMENT_ONLY_HEADERS = new Set([
744
+ ISR_REVALIDATE_HEADER,
745
+ ISR_REVALIDATE_TAGS_HEADER,
746
+ "content-security-policy",
747
+ ])
748
+
749
+ function swapHeaders(
750
+ document: Readonly<Record<string, string>>,
751
+ slot: string,
752
+ ): Record<string, string> {
753
+ const headers: Record<string, string> = {}
754
+ for (const [name, value] of Object.entries(document))
755
+ if (!DOCUMENT_ONLY_HEADERS.has(name)) headers[name] = value
756
+ headers["cache-control"] = "private, no-store"
757
+ headers[SWAP_HEADER] = slot
758
+ return headers
759
+ }
760
+
670
761
  class BufferedRenderedPage implements RenderedPage {
671
762
  readonly [RESPONSE_RESULT] = true
672
763
  private readonly body: string
@@ -732,6 +823,8 @@ function streamDocument(
732
823
  tail: Uint8Array,
733
824
  enc: TextEncoder,
734
825
  nonceAttr: string,
826
+ signal: AbortSignal | undefined,
827
+ logError: ((error: unknown) => void) | undefined,
735
828
  ): ReadableStream<Uint8Array> {
736
829
  const reader = stream.getReader()
737
830
  // A client that goes away cancels the body: the renderer stops, and nothing more is written.
@@ -756,7 +849,7 @@ function streamDocument(
756
849
  // A rejected (or unserializable) deferred streams __nifraReject (the client `<Await>`
757
850
  // surfaces it) - it must not break the whole body. Redact: stream a stable opaque code,
758
851
  // never the raw error text; log the real reason server-side.
759
- console.error("[nifra/web] deferred value rejected:", err)
852
+ reportDeferredRejection(signal, err, logError)
760
853
  return `<script${nonceAttr}>window.__nifraReject(${d.id},${serializeData(DEFERRED_ERROR_CODE)})</script>`
761
854
  }),
762
855
  )
@@ -857,24 +950,6 @@ export function redirect(location: string, options: RedirectOptions = {}): Respo
857
950
  })
858
951
  }
859
952
 
860
- /**
861
- * Brand marking a `Response` as a **terminal-status signal** rather than a response to serve verbatim.
862
- *
863
- * A loader that throws a bare `Response` has always been passed straight through - that is how
864
- * `throw redirect(...)` works, and apps rely on it. So "render the `_404` boundary at 404" cannot be
865
- * expressed by throwing a plain `Response`: the framework would have no way to tell it apart from a
866
- * hand-rolled body the app wants served exactly as written. The brand is that distinction, and
867
- * checking it *before* the pass-through is what keeps every existing throw working unchanged.
868
- *
869
- * `Symbol.for` matches `RESPONSE_RESULT` above: a registry symbol survives two copies of this module,
870
- * which a `unique symbol` would not - and a duplicate-install turning a 404 into a raw 404 body would
871
- * be a maddening bug to trace.
872
- */
873
- /** Derived from the `Headers` constructor rather than the DOM lib's `HeadersInit`: `@nifrajs/web`
874
- * is consumed from DOM-free programs (the root tsconfig runs `types: ["bun"]`), and naming the DOM
875
- * type here would break their build. Same workaround as `SSEInit.headers` in core. */
876
- export type HeadersLike = ConstructorParameters<typeof Headers>[0]
877
-
878
953
  /**
879
954
  * The signatures a **duplicate module instance** produces during SSR, across engines.
880
955
  *
@@ -977,8 +1052,6 @@ export const layoutErrorId = (err: unknown): string | undefined =>
977
1052
  ? ((err as Record<symbol, unknown>)[LAYOUT_ERROR_ID] as string | undefined)
978
1053
  : undefined
979
1054
 
980
- export const STATUS_SIGNAL = Symbol.for("nifra.web.status-signal")
981
-
982
1055
  /** Reason phrases for the plain-text fallback, used only when the app authored no `_<status>` and no
983
1056
  * `_404` page. Deliberately not exhaustive - anything unlisted falls back to "Error", which is more
984
1057
  * useful than shipping a table of every RFC status for a body almost nobody will see. */
@@ -993,84 +1066,49 @@ export const STATUS_TEXT: Readonly<Record<number, string>> = {
993
1066
  500: "Internal Server Error",
994
1067
  }
995
1068
 
996
- export interface StatusSignal extends Response {
997
- readonly [STATUS_SIGNAL]: { readonly status: number; readonly headers?: HeadersLike }
998
- }
999
-
1000
- export function isStatusSignal(value: unknown): value is StatusSignal {
1001
- return value instanceof Response && STATUS_SIGNAL in value
1002
- }
1003
-
1004
- function statusSignal(status: number, options: StatusPageOptions): never {
1005
- if (!Number.isInteger(status) || status < 400 || status > 599) {
1006
- throw new Error(
1007
- `[nifra/web] statusPage(${JSON.stringify(status)}) must be an integer 4xx or 5xx status. Use redirect() for 3xx, and return data for a successful render.`,
1008
- )
1069
+ /** The wrapper `revalidate()` returns: the action's `data` plus the paths it changed. A plain tagged
1070
+ * shape (not a class) so `@nifrajs/client`'s `ActionData` can unwrap it structurally without importing
1071
+ * from `@nifrajs/web`. `createWebApp` strips the wrapper - the client receives `data` as the body and
1072
+ * the paths via the `X-Nifra-Revalidate` header. */
1073
+ export interface RevalidateResult<T> {
1074
+ readonly __nifraRevalidate: readonly string[]
1075
+ readonly data: T
1076
+ /** What else the write made stale: cache tags and live keys. */
1077
+ readonly __nifraInvalidate?: {
1078
+ readonly tags: readonly string[]
1079
+ readonly keys: readonly string[]
1009
1080
  }
1010
- const response = new Response(null, { status })
1011
- // Non-enumerable so the brand never lands in a structured clone or a JSON log of the response.
1012
- Object.defineProperty(response, STATUS_SIGNAL, {
1013
- value: { status, ...(options.headers !== undefined ? { headers: options.headers } : {}) },
1014
- enumerable: false,
1015
- })
1016
- throw response
1017
1081
  }
1018
1082
 
1019
- /** Options shared by {@link notFound}, {@link gone}, and {@link statusPage}. */
1020
- export interface StatusPageOptions {
1021
- /**
1022
- * Extra headers for the rendered response - `cache-control` above all.
1023
- *
1024
- * The defaults differ by status for a reason: a 404 may be a race with publication, so it wants a
1025
- * short TTL, while a 410 is a promise that the URL is permanently gone and can be cached hard.
1026
- * Getting this wrong is the difference between a crawler dropping a dead URL and re-fetching it
1027
- * for weeks.
1028
- */
1029
- readonly headers?: HeadersLike
1083
+ /** `value` is what {@link revalidate} returned. */
1084
+ export function isRevalidateResult(value: unknown): value is RevalidateResult<unknown> {
1085
+ return typeof value === "object" && value !== null && "__nifraRevalidate" in value
1030
1086
  }
1031
1087
 
1032
- /**
1033
- * Render the nearest `_404` page at status **404**. `throw` it from a loader when the record does not
1034
- * exist.
1035
- *
1036
- * This is the fix for the soft 404: a matched route whose loader finds nothing has otherwise no way to
1037
- * answer 404, so the path of least resistance is to return empty data and render "not found" inside a
1038
- * **200**. That looks correct in a browser and is invisible in review, which is why it ships and stays
1039
- * shipped - while search engines penalise it and keep the dead URL indexed.
1040
- *
1041
- * Returns `never`: these throw, so a loader narrows without a redundant `return`, and the type states
1042
- * the thing the "loaders `throw` redirect, actions `return` it" rule already trips people on.
1043
- */
1044
- export function notFound(options: StatusPageOptions = {}): never {
1045
- return statusSignal(404, options)
1088
+ /** Cache tags and live keys an action's write made stale, beside its paths. */
1089
+ export interface RevalidateOptions {
1090
+ /** Cache tags: data caches, the page store and the CDN drop entries carrying them; live pages watching a tag as a key reload. */
1091
+ readonly tags?: readonly string[]
1092
+ /** Live keys (`ctx.live`): every open page watching one reloads it. */
1093
+ readonly keys?: readonly string[]
1046
1094
  }
1047
1095
 
1048
- /**
1049
- * Render a terminal page at status **410 Gone**. `throw` it from a loader for a record that existed and
1050
- * was deliberately removed - a withdrawn listing, a deleted post.
1051
- *
1052
- * 410 is not a pedantic 404: it tells a crawler to **drop** the URL rather than re-fetch it for weeks
1053
- * on the assumption the 404 was transient. Uses `_410.tsx` if the app has one, otherwise `_404`.
1054
- */
1055
- export function gone(options: StatusPageOptions = {}): never {
1056
- return statusSignal(410, options)
1057
- }
1058
-
1059
- /**
1060
- * Render a terminal page at any 4xx/5xx status - the escape hatch behind {@link notFound} and
1061
- * {@link gone} (402, 451, …). Uses `_<status>.tsx` if present, otherwise `_404`.
1062
- */
1063
- export function statusPage(status: number, options: StatusPageOptions = {}): never {
1064
- return statusSignal(status, options)
1065
- }
1096
+ const INVALIDATION_NAME = /^[A-Za-z][A-Za-z0-9._:/-]{0,127}$/
1097
+ const MAX_INVALIDATION_NAMES = 64
1066
1098
 
1067
- /** The wrapper `revalidate()` returns: the action's `data` plus the paths it changed. A plain tagged
1068
- * shape (not a class) so `@nifrajs/client`'s `ActionData` can unwrap it structurally without importing
1069
- * from `@nifrajs/web`. `createWebApp` strips the wrapper - the client receives `data` as the body and
1070
- * the paths via the `X-Nifra-Revalidate` header. */
1071
- export interface RevalidateResult<T> {
1072
- readonly __nifraRevalidate: readonly string[]
1073
- readonly data: T
1099
+ function invalidationNames(names: readonly string[] | undefined, kind: string): readonly string[] {
1100
+ if (names === undefined) return []
1101
+ if (!Array.isArray(names) || names.length > MAX_INVALIDATION_NAMES) {
1102
+ throw new TypeError(`[nifra/web] revalidate() takes at most ${MAX_INVALIDATION_NAMES} ${kind}`)
1103
+ }
1104
+ for (const name of names) {
1105
+ if (typeof name !== "string" || !INVALIDATION_NAME.test(name)) {
1106
+ throw new TypeError(
1107
+ `[nifra/web] revalidate() ${kind} are a letter followed by up to 127 of A-Z a-z 0-9 . _ : / -`,
1108
+ )
1109
+ }
1110
+ }
1111
+ return [...new Set(names)]
1074
1112
  }
1075
1113
 
1076
1114
  /**
@@ -1080,8 +1118,15 @@ export interface RevalidateResult<T> {
1080
1118
  * so a mutation can refresh views beyond the one that was submitted. `data` is still surfaced to the
1081
1119
  * component as `actionData` (the wrapper is transparent to `ActionData<typeof action>`).
1082
1120
  */
1083
- export function revalidate<T>(paths: readonly string[], data: T): RevalidateResult<T> {
1084
- return { __nifraRevalidate: paths, data }
1121
+ export function revalidate<T>(
1122
+ paths: readonly string[],
1123
+ data: T,
1124
+ options?: RevalidateOptions,
1125
+ ): RevalidateResult<T> {
1126
+ if (options === undefined) return { __nifraRevalidate: paths, data }
1127
+ const tags = invalidationNames(options.tags, "tags")
1128
+ const keys = invalidationNames(options.keys, "keys")
1129
+ return { __nifraRevalidate: paths, data, __nifraInvalidate: { tags, keys } }
1085
1130
  }
1086
1131
 
1087
1132
  /**
@@ -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. */
@@ -0,0 +1,136 @@
1
+ const isPlain = (value: unknown): value is Record<string, unknown> =>
2
+ typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype
3
+
4
+ const TOO_DEEP = Symbol("data too deep")
5
+
6
+ // Client loaders may return graphs, unlike JSON. Check the whole graph before comparing: a cycle
7
+ // inside a new subtree or beyond the array sample must not acquire copied parents. Shared references
8
+ // and accessors also keep the fresh result intact, rather than changing aliases or invoking getters.
9
+ function isTree(value: unknown, seen: Set<object>, depth: number): boolean {
10
+ if (!Array.isArray(value) && !isPlain(value)) return true
11
+ if (depth >= 128 || seen.has(value)) return false
12
+ seen.add(value)
13
+ for (const key of Reflect.ownKeys(value)) {
14
+ const field = Object.getOwnPropertyDescriptor(value, key)
15
+ if (
16
+ field === undefined ||
17
+ !("value" in field) ||
18
+ (!field.enumerable && !(Array.isArray(value) && key === "length")) ||
19
+ !isTree(field.value, seen, depth + 1)
20
+ )
21
+ return false
22
+ }
23
+ return true
24
+ }
25
+
26
+ /**
27
+ * Structural sharing for reloaded route data. Plain objects and arrays compare by content and Dates
28
+ * by time; other values and symbol-bearing objects come from `next`. Non-tree client data stays
29
+ * intact. `plainJson` is only for freshly parsed JSON, before any client-loader transformation.
30
+ *
31
+ * Array elements with an `id` pair by it before position, preserving unchanged rows on a prepend or
32
+ * reorder. An array whose first 32 elements keep nothing is taken whole, without a full old-row index.
33
+ */
34
+ export function shareData<T>(prev: unknown, next: T, plainJson?: boolean): T
35
+ export function shareData(prev: unknown, next: unknown, plainJson = false): unknown {
36
+ if (Object.is(prev, next)) return prev
37
+ if (!plainJson && !isTree(next, new Set(), 0)) return next
38
+ try {
39
+ return share(prev, next, 0, plainJson)
40
+ } catch (error) {
41
+ if (error === TOO_DEEP) return next
42
+ throw error
43
+ }
44
+ }
45
+
46
+ // Client loaders can attach metadata that an enumerable-string-key copy would lose. Parsed JSON has
47
+ // no symbol keys, and the check would cost a third of a reload that changed one row.
48
+ const hasSymbols = (a: object, b: object): boolean =>
49
+ Object.getOwnPropertySymbols(a).length > 0 || Object.getOwnPropertySymbols(b).length > 0
50
+
51
+ function share(prev: unknown, next: unknown, depth: number, json: boolean): unknown {
52
+ if (Object.is(prev, next)) return prev
53
+ if (prev instanceof Date)
54
+ return next instanceof Date && prev.getTime() === next.getTime() ? prev : next
55
+ if (Array.isArray(next)) {
56
+ if (!Array.isArray(prev) || (!json && hasSymbols(prev, next))) return next
57
+ if (depth >= 128) throw TOO_DEEP
58
+ const out: unknown[] = []
59
+ let same = prev.length === next.length
60
+ let byId: Map<unknown, number> | undefined
61
+ let completeIndex = false
62
+ // Which old elements a new one took, from the first id lookup on: each pairs at most once, so
63
+ // duplicate ids never put one old object at two places. Every element before that lookup took
64
+ // its own position.
65
+ let taken: Uint8Array | undefined
66
+ let kept = 0
67
+ let i = 0
68
+ for (const item of next) {
69
+ let old: unknown = taken?.[i] === 1 ? undefined : prev[i]
70
+ const id = isPlain(item) ? item.id : undefined
71
+ if (id !== undefined && !(isPlain(old) && old.id === id)) {
72
+ if (taken === undefined) {
73
+ taken = new Uint8Array(prev.length)
74
+ taken.fill(1, 0, i)
75
+ }
76
+ if (byId === undefined || (i >= 32 && !completeIndex)) {
77
+ // Index only IDs needed by the sample until a kept row makes a full index worthwhile.
78
+ const wanted =
79
+ i < 32
80
+ ? new Set(next.slice(0, 32).map((value) => (isPlain(value) ? value.id : undefined)))
81
+ : undefined
82
+ completeIndex = wanted === undefined
83
+ let min = Number.POSITIVE_INFINITY
84
+ let max = Number.NEGATIVE_INFINITY
85
+ for (const key of wanted ?? []) {
86
+ if (typeof key !== "number" || Number.isNaN(key)) {
87
+ min = Number.NEGATIVE_INFINITY
88
+ max = Number.POSITIVE_INFINITY
89
+ break
90
+ }
91
+ min = Math.min(min, key)
92
+ max = Math.max(max, key)
93
+ }
94
+ byId = new Map()
95
+ for (let at = 0; at < prev.length; at++) {
96
+ const entry = prev[at]
97
+ if (!isPlain(entry)) continue
98
+ const entryId = entry.id
99
+ if (
100
+ wanted === undefined ||
101
+ ((typeof entryId !== "number" || !(entryId < min || entryId > max)) &&
102
+ wanted.has(entryId))
103
+ )
104
+ byId.set(entryId, at)
105
+ }
106
+ }
107
+ const at = byId.get(id)
108
+ old = at === undefined || taken[at] === 1 ? undefined : prev[at]
109
+ if (at !== undefined) taken[at] = 1
110
+ } else if (taken !== undefined && i < taken.length && old !== undefined) {
111
+ taken[i] = 1
112
+ }
113
+ const value = share(old, item, depth + 1, json)
114
+ out.push(value)
115
+ if (value === old) kept++
116
+ else if (i === 31 && kept === 0) return next
117
+ if (value !== prev[i]) same = false
118
+ i++
119
+ }
120
+ return same ? prev : out
121
+ }
122
+ if (!isPlain(prev) || !isPlain(next) || (!json && hasSymbols(prev, next))) return next
123
+ if (depth >= 128) throw TOO_DEEP
124
+ const keys = Object.keys(next)
125
+ let same = keys.length === Object.keys(prev).length
126
+ const out: Record<string, unknown> = {}
127
+ for (const key of keys) {
128
+ // A parsed payload can own a `__proto__` key, which a copy would turn into its prototype.
129
+ if (key === "__proto__") return next
130
+ const old = prev[key]
131
+ const value = share(old, next[key], depth + 1, json)
132
+ if (value !== old || (old === undefined && !Object.hasOwn(prev, key))) same = false
133
+ out[key] = value
134
+ }
135
+ return same ? prev : out
136
+ }