@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
package/dist/router.js CHANGED
@@ -7,14 +7,22 @@
7
7
  import { decodeRouteParams } from "@nifrajs/core/pattern";
8
8
  import { Router as CoreRouter } from "@nifrajs/core/router";
9
9
  import { parseNdjsonData } from "./deferred.js";
10
+ import { epochOf, IDENTITY_CHANNEL, onIdentityChange } from "./identity.js";
11
+ import { present } from "./internal/present.js";
10
12
  import { parsePageData } from "./internal/revive-dates.js";
11
13
  import { appUrl, requestPattern } from "./internal/route-path.js";
14
+ import { shareData } from "./internal/share-data.js";
12
15
  import { isClientOnlySearchChange } from "./search.js";
13
16
  /**
14
17
  * Request header that asks a nifra route's GET to return just the loader data as JSON (instead of
15
18
  * the full HTML document). Set by client-side navigation; read by `createWebApp`'s GET handler.
16
19
  */
17
20
  export const DATA_HEADER = "x-nifra-data";
21
+ /**
22
+ * Sent beside {@link DATA_HEADER} by a prefetch. A partially prerendered route answers it with its
23
+ * shell data and starts no hole: nothing could cancel that work if the link is never followed.
24
+ */
25
+ export const PREFETCH_HEADER = "x-nifra-prefetch";
18
26
  /**
19
27
  * Global the server injects (`createWebApp({ prerenderedPaths })`) listing the SSG-prerendered paths.
20
28
  * The client's default data fetch reads it: a soft-nav INTO a prerendered route fetches its static
@@ -71,6 +79,18 @@ export const RETAIN_HEADER = "x-nifra-retain";
71
79
  * the full-chain case.
72
80
  */
73
81
  export const NAV_FROM_HEADER = "x-nifra-from";
82
+ /**
83
+ * Sent on a data request that reloads the route instance on screen: the holes, comma separated, whose
84
+ * hosts the partially prerendered page holds. Each one's state then carries its fill (server HTML,
85
+ * plus props when it hydrates) instead of its data, so the client updates the host in place.
86
+ */
87
+ export const HOLE_FILL_HEADER = "x-nifra-hole-fill";
88
+ /**
89
+ * Holes of a partially prerendered page the client keeps, comma separated: they are not loaded, and
90
+ * each state says so. Only the live load endpoint sets it, for holes whose keys did not change. A name
91
+ * the route does not declare is refused with a 400.
92
+ */
93
+ export const KEEP_HOLES_HEADER = "x-nifra-keep-holes";
74
94
  /** Recognise the envelope without mistaking a plain loader object that happens to have a `data` key. */
75
95
  export function isRouteDataEnvelope(value) {
76
96
  return (typeof value === "object" &&
@@ -112,9 +132,16 @@ export function createMatcher(patterns) {
112
132
  /** Read a nifra data response: a deferred loader/action streams NDJSON (parse line 1 + settle
113
133
  * `<Await>` markers as resolution lines arrive); a non-deferred one returns a single JSON. Shared by
114
134
  * navigation fetches and action submits - both transports are identical. */
115
- const readResponseData = (res, signal) => (res.headers.get("content-type") ?? "").includes("application/x-ndjson") && res.body !== null
135
+ const readResponseData = (res, signal, read) => (res.headers.get("content-type") ?? "").includes("application/x-ndjson") && res.body !== null
116
136
  ? parseNdjsonData(res.body, signal)
117
- : res.text().then(parsePageData);
137
+ : res.text().then((text) => {
138
+ if (read === undefined)
139
+ return parsePageData(text);
140
+ if (text === read.previous)
141
+ return read;
142
+ read.text = text;
143
+ return parsePageData(text);
144
+ });
118
145
  // Index scan, not `/\/+$/` - the unanchored-start trailing-run replace backtracks quadratically and
119
146
  // the pathname is caller-controlled.
120
147
  const trimTrailingSlashes = (path) => {
@@ -169,7 +196,24 @@ const targetUrl = (to) => {
169
196
  }
170
197
  };
171
198
  const redirected = (to, cause) => Object.assign(new Error(`[nifra/web] redirected to ${to}`, { cause }), { redirectTo: to });
172
- const defaultFetchData = async (path, _match, signal, navigation) => {
199
+ /**
200
+ * A route data response as `fetchData` resolves it: a loader's in-app redirect rejects with its
201
+ * target, a terminal status becomes an envelope carrying it, and any other failure rejects.
202
+ */
203
+ export const routeDataOf = async (res, path, signal, read) => {
204
+ const redirectTo = res.headers.get(REDIRECT_HEADER);
205
+ if (redirectTo !== null)
206
+ throw redirected(redirectTo);
207
+ const status = Number(res.headers.get(STATUS_HEADER));
208
+ if (Number.isInteger(status) && status >= 400 && status <= 599) {
209
+ return { v: 1, data: null, status };
210
+ }
211
+ if (!res.ok)
212
+ throw new Error(`[nifra/web] navigation data fetch failed (${res.status}): ${path}`);
213
+ return readResponseData(res, signal, read);
214
+ };
215
+ /** The router's own transport, which a `fetchData` override may layer over. */
216
+ export const fetchRouteData = async (path, _match, signal, navigation, purpose, hosts, read) => {
173
217
  // SSG fast path: if this path was prerendered, its loader data is a static file - fetch that (no
174
218
  // worker). Falls through to the dynamic header-GET on any miss (file absent, e.g. a deferred route,
175
219
  // or a stale set), so it's always safe. Non-SSG apps have no global → the dynamic path, unchanged.
@@ -184,25 +228,19 @@ const defaultFetchData = async (path, _match, signal, navigation) => {
184
228
  if (prerenderedSetOf(prerendered).has(pathname)) {
185
229
  const staticRes = await fetch(appUrl(dataUrlFor(pathname)), { signal: signal ?? null });
186
230
  if (staticRes.ok)
187
- return readResponseData(staticRes, signal);
231
+ return readResponseData(staticRes, signal, read);
188
232
  }
189
233
  }
190
234
  const headers = new Headers({ [DATA_HEADER]: "1" });
235
+ if (purpose === "prefetch")
236
+ headers.set(PREFETCH_HEADER, "1");
237
+ if (hosts !== undefined && hosts.length > 0)
238
+ headers.set(HOLE_FILL_HEADER, hosts.join(","));
191
239
  if (navigation !== undefined && navigation.retain.length > 0) {
192
240
  headers.set(RETAIN_HEADER, navigation.retain.join(","));
193
241
  headers.set(NAV_FROM_HEADER, navigation.from);
194
242
  }
195
- const res = await fetch(appUrl(path), { headers, signal: signal ?? null });
196
- const redirectTo = res.headers.get(REDIRECT_HEADER);
197
- if (redirectTo !== null)
198
- throw redirected(redirectTo);
199
- const status = Number(res.headers.get(STATUS_HEADER));
200
- if (Number.isInteger(status) && status >= 400 && status <= 599) {
201
- return { v: 1, data: null, status };
202
- }
203
- if (!res.ok)
204
- throw new Error(`[nifra/web] navigation data fetch failed (${res.status}): ${path}`);
205
- return readResponseData(res, signal);
243
+ return routeDataOf(await fetch(appUrl(path), { headers, signal: signal ?? null }), path, signal, read);
206
244
  };
207
245
  /**
208
246
  * Create the agnostic router store. `navigate` is guarded by a monotonic generation so that when
@@ -224,9 +262,9 @@ const rawSearchOf = (path) => {
224
262
  const isClientActionResult = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
225
263
  export function createClientRouter(options) {
226
264
  const match = createMatcher(options.patterns);
227
- const fetchData = options.fetchData ?? defaultFetchData;
265
+ const fetchData = options.fetchData ?? fetchRouteData;
228
266
  // Start a handed-over list's fetch now, so the first navigation rarely waits for it.
229
- if (fetchData === defaultFetchData)
267
+ if (fetchData === fetchRouteData)
230
268
  prerenderedOf();
231
269
  // routeId → the route's client-only search keys. Read by reference (the generated entry keeps writing
232
270
  // to it as routes load), so a same-route nav always sees the current route's keys.
@@ -273,18 +311,68 @@ export function createClientRouter(options) {
273
311
  data: value.data,
274
312
  layoutData: value.layoutData,
275
313
  serverComponents: value.serverComponents,
314
+ live: value.live,
276
315
  ...(value.boundaries === undefined ? {} : { boundaries: value.boundaries }),
277
316
  });
317
+ // The path the page on screen was loaded from. While a boundary holds an intercepted target, `path`
318
+ // is that target's URL, so anything that reloads or caches the page must read this instead.
319
+ const pagePathOf = (current) => current.intercepted?.pagePath ?? current.path;
320
+ // `boundaries` with `name` holding a target's outcome. The URL names one target, so every other
321
+ // intercept boundary goes back to unresolved. A target that answered with a status, or failed, is
322
+ // an error state: its data never reaches the boundary.
323
+ const holdTarget = (boundaries, descriptor, outcome) => {
324
+ const next = {};
325
+ for (const [name, boundary] of Object.entries(boundaries ?? {})) {
326
+ next[name] =
327
+ typeof boundary.mode === "string" || name === descriptor.name
328
+ ? boundary
329
+ : {
330
+ name: boundary.name,
331
+ mode: boundary.mode,
332
+ status: "unresolved",
333
+ ...(boundary.errorId === undefined ? {} : { errorId: boundary.errorId }),
334
+ };
335
+ }
336
+ const base = {
337
+ name: descriptor.name,
338
+ mode: descriptor.mode,
339
+ ...(descriptor.errorId === undefined ? {} : { errorId: descriptor.errorId }),
340
+ };
341
+ next[descriptor.name] =
342
+ outcome === undefined || outcome.terminalRouteId !== undefined
343
+ ? { ...base, status: "error", error: { name: "Error", message: "Boundary failed" } }
344
+ : { ...base, status: "ready", data: outcome.data, live: outcome.live };
345
+ return next;
346
+ };
347
+ // `boundaries` with `name`'s action result set (or cleared, for `undefined`). Only a boundary showing
348
+ // its target's data carries one.
349
+ const withActionData = (boundaries, name, actionData) => {
350
+ const held = boundaries?.[name];
351
+ if (held === undefined || held.status !== "ready")
352
+ return boundaries;
353
+ const { actionData: _previous, ...rest } = held;
354
+ return { ...boundaries, [name]: actionData === undefined ? rest : { ...rest, actionData } };
355
+ };
278
356
  const loadRouteData = async (path, match, signal,
279
357
  // Offer the server the layout data on screen: it re-runs whichever of those loaders it must.
280
- keepLayouts) => {
358
+ keepLayouts, purpose, hosts,
359
+ // What this path shows, when this is a reload of it: the same response text again is that, unparsed.
360
+ shown) => {
281
361
  const retainFrom = keepLayouts
282
- ? state.layoutData && { path: state.path, layoutData: state.layoutData }
362
+ ? state.layoutData && { path: pagePathOf(state), layoutData: state.layoutData }
283
363
  : undefined;
284
364
  const retain = retainFrom?.layoutData.map((_, index) => index) ?? [];
285
- const payload = await fetchData(path, match, signal, retainFrom === undefined ? undefined : { from: retainFrom.path, retain });
365
+ const hooks = routeHooks[match.routeId];
366
+ // A client loader's output is not what the text says, and an opted-out route takes fresh data.
367
+ const read = {
368
+ previous: hooks?.clientLoader === undefined && hooks?.shareData !== false ? shown?.text : undefined,
369
+ };
370
+ const payload = await fetchData(path, match, signal, retainFrom === undefined ? undefined : { from: retainFrom.path, retain }, purpose, hosts, read);
371
+ if (shown !== undefined && payload === read)
372
+ return shown;
373
+ const text = read.text;
286
374
  if (!isRouteDataEnvelope(payload))
287
- return { data: payload };
375
+ return { data: payload, text };
288
376
  if (payload.status !== undefined) {
289
377
  const terminalRouteId = options.statusRoutes?.[payload.status] ?? options.statusRoutes?.[404];
290
378
  if (terminalRouteId === undefined) {
@@ -301,7 +389,27 @@ export function createClientRouter(options) {
301
389
  data: payload.data,
302
390
  layoutData,
303
391
  serverComponents: payload.serverComponents,
392
+ live: payload.live,
304
393
  ...(payload.boundaries !== undefined ? { boundaries: payload.boundaries } : {}),
394
+ text,
395
+ };
396
+ };
397
+ // A reload with every part equal to what its path shows replaced by that part, so what the reload
398
+ // did not change keeps its identity on screen (`shareData`), unless the route exports
399
+ // `shareData = false`.
400
+ const shareShown = (routeId, shown, loaded) => {
401
+ if (loaded.terminalRouteId !== undefined || routeHooks[routeId]?.shareData === false)
402
+ return loaded;
403
+ // Parsed JSON is a tree; a client loader/custom transport can return cyclic or aliased data.
404
+ const plainJson = loaded.text !== undefined && routeHooks[routeId]?.clientLoader === undefined;
405
+ return {
406
+ ...loaded,
407
+ data: shareData(shown.data, loaded.data, plainJson),
408
+ layoutData: shareData(shown.layoutData, loaded.layoutData, plainJson),
409
+ serverComponents: shareData(shown.serverComponents, loaded.serverComponents, plainJson),
410
+ ...(loaded.boundaries === undefined
411
+ ? {}
412
+ : { boundaries: shareData(shown.boundaries, loaded.boundaries, plainJson) }),
305
413
  };
306
414
  };
307
415
  /**
@@ -349,7 +457,10 @@ export function createClientRouter(options) {
349
457
  params: target.params,
350
458
  signal,
351
459
  body,
352
- serverLoader: async () => state.data,
460
+ // An action posted to the target an intercept boundary holds belongs to that target.
461
+ serverLoader: async () => state.intercepted !== undefined && pathnameOf(action) === pathnameOf(state.path)
462
+ ? state.boundaries?.[state.intercepted.boundary]?.data
463
+ : state.data,
353
464
  });
354
465
  if (result === undefined)
355
466
  return { body };
@@ -377,19 +488,15 @@ export function createClientRouter(options) {
377
488
  const loaded = await applyClientLoader(state.path, route, ac.signal, () => Promise.resolve(initial), initial);
378
489
  if (mine !== generation)
379
490
  return;
380
- if (loaded.terminalRouteId !== undefined) {
381
- await loadModule?.(loaded.terminalRouteId, state.path);
382
- state = { ...state, routeId: loaded.terminalRouteId, params: {}, data: null };
383
- }
384
- else {
385
- state = {
386
- ...state,
387
- ...dataFields(loaded),
388
- };
389
- }
491
+ // No terminal status can arrive here: the server already rendered this route (a status page
492
+ // included) and its data is the only server result the client loader sees.
493
+ state = { ...state, ...dataFields(loaded) };
390
494
  emit();
391
495
  };
392
496
  let state = options.initial;
497
+ // The page as last published from a load, with the response text it was read from. Every publish of
498
+ // page data replaces it, and a reload reuses it only for the same text, which says the same.
499
+ let shownPage;
393
500
  const listeners = new Set();
394
501
  let generation = 0;
395
502
  // Snapshot listeners to defend against accidental un/subscribe during notification. The adapters
@@ -406,12 +513,33 @@ export function createClientRouter(options) {
406
513
  // fresh: a link warmed as it scrolled into view can be clicked minutes later.
407
514
  const MAX_PREFETCH = 10;
408
515
  const PREFETCH_MAX_AGE_MS = 30_000;
516
+ // path -> [data, when it arrived, the identity epoch it was loaded under]
409
517
  const prefetched = new Map();
410
- const inflight = new Set();
518
+ // path -> the token of the prefetch loading it; a prefetch stores what it read only while its
519
+ // token is still there, so dropping the token discards a load that started before the drop.
520
+ const inflight = new Map();
521
+ // Read without the DOM types: this module also builds in tsconfigs that leave the DOM lib out.
522
+ const readEpoch = () => {
523
+ const cookie = Reflect.get(Reflect.get(globalThis, "document") ?? {}, "cookie");
524
+ return typeof cookie === "string" ? epochOf(cookie) : "";
525
+ };
411
526
  const freshPrefetch = (path) => {
412
527
  const held = prefetched.get(path);
413
- return held !== undefined && Date.now() - held[1] < PREFETCH_MAX_AGE_MS ? held[0] : undefined;
528
+ return held !== undefined &&
529
+ Date.now() - held[1] < PREFETCH_MAX_AGE_MS &&
530
+ held[2] === readEpoch()
531
+ ? held[0]
532
+ : undefined;
414
533
  };
534
+ // Warmed data was loaded before a mutation or for the previous visitor, so either one drops all of
535
+ // it, along with what a prefetch still in flight read.
536
+ const dropPrefetched = () => {
537
+ inflight.clear();
538
+ prefetched.clear();
539
+ };
540
+ onIdentityChange(dropPrefetched);
541
+ if (typeof BroadcastChannel !== "undefined")
542
+ new BroadcastChannel(IDENTITY_CHANNEL).addEventListener("message", dropPrefetched);
415
543
  // Keyed data cache (path → latest loader data + freshness). Written on every published data
416
544
  // (navigate/submit), read by `invalidate` (+ targeted revalidation and fetchers in later F16
417
545
  // increments). Bounded - evict the oldest-inserted past the cap (route data is small; this just
@@ -442,6 +570,8 @@ export function createClientRouter(options) {
442
570
  // `X-Nifra-Revalidate` handling in `submit`.
443
571
  const markStale = (paths) => {
444
572
  for (const p of paths) {
573
+ prefetched.delete(p);
574
+ inflight.delete(p);
445
575
  const entry = cache.get(p);
446
576
  if (entry !== undefined) {
447
577
  cache.set(p, {
@@ -477,6 +607,14 @@ export function createClientRouter(options) {
477
607
  const target = match(path);
478
608
  return target === null ? undefined : [path, target];
479
609
  };
610
+ // Load the status route a terminal answer for `at` renders. One inside layouts needs those layouts'
611
+ // loaders, which only a document request runs, so the caller loads `at` as a document instead.
612
+ const loadTerminal = async (routeId, at) => {
613
+ await loadModule?.(routeId, at);
614
+ if (options.layoutStatusRoutes?.[routeId] === true) {
615
+ throw new Error(`[nifra/web] ${routeId} renders inside layouts; loading ${at} as a document`);
616
+ }
617
+ };
480
618
  // Load `path` for the navigation or submit `mine`, following the redirects its loaders answer with
481
619
  // while they stay in the app (20 at most, as a browser allows).
482
620
  const loadFollowing = async (mine, path, target, signal, keepLayouts, usePrefetch) => {
@@ -494,12 +632,8 @@ export function createClientRouter(options) {
494
632
  prefetched.delete(at);
495
633
  try {
496
634
  const loaded = await applyClientLoader(at, route, signal, () => loadRouteData(at, route, signal, keepLayouts), prefetchedData);
497
- if (loaded.terminalRouteId !== undefined) {
498
- await loadModule?.(loaded.terminalRouteId, at);
499
- if (options.layoutStatusRoutes?.[loaded.terminalRouteId] === true) {
500
- throw new Error(`[nifra/web] ${loaded.terminalRouteId} renders inside layouts; loading ${at} as a document`);
501
- }
502
- }
635
+ if (loaded.terminalRouteId !== undefined)
636
+ await loadTerminal(loaded.terminalRouteId, at);
503
637
  return [at, route, loaded];
504
638
  }
505
639
  catch (error) {
@@ -510,9 +644,91 @@ export function createClientRouter(options) {
510
644
  }
511
645
  }
512
646
  };
513
- // Refetch the active route + republish, single-flight via the shared `generation`. Shared by `invalidate`
514
- // and targeted revalidation. Rejects if the fetch fails (clearing its own `pending` first).
515
- const refetchActive = async () => {
647
+ // Reload what is on screen: the page under its own path when `page`, and, while an intercept boundary
648
+ // holds a target, that target under its URL when `target`. The part not reloaded is kept as shown.
649
+ // Returns the fields to publish, both parts together. `route` is set when the page's reload names
650
+ // another route - the status route it answered with, or, on a status page, its URL's own route
651
+ // answering again - which then replaces the page and any boundary over it, so data is only ever
652
+ // rendered by the route it was loaded for. A target that fails becomes its
653
+ // boundary's error state rather than failing the page, as a dynamic boundary's load does. Nothing is
654
+ // cached once `mine` is superseded: a newer action's reload owns the cache.
655
+ const reloadActive = async (signal, page, target, mine) => {
656
+ const pagePath = pagePathOf(state);
657
+ const intercepted = state.intercepted;
658
+ const targetPath = state.path;
659
+ // The route the page's URL matches: the one on screen, except on a status page.
660
+ const pageRoute = match(pagePath) ?? { routeId: state.routeId, params: state.params };
661
+ const rematched = pageRoute.routeId !== state.routeId;
662
+ const targetRoute = intercepted !== undefined && target ? match(targetPath) : null;
663
+ // The page's own route shares with what the page shows; a status page answered by its URL's route
664
+ // has nothing of that route on screen.
665
+ const shown = rematched ? undefined : (shownPage ?? state);
666
+ const shownTarget = { data: intercepted && state.boundaries?.[intercepted.boundary]?.data };
667
+ // A hole on screen that is a host stays one: it reloads as a fill.
668
+ const hosts = rematched
669
+ ? []
670
+ : Object.values(state.boundaries ?? {})
671
+ .filter((held) => held.ppr === true)
672
+ .map((held) => held.name);
673
+ const [reloaded, targetOutcome] = await Promise.all([
674
+ page
675
+ ? (async () => {
676
+ if (rematched)
677
+ await loadModule?.(pageRoute.routeId, pagePath);
678
+ const loaded = await applyClientLoader(pagePath, pageRoute, signal, () => loadRouteData(pagePath, pageRoute, signal, false, undefined, hosts.length === 0 ? undefined : hosts, shown));
679
+ return shown === undefined ? loaded : shareShown(pageRoute.routeId, shown, loaded);
680
+ })()
681
+ : undefined,
682
+ targetRoute === null
683
+ ? undefined
684
+ : (async () => {
685
+ try {
686
+ await loadModule?.(targetRoute.routeId, targetPath);
687
+ const outcome = shareShown(targetRoute.routeId, shownTarget, await applyClientLoader(targetPath, targetRoute, signal, () => loadRouteData(targetPath, targetRoute, signal)));
688
+ if (mine === generation)
689
+ cachePut(targetPath, outcome);
690
+ return outcome;
691
+ }
692
+ catch {
693
+ return undefined;
694
+ }
695
+ })(),
696
+ ]);
697
+ if (reloaded?.terminalRouteId !== undefined)
698
+ await loadTerminal(reloaded.terminalRouteId, pagePath);
699
+ if (reloaded !== undefined && mine === generation) {
700
+ cachePut(pagePath, reloaded);
701
+ shownPage = reloaded;
702
+ }
703
+ const fields = dataFields(reloaded ?? state);
704
+ if (reloaded?.terminalRouteId !== undefined)
705
+ return { fields, route: { routeId: reloaded.terminalRouteId, params: {} } };
706
+ if (reloaded !== undefined && rematched)
707
+ return { fields, route: pageRoute };
708
+ if (intercepted === undefined)
709
+ return { fields };
710
+ const held = state.boundaries?.[intercepted.boundary];
711
+ if (held === undefined)
712
+ return { fields };
713
+ if (targetRoute === null)
714
+ return {
715
+ fields: {
716
+ ...fields,
717
+ boundaries: { ...(fields.boundaries ?? {}), [intercepted.boundary]: held },
718
+ },
719
+ };
720
+ // A reload keeps the result of the last action posted to the target, as the page keeps its own.
721
+ const boundaries = withActionData(holdTarget(fields.boundaries, held, targetOutcome), intercepted.boundary, held.actionData);
722
+ return { fields: { ...fields, ...(boundaries === undefined ? {} : { boundaries }) } };
723
+ };
724
+ // Refetch what is on screen + republish, single-flight via the shared `generation`. Shared by
725
+ // `invalidate` and targeted revalidation: `inScope` says which paths to refresh, and a page or held
726
+ // target outside it is kept. Rejects if the page's fetch fails (clearing its own `pending` first).
727
+ const refetchActive = async (inScope) => {
728
+ const page = inScope === undefined || inScope(pagePathOf(state));
729
+ const target = state.intercepted !== undefined && (inScope === undefined || inScope(state.path));
730
+ if (!page && !target)
731
+ return;
516
732
  const mine = ++generation;
517
733
  navAbort?.abort();
518
734
  const ac = new AbortController();
@@ -520,16 +736,22 @@ export function createClientRouter(options) {
520
736
  state = { ...state, pending: true };
521
737
  emit();
522
738
  try {
523
- const active = { routeId: state.routeId, params: state.params };
524
- const loaded = await applyClientLoader(state.path, active, ac.signal, () => loadRouteData(state.path, active, ac.signal));
739
+ const { fields, route } = await reloadActive(ac.signal, page, target, mine);
525
740
  if (mine !== generation)
526
741
  return; // superseded - drop the stale result
527
- cachePut(state.path, loaded);
528
- state = {
529
- ...state,
530
- ...dataFields(loaded),
531
- pending: false,
532
- };
742
+ // A terminal answer for the page (the session expired, the record is gone) replaces it, and any
743
+ // boundary over it, with its status route, as a navigation to this URL would.
744
+ state =
745
+ route === undefined
746
+ ? { ...state, ...fields, pending: false }
747
+ : {
748
+ routeId: route.routeId,
749
+ params: route.params,
750
+ path: state.path,
751
+ ...fields,
752
+ actionData: undefined,
753
+ pending: false,
754
+ };
533
755
  emit();
534
756
  }
535
757
  catch (err) {
@@ -558,8 +780,8 @@ export function createClientRouter(options) {
558
780
  // swallowed - a failed targeted refresh must not fail the mutation that triggered it.
559
781
  const refreshMounted = async (paths, skipActive) => {
560
782
  const jobs = [];
561
- if (!skipActive && paths.includes(state.path))
562
- jobs.push(refetchActive().catch(() => { }));
783
+ if (!skipActive)
784
+ jobs.push(refetchActive((path) => paths.includes(path)).catch(() => { }));
563
785
  for (const entry of fetchers.values())
564
786
  jobs.push(entry.refreshIfShowing(paths));
565
787
  await Promise.all(jobs);
@@ -569,6 +791,8 @@ export function createClientRouter(options) {
569
791
  let fGeneration = 0;
570
792
  let fAbort;
571
793
  let loadedPath;
794
+ // `data` as loaded, with the response text it was read from.
795
+ let shown;
572
796
  const fListeners = new Set();
573
797
  const fEmit = () => {
574
798
  if (fListeners.size > 0)
@@ -587,12 +811,16 @@ export function createClientRouter(options) {
587
811
  fState = { ...fState, pending: true };
588
812
  fEmit();
589
813
  try {
590
- const [, loaded] = await Promise.all([
814
+ // Loading the path again is a reload of what this fetcher shows.
815
+ const before = path === loadedPath ? shown : undefined;
816
+ const [, fresh] = await Promise.all([
591
817
  loadModule?.(matched.routeId),
592
- loadRouteData(path, matched, ac.signal),
818
+ loadRouteData(path, matched, ac.signal, false, undefined, undefined, before),
593
819
  ]);
594
820
  if (mine !== fGeneration)
595
821
  return; // a newer load/submit on THIS fetcher superseded us
822
+ const loaded = before === undefined ? fresh : shareShown(matched.routeId, before, fresh);
823
+ shown = loaded;
596
824
  // Record the loaded path only on SUCCESS - a thrown or superseded load must not
597
825
  // leave `loadedPath` pointing at a path this fetcher never actually showed, or a later
598
826
  // `X-Nifra-Revalidate` for it would spuriously refetch onto unexpected data.
@@ -640,6 +868,7 @@ export function createClientRouter(options) {
640
868
  body,
641
869
  headers: { [DATA_HEADER]: "1" },
642
870
  });
871
+ dropPrefetched();
643
872
  if (!res.ok)
644
873
  throw new Error(`[nifra/web] fetcher action failed (${res.status}): ${action}`);
645
874
  const actionData = res.status === 204 ? undefined : await readResponseData(res, ac.signal);
@@ -687,19 +916,34 @@ export function createClientRouter(options) {
687
916
  const matched = match(path);
688
917
  if (matched === null)
689
918
  return;
919
+ // The target an open boundary already shows (a `#fragment` entry inside it, a link to itself):
920
+ // reloading it would rebuild the boundary and drop its action result. One in its error state
921
+ // loads again, so a link can retry it.
922
+ if (state.intercepted !== undefined &&
923
+ state.boundaries?.[state.intercepted.boundary]?.status === "ready" &&
924
+ !state.pending &&
925
+ path === state.path) {
926
+ emit();
927
+ return;
928
+ }
690
929
  // Client-only search fast path: same route + same pathname, and every changed search key is
691
930
  // declared client-only. Publish the new URL (so `useSearch`/`useLocation` update) WITHOUT a data
692
931
  // fetch - the loader's inputs did not change. Any loader-affecting key change falls through to the
693
932
  // normal match → fetch → publish below, so data is never stale. Abort an in-flight fetch first: a
694
933
  // rapid `?tab` after a real nav must not let the older fetch overwrite this synchronous update.
695
- if (matched.routeId === state.routeId &&
934
+ if (state.intercepted === undefined &&
935
+ matched.routeId === state.routeId &&
696
936
  pathnameOf(path) === pathnameOf(state.path) &&
697
937
  isClientOnlySearchChange(rawSearchOf(state.path), rawSearchOf(path), searchClientKeys[state.routeId] ?? [])) {
698
- ++generation; // supersede any in-flight navigation so its late result is dropped
938
+ const mine = ++generation; // supersede any in-flight navigation so its late result is dropped
699
939
  navAbort?.abort();
700
- cachePut(path, dataFields(state));
701
- state = { ...state, path, pending: false, pendingPath: undefined };
702
- emit();
940
+ await present(() => {
941
+ if (mine !== generation)
942
+ return;
943
+ cachePut(path, dataFields(state));
944
+ state = { ...state, path, pending: false, pendingPath: undefined };
945
+ emit();
946
+ });
703
947
  return;
704
948
  }
705
949
  const mine = ++generation;
@@ -713,57 +957,71 @@ export function createClientRouter(options) {
713
957
  if (mine !== generation)
714
958
  return; // a newer navigation superseded this one - drop the stale result
715
959
  cachePut(to, loaded); // keep the keyed cache coherent with what we publish
716
- const interception = loaded.terminalRouteId === undefined
717
- ? interceptionFor(state.routeId, to, target)
718
- : undefined;
719
- if (interception !== undefined) {
720
- // The target's ordinary data request has already completed (including its server authz and
721
- // optional client wrapper). Only the presentation route is retained; the current page stays
722
- // mounted underneath the named slot. A target that returned a terminal status never reaches
723
- // this branch, so an unauthorized/missing target cannot become a modal.
960
+ // The swap can wait a frame for the presenter, and a newer navigation started meanwhile owns it.
961
+ await present(() => {
962
+ if (mine !== generation)
963
+ return;
964
+ // The page's own URL is never intercepted: navigating there (Back out of a boundary, a link
965
+ // to itself) shows the page, even when a boundary's pattern matches it.
966
+ const pagePath = pagePathOf(state);
967
+ const interception = loaded.terminalRouteId === undefined && pathnameOf(to) !== pathnameOf(pagePath)
968
+ ? interceptionFor(state.routeId, to, target)
969
+ : undefined;
970
+ if (interception !== undefined) {
971
+ // The target's ordinary data request has already completed (including its server authz and
972
+ // optional client wrapper). Only the presentation route is retained; the current page stays
973
+ // mounted underneath the named slot. A target that returned a terminal status never reaches
974
+ // this branch, so an unauthorized/missing target cannot become a modal.
975
+ state = {
976
+ ...state,
977
+ path: to,
978
+ boundaries: holdTarget(state.boundaries, interception, loaded),
979
+ intercepted: { boundary: interception.name, pagePath },
980
+ pending: false,
981
+ pendingPath: undefined,
982
+ };
983
+ emit();
984
+ return;
985
+ }
724
986
  state = {
725
- ...state,
987
+ routeId: loaded.terminalRouteId ?? target.routeId,
988
+ params: loaded.terminalRouteId === undefined ? target.params : {},
726
989
  path: to,
727
- boundaries: {
728
- ...(state.boundaries ?? {}),
729
- [interception.name]: {
730
- name: interception.name,
731
- mode: interception.mode,
732
- status: "ready",
733
- data: loaded.data,
734
- ...(interception.errorId === undefined ? {} : { errorId: interception.errorId }),
735
- },
736
- },
737
- actionData: undefined,
990
+ ...dataFields(loaded),
991
+ actionData: undefined, // a fresh navigation has no action result
738
992
  pending: false,
739
- pendingPath: undefined,
740
993
  };
994
+ shownPage = loaded;
741
995
  emit();
742
- return;
743
- }
744
- state = {
745
- routeId: loaded.terminalRouteId ?? target.routeId,
746
- params: loaded.terminalRouteId === undefined ? target.params : {},
747
- path: to,
748
- ...dataFields(loaded),
749
- actionData: undefined, // a fresh navigation has no action result
750
- pending: false,
751
- };
752
- emit();
996
+ });
753
997
  }
754
998
  catch (err) {
755
- // Clear our pending flag + target (only if still current) and rethrow - the caller decides how
756
- // to recover (the history layer falls back to a full-page navigation).
757
- if (mine === generation) {
758
- state = { ...state, pending: false, pendingPath: undefined };
759
- emit();
760
- }
999
+ // A newer navigation or submit owns the page, and aborting this one's request is how it took
1000
+ // over: as with a submit, the superseded navigation settles quietly.
1001
+ if (mine !== generation)
1002
+ return;
1003
+ // Clear our pending flag + target and rethrow - the caller decides how to recover (the history
1004
+ // layer falls back to a full-page navigation).
1005
+ state = { ...state, pending: false, pendingPath: undefined };
1006
+ emit();
761
1007
  throw err;
762
1008
  }
763
1009
  },
764
1010
  submit: async (action, body, opts) => {
765
1011
  const mine = ++generation;
766
1012
  const previousActionData = state.actionData;
1013
+ // An action posted to the target an intercept boundary holds answers that boundary, not the page.
1014
+ const actionBoundary = state.intercepted !== undefined && pathnameOf(action) === pathnameOf(state.path)
1015
+ ? state.intercepted.boundary
1016
+ : undefined;
1017
+ const previousBoundaryActionData = actionBoundary === undefined ? undefined : state.boundaries?.[actionBoundary]?.actionData;
1018
+ // The state with `value` as the action's result, on whichever owns the action.
1019
+ const answered = (current, value) => {
1020
+ if (actionBoundary === undefined)
1021
+ return { actionData: value };
1022
+ const boundaries = withActionData(current.boundaries, actionBoundary, value);
1023
+ return boundaries === undefined ? {} : { boundaries };
1024
+ };
767
1025
  // A superseding navigation/submit aborts this submit's FOLLOW-UP reads (revalidation / redirect
768
1026
  // fetch + their NDJSON drains) - not the mutation POST itself, which should complete.
769
1027
  // Wire into `navAbort` like `navigate`/`refetchActive`, so a later nav cancels the in-flight read.
@@ -784,7 +1042,7 @@ export function createClientRouter(options) {
784
1042
  try {
785
1043
  const prepared = await prepareClientAction(action, body, ac.signal);
786
1044
  if (Object.hasOwn(prepared, "optimisticData")) {
787
- state = { ...state, actionData: prepared.optimisticData };
1045
+ state = { ...state, ...answered(state, prepared.optimisticData) };
788
1046
  emit();
789
1047
  }
790
1048
  const res = await fetch(appUrl(action), {
@@ -794,6 +1052,7 @@ export function createClientRouter(options) {
794
1052
  });
795
1053
  // Whatever the status, the server took the POST: the action may have run before it failed.
796
1054
  posted = true;
1055
+ dropPrefetched();
797
1056
  if (!res.ok)
798
1057
  throw new Error(`[nifra/web] action failed (${res.status}): ${action}`);
799
1058
  const redirectTo = res.headers.get(REDIRECT_HEADER);
@@ -818,6 +1077,7 @@ export function createClientRouter(options) {
818
1077
  actionData: undefined,
819
1078
  pending: false,
820
1079
  };
1080
+ shownPage = loaded;
821
1081
  emit();
822
1082
  return;
823
1083
  }
@@ -828,31 +1088,43 @@ export function createClientRouter(options) {
828
1088
  // Routes the action declared changed (via the `revalidate()` helper → `X-Nifra-Revalidate`),
829
1089
  // validated against the manifest.
830
1090
  const changed = parseRevalidate(res.headers.get(REVALIDATE_HEADER));
831
- // Revalidate the active route's loader so the mutation is reflected - unless the caller opted
832
- // out (`revalidate: false`). A server-declared revalidate of the active path overrides the
833
- // opt-out (the server says it changed, so stale data would be wrong). Default is to revalidate.
834
- const skipActive = opts?.revalidate === false && !changed.includes(state.path);
835
- const active = { routeId: state.routeId, params: state.params };
836
- const loaded = skipActive
837
- ? {
838
- ...dataFields(state),
839
- }
840
- : await applyClientLoader(state.path, active, ac.signal, () => loadRouteData(state.path, active, ac.signal));
1091
+ // Revalidate what is on screen so the mutation is reflected - the page, and a target an
1092
+ // intercept boundary holds - unless the caller opted out (`revalidate: false`). A
1093
+ // server-declared revalidate of a path on screen overrides the opt-out (the server says it
1094
+ // changed, so stale data would be wrong). Default is to revalidate.
1095
+ const pagePath = pagePathOf(state);
1096
+ const reloadPage = opts?.revalidate !== false || changed.includes(pagePath);
1097
+ const reloadTarget = state.intercepted !== undefined &&
1098
+ (opts?.revalidate !== false || changed.includes(state.path));
1099
+ const { fields, route } = await reloadActive(ac.signal, reloadPage, reloadTarget, mine);
841
1100
  if (mine !== generation)
842
1101
  return;
843
- cachePut(state.path, loaded); // the revalidated (or kept) data is now the cache's truth
844
1102
  // Mark the OTHER changed routes stale so the next access refetches.
845
- markStale(changed.filter((p) => p !== state.path));
1103
+ markStale(changed.filter((p) => p !== pagePath && (!reloadTarget || p !== state.path)));
846
1104
  // Reconcile: publish the revalidated data + actionData; omit `submission` (the optimistic
847
- // window is over - the real data now drives the view).
848
- state = {
849
- routeId: state.routeId,
850
- params: state.params,
851
- path: state.path,
852
- ...dataFields(loaded),
853
- actionData,
854
- pending: false,
855
- };
1105
+ // window is over - the real data now drives the view). A reload that names another route (a
1106
+ // terminal answer for the page) renders it in the page's place, closing any boundary, and the
1107
+ // action's result goes with the page.
1108
+ state =
1109
+ route === undefined
1110
+ ? {
1111
+ routeId: state.routeId,
1112
+ params: state.params,
1113
+ path: state.path,
1114
+ ...fields,
1115
+ actionData: previousActionData,
1116
+ ...answered(fields, actionData),
1117
+ pending: false,
1118
+ ...(state.intercepted === undefined ? {} : { intercepted: state.intercepted }),
1119
+ }
1120
+ : {
1121
+ routeId: route.routeId,
1122
+ params: route.params,
1123
+ path: state.path,
1124
+ ...fields,
1125
+ actionData: undefined,
1126
+ pending: false,
1127
+ };
856
1128
  emit();
857
1129
  // Refresh any mounted fetcher showing one of the changed routes (the active route was just
858
1130
  // revalidated inline above, so skip it here). Best-effort - never rejects the submit.
@@ -868,7 +1140,11 @@ export function createClientRouter(options) {
868
1140
  path: state.path,
869
1141
  ...dataFields(state),
870
1142
  actionData: previousActionData,
1143
+ ...(actionBoundary === undefined
1144
+ ? {}
1145
+ : answered(dataFields(state), previousBoundaryActionData)),
871
1146
  pending: false,
1147
+ ...(state.intercepted === undefined ? {} : { intercepted: state.intercepted }),
872
1148
  };
873
1149
  emit();
874
1150
  }
@@ -876,21 +1152,24 @@ export function createClientRouter(options) {
876
1152
  throw err;
877
1153
  if (mine !== generation)
878
1154
  return; // a newer navigation or submit owns the page
879
- throw redirectOf(err) === undefined ? redirected(landing ?? state.path, err) : err;
1155
+ // Otherwise show the route that owns the action, as a native post to it would.
1156
+ const owner = actionBoundary === undefined ? pagePathOf(state) : state.path;
1157
+ throw redirectOf(err) === undefined ? redirected(landing ?? owner, err) : err;
880
1158
  }
881
1159
  },
882
1160
  hydrate,
883
1161
  invalidate: async (paths) => {
884
1162
  // Mark targeted cache entries stale (all entries when no `paths`) - unmounted ones refetch
885
- // lazily on next access.
1163
+ // lazily on next access. A warmed prefetch of a target is dropped: it would show the old data.
886
1164
  const targets = paths ?? [...cache.keys()];
887
1165
  markStale(targets);
1166
+ if (paths === undefined)
1167
+ dropPrefetched();
888
1168
  // Refresh mounted readers now: the active route (when in scope - an explicit list including it,
889
1169
  // or an invalidate-all) + any fetcher showing a targeted path. The active refetch rejects on
890
1170
  // failure (the caller asked to refresh); fetcher refreshes are best-effort (swallowed).
891
1171
  const jobs = [];
892
- if (paths === undefined || paths.includes(state.path))
893
- jobs.push(refetchActive());
1172
+ jobs.push(refetchActive(paths === undefined ? undefined : (path) => paths.includes(path)));
894
1173
  for (const entry of fetchers.values())
895
1174
  jobs.push(entry.refreshIfShowing(targets));
896
1175
  await Promise.all(jobs);
@@ -901,13 +1180,15 @@ export function createClientRouter(options) {
901
1180
  const matched = match(path);
902
1181
  if (matched === null)
903
1182
  return;
904
- inflight.add(path);
1183
+ const token = {};
1184
+ inflight.set(path, token);
1185
+ const epoch = readEpoch();
905
1186
  try {
906
1187
  // Unwrapped here, so a prefetched entry is the same shape `navigate` builds state from -
907
1188
  // otherwise a prefetch hit would publish an envelope where a miss publishes loader data.
908
1189
  const [, loaded] = await Promise.all([
909
1190
  loadModule?.(matched.routeId, path),
910
- loadRouteData(path, matched),
1191
+ loadRouteData(path, matched, undefined, false, "prefetch"),
911
1192
  ]);
912
1193
  prefetched.delete(path); // a stale entry re-enters as the newest
913
1194
  if (prefetched.size >= MAX_PREFETCH) {
@@ -915,14 +1196,20 @@ export function createClientRouter(options) {
915
1196
  if (oldest !== undefined)
916
1197
  prefetched.delete(oldest);
917
1198
  }
918
- if (loaded.serverComponents === undefined)
919
- prefetched.set(path, [loaded, Date.now()]);
1199
+ // A prefetched hole is an empty host nothing fills; the navigation fetches it, cancellably.
1200
+ const holes = Object.values(loaded.boundaries ?? {}).some((state) => state.ppr === true);
1201
+ if (loaded.serverComponents === undefined &&
1202
+ !holes &&
1203
+ inflight.get(path) === token &&
1204
+ epoch === readEpoch())
1205
+ prefetched.set(path, [loaded, Date.now(), epoch]);
920
1206
  }
921
1207
  catch {
922
1208
  // Best-effort: a failed prefetch just means the eventual navigate fetches normally.
923
1209
  }
924
1210
  finally {
925
- inflight.delete(path);
1211
+ if (inflight.get(path) === token)
1212
+ inflight.delete(path);
926
1213
  }
927
1214
  },
928
1215
  fetcher: (key) => {