@nifrajs/web 5.5.1 → 5.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. package/CHANGELOG.md +243 -0
  2. package/README.md +21 -11
  3. package/dist/boundary.d.ts +67 -7
  4. package/dist/boundary.d.ts.map +1 -1
  5. package/dist/boundary.js +6 -4
  6. package/dist/boundary.js.map +1 -1
  7. package/dist/build-plan.d.ts +3 -0
  8. package/dist/build-plan.d.ts.map +1 -1
  9. package/dist/build-plan.js.map +1 -1
  10. package/dist/build-vite.d.ts.map +1 -1
  11. package/dist/build-vite.js +2 -0
  12. package/dist/build-vite.js.map +1 -1
  13. package/dist/build.d.ts +9 -2
  14. package/dist/build.d.ts.map +1 -1
  15. package/dist/build.js +10 -0
  16. package/dist/build.js.map +1 -1
  17. package/dist/client.d.ts +3 -1
  18. package/dist/client.d.ts.map +1 -1
  19. package/dist/client.js +174 -23
  20. package/dist/client.js.map +1 -1
  21. package/dist/csp.d.ts +8 -0
  22. package/dist/csp.d.ts.map +1 -1
  23. package/dist/csp.js +11 -2
  24. package/dist/csp.js.map +1 -1
  25. package/dist/deferred.d.ts +6 -4
  26. package/dist/deferred.d.ts.map +1 -1
  27. package/dist/deferred.js +10 -6
  28. package/dist/deferred.js.map +1 -1
  29. package/dist/dev-feed.d.ts.map +1 -1
  30. package/dist/dev-feed.js +11 -2
  31. package/dist/dev-feed.js.map +1 -1
  32. package/dist/dev-session.d.ts.map +1 -1
  33. package/dist/dev-session.js +13 -6
  34. package/dist/dev-session.js.map +1 -1
  35. package/dist/dev.d.ts +4 -1
  36. package/dist/dev.d.ts.map +1 -1
  37. package/dist/dev.js +12 -5
  38. package/dist/dev.js.map +1 -1
  39. package/dist/diagnostic-catalog.d.ts +4 -0
  40. package/dist/diagnostic-catalog.d.ts.map +1 -1
  41. package/dist/diagnostic-catalog.js +48 -0
  42. package/dist/diagnostic-catalog.js.map +1 -1
  43. package/dist/diagnostic.d.ts +1 -1
  44. package/dist/diagnostic.d.ts.map +1 -1
  45. package/dist/diagnostic.js +1 -1
  46. package/dist/diagnostic.js.map +1 -1
  47. package/dist/fn.d.ts +5 -3
  48. package/dist/fn.d.ts.map +1 -1
  49. package/dist/fn.js +20 -5
  50. package/dist/fn.js.map +1 -1
  51. package/dist/fs.d.ts.map +1 -1
  52. package/dist/fs.js +13 -3
  53. package/dist/fs.js.map +1 -1
  54. package/dist/identity.d.ts +22 -0
  55. package/dist/identity.d.ts.map +1 -0
  56. package/dist/identity.js +60 -0
  57. package/dist/identity.js.map +1 -0
  58. package/dist/index.d.ts +8 -6
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +6 -3
  61. package/dist/index.js.map +1 -1
  62. package/dist/internal/cdn/cloudflare.d.ts +2 -1
  63. package/dist/internal/cdn/cloudflare.d.ts.map +1 -1
  64. package/dist/internal/cdn/cloudflare.js +2 -1
  65. package/dist/internal/cdn/cloudflare.js.map +1 -1
  66. package/dist/internal/certified-components.d.ts +1 -1
  67. package/dist/internal/certified-components.js +1 -1
  68. package/dist/internal/codegen.d.ts +11 -10
  69. package/dist/internal/codegen.d.ts.map +1 -1
  70. package/dist/internal/codegen.js +73 -10
  71. package/dist/internal/codegen.js.map +1 -1
  72. package/dist/internal/dev-websocket.d.ts +73 -0
  73. package/dist/internal/dev-websocket.d.ts.map +1 -0
  74. package/dist/internal/dev-websocket.js +145 -0
  75. package/dist/internal/dev-websocket.js.map +1 -0
  76. package/dist/internal/hole-runtime.d.ts +66 -0
  77. package/dist/internal/hole-runtime.d.ts.map +1 -0
  78. package/dist/internal/hole-runtime.js +211 -0
  79. package/dist/internal/hole-runtime.js.map +1 -0
  80. package/dist/internal/identity-epoch.d.ts +50 -0
  81. package/dist/internal/identity-epoch.d.ts.map +1 -0
  82. package/dist/internal/identity-epoch.js +180 -0
  83. package/dist/internal/identity-epoch.js.map +1 -0
  84. package/dist/internal/invalidation-contract.d.ts +23 -0
  85. package/dist/internal/invalidation-contract.d.ts.map +1 -0
  86. package/dist/internal/invalidation-contract.js +11 -0
  87. package/dist/internal/invalidation-contract.js.map +1 -0
  88. package/dist/internal/invalidation.d.ts +62 -0
  89. package/dist/internal/invalidation.d.ts.map +1 -0
  90. package/dist/internal/invalidation.js +149 -0
  91. package/dist/internal/invalidation.js.map +1 -0
  92. package/dist/internal/island-bindings.d.ts +51 -0
  93. package/dist/internal/island-bindings.d.ts.map +1 -0
  94. package/dist/internal/island-bindings.js +147 -0
  95. package/dist/internal/island-bindings.js.map +1 -0
  96. package/dist/internal/island-list.d.ts +26 -0
  97. package/dist/internal/island-list.d.ts.map +1 -0
  98. package/dist/internal/island-list.js +78 -0
  99. package/dist/internal/island-list.js.map +1 -0
  100. package/dist/internal/island-mount.d.ts +4 -0
  101. package/dist/internal/island-mount.d.ts.map +1 -0
  102. package/dist/internal/island-mount.js +66 -0
  103. package/dist/internal/island-mount.js.map +1 -0
  104. package/dist/internal/island-owner.d.ts +5 -0
  105. package/dist/internal/island-owner.d.ts.map +1 -0
  106. package/dist/internal/island-owner.js +20 -0
  107. package/dist/internal/island-owner.js.map +1 -0
  108. package/dist/internal/island-resource.d.ts +30 -0
  109. package/dist/internal/island-resource.d.ts.map +1 -0
  110. package/dist/internal/island-resource.js +57 -0
  111. package/dist/internal/island-resource.js.map +1 -0
  112. package/dist/internal/island-signals.d.ts +51 -0
  113. package/dist/internal/island-signals.d.ts.map +1 -0
  114. package/dist/internal/island-signals.js +220 -0
  115. package/dist/internal/island-signals.js.map +1 -0
  116. package/dist/internal/keep-alive-table.d.ts +23 -0
  117. package/dist/internal/keep-alive-table.d.ts.map +1 -0
  118. package/dist/internal/keep-alive-table.js +53 -0
  119. package/dist/internal/keep-alive-table.js.map +1 -0
  120. package/dist/internal/live-contract.d.ts +101 -0
  121. package/dist/internal/live-contract.d.ts.map +1 -0
  122. package/dist/internal/live-contract.js +5 -0
  123. package/dist/internal/live-contract.js.map +1 -0
  124. package/dist/internal/live-queries.d.ts +22 -0
  125. package/dist/internal/live-queries.d.ts.map +1 -0
  126. package/dist/internal/live-queries.js +54 -0
  127. package/dist/internal/live-queries.js.map +1 -0
  128. package/dist/internal/page-events.d.ts +10 -0
  129. package/dist/internal/page-events.d.ts.map +1 -0
  130. package/dist/internal/page-events.js +26 -0
  131. package/dist/internal/page-events.js.map +1 -0
  132. package/dist/internal/page-execution.d.ts +16 -1
  133. package/dist/internal/page-execution.d.ts.map +1 -1
  134. package/dist/internal/page-execution.js +432 -31
  135. package/dist/internal/page-execution.js.map +1 -1
  136. package/dist/internal/parity.d.ts +3 -0
  137. package/dist/internal/parity.d.ts.map +1 -1
  138. package/dist/internal/parity.js +28 -5
  139. package/dist/internal/parity.js.map +1 -1
  140. package/dist/internal/ppr/admission.d.ts +83 -0
  141. package/dist/internal/ppr/admission.d.ts.map +1 -0
  142. package/dist/internal/ppr/admission.js +221 -0
  143. package/dist/internal/ppr/admission.js.map +1 -0
  144. package/dist/internal/ppr/canary.d.ts +24 -0
  145. package/dist/internal/ppr/canary.d.ts.map +1 -0
  146. package/dist/internal/ppr/canary.js +102 -0
  147. package/dist/internal/ppr/canary.js.map +1 -0
  148. package/dist/internal/ppr/compile-shell.d.ts +39 -0
  149. package/dist/internal/ppr/compile-shell.d.ts.map +1 -0
  150. package/dist/internal/ppr/compile-shell.js +298 -0
  151. package/dist/internal/ppr/compile-shell.js.map +1 -0
  152. package/dist/internal/ppr/contract.d.ts +177 -0
  153. package/dist/internal/ppr/contract.d.ts.map +1 -0
  154. package/dist/internal/ppr/contract.js +80 -0
  155. package/dist/internal/ppr/contract.js.map +1 -0
  156. package/dist/internal/ppr/frames.d.ts +71 -0
  157. package/dist/internal/ppr/frames.d.ts.map +1 -0
  158. package/dist/internal/ppr/frames.js +119 -0
  159. package/dist/internal/ppr/frames.js.map +1 -0
  160. package/dist/internal/ppr/integration.d.ts +19 -0
  161. package/dist/internal/ppr/integration.d.ts.map +1 -0
  162. package/dist/internal/ppr/integration.js +106 -0
  163. package/dist/internal/ppr/integration.js.map +1 -0
  164. package/dist/internal/ppr/options.d.ts +96 -0
  165. package/dist/internal/ppr/options.d.ts.map +1 -0
  166. package/dist/internal/ppr/options.js +125 -0
  167. package/dist/internal/ppr/options.js.map +1 -0
  168. package/dist/internal/ppr/route-rules.d.ts +12 -0
  169. package/dist/internal/ppr/route-rules.d.ts.map +1 -0
  170. package/dist/internal/ppr/route-rules.js +53 -0
  171. package/dist/internal/ppr/route-rules.js.map +1 -0
  172. package/dist/internal/ppr/runtime.d.ts +29 -0
  173. package/dist/internal/ppr/runtime.d.ts.map +1 -0
  174. package/dist/internal/ppr/runtime.js +35 -0
  175. package/dist/internal/ppr/runtime.js.map +1 -0
  176. package/dist/internal/ppr/server.d.ts +29 -0
  177. package/dist/internal/ppr/server.d.ts.map +1 -0
  178. package/dist/internal/ppr/server.js +1409 -0
  179. package/dist/internal/ppr/server.js.map +1 -0
  180. package/dist/internal/ppr/shell-store.d.ts +39 -0
  181. package/dist/internal/ppr/shell-store.d.ts.map +1 -0
  182. package/dist/internal/ppr/shell-store.js +106 -0
  183. package/dist/internal/ppr/shell-store.js.map +1 -0
  184. package/dist/internal/prerendered-pages.d.ts +1 -1
  185. package/dist/internal/prerendered-pages.d.ts.map +1 -1
  186. package/dist/internal/prerendered-pages.js +7 -4
  187. package/dist/internal/prerendered-pages.js.map +1 -1
  188. package/dist/internal/present.d.ts +11 -0
  189. package/dist/internal/present.d.ts.map +1 -0
  190. package/dist/internal/present.js +13 -0
  191. package/dist/internal/present.js.map +1 -0
  192. package/dist/internal/query-digest.d.ts +9 -0
  193. package/dist/internal/query-digest.d.ts.map +1 -0
  194. package/dist/internal/query-digest.js +23 -0
  195. package/dist/internal/query-digest.js.map +1 -0
  196. package/dist/internal/query-runtime.d.ts +6 -1
  197. package/dist/internal/query-runtime.d.ts.map +1 -1
  198. package/dist/internal/query-runtime.js +9 -0
  199. package/dist/internal/query-runtime.js.map +1 -1
  200. package/dist/internal/query-seam.d.ts +29 -0
  201. package/dist/internal/query-seam.d.ts.map +1 -0
  202. package/dist/internal/query-seam.js +23 -0
  203. package/dist/internal/query-seam.js.map +1 -0
  204. package/dist/internal/render-document.d.ts +34 -64
  205. package/dist/internal/render-document.d.ts.map +1 -1
  206. package/dist/internal/render-document.js +82 -64
  207. package/dist/internal/render-document.js.map +1 -1
  208. package/dist/internal/request-abort.d.ts +8 -0
  209. package/dist/internal/request-abort.d.ts.map +1 -0
  210. package/dist/internal/request-abort.js +22 -0
  211. package/dist/internal/request-abort.js.map +1 -0
  212. package/dist/internal/server-boundary.d.ts +2 -0
  213. package/dist/internal/server-boundary.d.ts.map +1 -1
  214. package/dist/internal/server-boundary.js +8 -1
  215. package/dist/internal/server-boundary.js.map +1 -1
  216. package/dist/internal/share-data.d.ts +10 -0
  217. package/dist/internal/share-data.d.ts.map +1 -0
  218. package/dist/internal/share-data.js +135 -0
  219. package/dist/internal/share-data.js.map +1 -0
  220. package/dist/internal/status-page.d.ts +68 -0
  221. package/dist/internal/status-page.d.ts.map +1 -0
  222. package/dist/internal/status-page.js +66 -0
  223. package/dist/internal/status-page.js.map +1 -0
  224. package/dist/internal/store-signature.d.ts +15 -0
  225. package/dist/internal/store-signature.d.ts.map +1 -0
  226. package/dist/internal/store-signature.js +114 -0
  227. package/dist/internal/store-signature.js.map +1 -0
  228. package/dist/internal/vite-plugin-compat.d.ts +2 -2
  229. package/dist/internal/vite-plugin-compat.js +2 -2
  230. package/dist/internal/web-app.d.ts +26 -0
  231. package/dist/internal/web-app.d.ts.map +1 -1
  232. package/dist/internal/web-app.js +79 -3
  233. package/dist/internal/web-app.js.map +1 -1
  234. package/dist/islands-swap.d.ts +41 -0
  235. package/dist/islands-swap.d.ts.map +1 -0
  236. package/dist/islands-swap.js +270 -0
  237. package/dist/islands-swap.js.map +1 -0
  238. package/dist/islands.d.ts +86 -24
  239. package/dist/islands.d.ts.map +1 -1
  240. package/dist/islands.js +138 -78
  241. package/dist/islands.js.map +1 -1
  242. package/dist/isr.d.ts +67 -13
  243. package/dist/isr.d.ts.map +1 -1
  244. package/dist/isr.js +247 -99
  245. package/dist/isr.js.map +1 -1
  246. package/dist/keep-alive.d.ts +147 -0
  247. package/dist/keep-alive.d.ts.map +1 -0
  248. package/dist/keep-alive.js +1102 -0
  249. package/dist/keep-alive.js.map +1 -0
  250. package/dist/live/bus.d.ts +70 -0
  251. package/dist/live/bus.d.ts.map +1 -0
  252. package/dist/live/bus.js +55 -0
  253. package/dist/live/bus.js.map +1 -0
  254. package/dist/live/core.d.ts +25 -0
  255. package/dist/live/core.d.ts.map +1 -0
  256. package/dist/live/core.js +29 -0
  257. package/dist/live/core.js.map +1 -0
  258. package/dist/live/history.d.ts +38 -0
  259. package/dist/live/history.d.ts.map +1 -0
  260. package/dist/live/history.js +104 -0
  261. package/dist/live/history.js.map +1 -0
  262. package/dist/live/hub-protocol.d.ts +46 -0
  263. package/dist/live/hub-protocol.d.ts.map +1 -0
  264. package/dist/live/hub-protocol.js +108 -0
  265. package/dist/live/hub-protocol.js.map +1 -0
  266. package/dist/live/hub.d.ts +185 -0
  267. package/dist/live/hub.d.ts.map +1 -0
  268. package/dist/live/hub.js +544 -0
  269. package/dist/live/hub.js.map +1 -0
  270. package/dist/live/keyring.d.ts +92 -0
  271. package/dist/live/keyring.d.ts.map +1 -0
  272. package/dist/live/keyring.js +265 -0
  273. package/dist/live/keyring.js.map +1 -0
  274. package/dist/live/read-capped.d.ts +6 -0
  275. package/dist/live/read-capped.d.ts.map +1 -0
  276. package/dist/live/read-capped.js +42 -0
  277. package/dist/live/read-capped.js.map +1 -0
  278. package/dist/live/revocations.d.ts +52 -0
  279. package/dist/live/revocations.d.ts.map +1 -0
  280. package/dist/live/revocations.js +116 -0
  281. package/dist/live/revocations.js.map +1 -0
  282. package/dist/live/settings.d.ts +14 -0
  283. package/dist/live/settings.d.ts.map +1 -0
  284. package/dist/live/settings.js +38 -0
  285. package/dist/live/settings.js.map +1 -0
  286. package/dist/live-client.d.ts +34 -0
  287. package/dist/live-client.d.ts.map +1 -0
  288. package/dist/live-client.js +878 -0
  289. package/dist/live-client.js.map +1 -0
  290. package/dist/live-hub.d.ts +130 -0
  291. package/dist/live-hub.d.ts.map +1 -0
  292. package/dist/live-hub.js +507 -0
  293. package/dist/live-hub.js.map +1 -0
  294. package/dist/live-redis.d.ts +60 -0
  295. package/dist/live-redis.d.ts.map +1 -0
  296. package/dist/live-redis.js +212 -0
  297. package/dist/live-redis.js.map +1 -0
  298. package/dist/live.d.ts +150 -0
  299. package/dist/live.d.ts.map +1 -0
  300. package/dist/live.js +972 -0
  301. package/dist/live.js.map +1 -0
  302. package/dist/manifest.d.ts +113 -2
  303. package/dist/manifest.d.ts.map +1 -1
  304. package/dist/manifest.js +109 -5
  305. package/dist/manifest.js.map +1 -1
  306. package/dist/navigation.d.ts +9 -3
  307. package/dist/navigation.d.ts.map +1 -1
  308. package/dist/navigation.js +13 -3
  309. package/dist/navigation.js.map +1 -1
  310. package/dist/plugins/vite-zone-guard.d.ts +6 -0
  311. package/dist/plugins/vite-zone-guard.d.ts.map +1 -1
  312. package/dist/plugins/vite-zone-guard.js +8 -2
  313. package/dist/plugins/vite-zone-guard.js.map +1 -1
  314. package/dist/ppr.d.ts +73 -0
  315. package/dist/ppr.d.ts.map +1 -0
  316. package/dist/ppr.js +76 -0
  317. package/dist/ppr.js.map +1 -0
  318. package/dist/query.d.ts +8 -13
  319. package/dist/query.d.ts.map +1 -1
  320. package/dist/query.js +112 -18
  321. package/dist/query.js.map +1 -1
  322. package/dist/render-seam.d.ts +32 -0
  323. package/dist/render-seam.d.ts.map +1 -1
  324. package/dist/render-seam.js +7 -0
  325. package/dist/render-seam.js.map +1 -1
  326. package/dist/route-types.d.ts.map +1 -1
  327. package/dist/route-types.js +2 -2
  328. package/dist/route-types.js.map +1 -1
  329. package/dist/router.d.ts +72 -4
  330. package/dist/router.d.ts.map +1 -1
  331. package/dist/router.js +416 -129
  332. package/dist/router.js.map +1 -1
  333. package/dist/server-components/build.d.ts.map +1 -1
  334. package/dist/server-components/build.js +3 -3
  335. package/dist/server-components/build.js.map +1 -1
  336. package/dist/server-components/client.d.ts +23 -1
  337. package/dist/server-components/client.d.ts.map +1 -1
  338. package/dist/server-components/client.js +92 -5
  339. package/dist/server-components/client.js.map +1 -1
  340. package/dist/server-components/definition.d.ts.map +1 -1
  341. package/dist/server-components/definition.js +2 -1
  342. package/dist/server-components/definition.js.map +1 -1
  343. package/dist/server-components/hole.d.ts +8 -0
  344. package/dist/server-components/hole.d.ts.map +1 -0
  345. package/dist/server-components/hole.js +20 -0
  346. package/dist/server-components/hole.js.map +1 -0
  347. package/dist/server-components/html-policy.d.ts.map +1 -1
  348. package/dist/server-components/html-policy.js +62 -4
  349. package/dist/server-components/html-policy.js.map +1 -1
  350. package/dist/server-components/html-scan.d.ts +17 -0
  351. package/dist/server-components/html-scan.d.ts.map +1 -0
  352. package/dist/server-components/html-scan.js +180 -0
  353. package/dist/server-components/html-scan.js.map +1 -0
  354. package/dist/server-components/html.d.ts.map +1 -1
  355. package/dist/server-components/html.js +19 -4
  356. package/dist/server-components/html.js.map +1 -1
  357. package/dist/server-components/router-integration.d.ts +4 -1
  358. package/dist/server-components/router-integration.d.ts.map +1 -1
  359. package/dist/server-components/router-integration.js +23 -2
  360. package/dist/server-components/router-integration.js.map +1 -1
  361. package/dist/server-components/runtime.d.ts +6 -1
  362. package/dist/server-components/runtime.d.ts.map +1 -1
  363. package/dist/server-components/runtime.js +88 -29
  364. package/dist/server-components/runtime.js.map +1 -1
  365. package/dist/server-components/state.d.ts +3 -1
  366. package/dist/server-components/state.d.ts.map +1 -1
  367. package/dist/server-components/state.js +50 -38
  368. package/dist/server-components/state.js.map +1 -1
  369. package/dist/server-components/types.d.ts +9 -0
  370. package/dist/server-components/types.d.ts.map +1 -1
  371. package/dist/server-components/vite.d.ts.map +1 -1
  372. package/dist/server-components/vite.js +26 -9
  373. package/dist/server-components/vite.js.map +1 -1
  374. package/dist/vite.d.ts +5 -1
  375. package/dist/vite.d.ts.map +1 -1
  376. package/dist/vite.js +58 -4
  377. package/dist/vite.js.map +1 -1
  378. package/package.json +48 -8
  379. package/src/boundary.ts +77 -5
  380. package/src/build-plan.ts +3 -0
  381. package/src/build-vite.ts +2 -0
  382. package/src/build.ts +20 -1
  383. package/src/client.ts +183 -23
  384. package/src/csp.ts +16 -2
  385. package/src/deferred.ts +11 -5
  386. package/src/dev-feed.ts +11 -2
  387. package/src/dev-session.ts +22 -11
  388. package/src/dev.ts +29 -8
  389. package/src/diagnostic-catalog.ts +53 -0
  390. package/src/diagnostic.ts +1 -0
  391. package/src/fn.ts +24 -7
  392. package/src/fs.ts +22 -8
  393. package/src/identity.ts +61 -0
  394. package/src/index.ts +20 -4
  395. package/src/internal/cdn/cloudflare.ts +2 -1
  396. package/src/internal/certified-components.ts +1 -1
  397. package/src/internal/codegen.ts +95 -9
  398. package/src/internal/dev-websocket.ts +230 -0
  399. package/src/internal/hole-runtime.ts +286 -0
  400. package/src/internal/identity-epoch.ts +239 -0
  401. package/src/internal/invalidation-contract.ts +35 -0
  402. package/src/internal/invalidation.ts +213 -0
  403. package/src/internal/island-bindings.ts +191 -0
  404. package/src/internal/island-list.ts +92 -0
  405. package/src/internal/island-mount.ts +68 -0
  406. package/src/internal/island-owner.ts +20 -0
  407. package/src/internal/island-resource.ts +76 -0
  408. package/src/internal/island-signals.ts +249 -0
  409. package/src/internal/keep-alive-table.ts +72 -0
  410. package/src/internal/live-contract.ts +111 -0
  411. package/src/internal/live-queries.ts +60 -0
  412. package/src/internal/page-events.ts +27 -0
  413. package/src/internal/page-execution.ts +605 -45
  414. package/src/internal/parity.ts +35 -6
  415. package/src/internal/ppr/admission.ts +300 -0
  416. package/src/internal/ppr/canary.ts +122 -0
  417. package/src/internal/ppr/compile-shell.ts +379 -0
  418. package/src/internal/ppr/contract.ts +265 -0
  419. package/src/internal/ppr/frames.ts +170 -0
  420. package/src/internal/ppr/integration.ts +137 -0
  421. package/src/internal/ppr/options.ts +221 -0
  422. package/src/internal/ppr/route-rules.ts +77 -0
  423. package/src/internal/ppr/runtime.ts +39 -0
  424. package/src/internal/ppr/server.ts +1766 -0
  425. package/src/internal/ppr/shell-store.ts +152 -0
  426. package/src/internal/prerendered-pages.ts +7 -4
  427. package/src/internal/present.ts +20 -0
  428. package/src/internal/query-digest.ts +34 -0
  429. package/src/internal/query-runtime.ts +14 -1
  430. package/src/internal/query-seam.ts +48 -0
  431. package/src/internal/render-document.ts +158 -113
  432. package/src/internal/request-abort.ts +26 -0
  433. package/src/internal/server-boundary.ts +10 -1
  434. package/src/internal/share-data.ts +136 -0
  435. package/src/internal/status-page.ts +95 -0
  436. package/src/internal/store-signature.ts +149 -0
  437. package/src/internal/vite-plugin-compat.ts +2 -2
  438. package/src/internal/web-app.ts +123 -3
  439. package/src/islands-swap.ts +283 -0
  440. package/src/islands.ts +169 -72
  441. package/src/isr.ts +348 -106
  442. package/src/keep-alive.ts +1354 -0
  443. package/src/live/bus.ts +104 -0
  444. package/src/live/core.ts +52 -0
  445. package/src/live/history.ts +109 -0
  446. package/src/live/hub-protocol.ts +133 -0
  447. package/src/live/hub.ts +620 -0
  448. package/src/live/keyring.ts +384 -0
  449. package/src/live/read-capped.ts +36 -0
  450. package/src/live/revocations.ts +155 -0
  451. package/src/live/settings.ts +46 -0
  452. package/src/live-client.ts +1011 -0
  453. package/src/live-hub.ts +658 -0
  454. package/src/live-redis.ts +284 -0
  455. package/src/live.ts +1257 -0
  456. package/src/manifest.ts +274 -6
  457. package/src/navigation.ts +14 -2
  458. package/src/plugins/vite-zone-guard.ts +15 -2
  459. package/src/ppr.ts +153 -0
  460. package/src/query.ts +147 -20
  461. package/src/render-seam.ts +37 -0
  462. package/src/route-types.ts +3 -1
  463. package/src/router.ts +534 -132
  464. package/src/server-components/build.ts +9 -3
  465. package/src/server-components/client.ts +89 -5
  466. package/src/server-components/definition.ts +2 -1
  467. package/src/server-components/hole.ts +23 -0
  468. package/src/server-components/html-policy.ts +63 -4
  469. package/src/server-components/html-scan.ts +159 -0
  470. package/src/server-components/html.ts +18 -4
  471. package/src/server-components/router-integration.ts +21 -2
  472. package/src/server-components/runtime.ts +114 -31
  473. package/src/server-components/state.ts +66 -44
  474. package/src/server-components/types.ts +16 -0
  475. package/src/server-components/vite.ts +33 -14
  476. package/src/vite.ts +75 -6
  477. package/dist/nano.d.ts +0 -125
  478. package/dist/nano.d.ts.map +0 -1
  479. package/dist/nano.js +0 -190
  480. package/dist/nano.js.map +0 -1
  481. package/src/nano.ts +0 -246
@@ -0,0 +1,1011 @@
1
+ /**
2
+ * Live refresh in the browser. The client entry of an app with `live` builds its router with this
3
+ * module's `fetchData`, so every route load is a live load that returns a grant beside its data, and
4
+ * attaches the router once it is mounted. One WebSocket per tab watches the grants of what is on
5
+ * screen - the page, and the target an intercept boundary holds - and a change to a key either read
6
+ * reloads it, keeping every owner whose keys did not change.
7
+ *
8
+ * A frame whose load returned no grant, and every frame while the socket is down, is polled instead.
9
+ *
10
+ * Query hooks that name live keys refetch on the same changes: each load carries a salted digest of
11
+ * every key it grants, and the client matches the keys the page's queries name against them.
12
+ */
13
+
14
+ import type { BoundaryState, BoundaryStates } from "./boundary.ts"
15
+ import {
16
+ advanceLiveTick,
17
+ clearQueryClients,
18
+ type LiveQuery,
19
+ liveQueries,
20
+ registeredAt,
21
+ watchLiveQueries,
22
+ } from "./internal/live-queries.ts"
23
+ import { queryDigester } from "./internal/query-digest.ts"
24
+ import { appUrl } from "./internal/route-path.ts"
25
+ import {
26
+ type ClientRouter,
27
+ DATA_HEADER,
28
+ type FetchRouteData,
29
+ HOLE_FILL_HEADER,
30
+ isRouteDataEnvelope,
31
+ NAV_FROM_HEADER,
32
+ RETAIN_HEADER,
33
+ type RouteDataEnvelope,
34
+ routeDataOf,
35
+ } from "./router.ts"
36
+
37
+ export interface LiveClientOptions {
38
+ /** The live socket, as `live({ path })` serves it. Default `/_nifra/live`. */
39
+ readonly path?: string
40
+ /** The live load endpoint, as `live({ loadPath })` serves it. Default `/_nifra/live/load`. */
41
+ readonly loadPath?: string
42
+ /**
43
+ * Sends every live load. Give one that adds an `Authorization` header when the app authenticates
44
+ * that way; cookies are sent by default.
45
+ */
46
+ readonly fetch?: (input: string, init: RequestInit) => Promise<Response>
47
+ /** Route id to its layout ids, outermost first. The generated client entry passes its own table. */
48
+ readonly layoutIdsOf?: Readonly<Record<string, readonly string[]>>
49
+ }
50
+
51
+ export interface LiveClient {
52
+ /** Pass as `createClientRouter({ fetchData })`: every route load goes through the live load endpoint. */
53
+ readonly fetchData: FetchRouteData
54
+ /** Keep what `router` shows fresh. Returns a function that stops and closes the socket. */
55
+ attach(router: ClientRouter): () => void
56
+ }
57
+
58
+ const HEARTBEAT = "p"
59
+ const HEARTBEAT_MS = 30_000
60
+ const RENEW_BEFORE_MS = 60_000
61
+ const RENEW_JITTER_MS = 30_000
62
+ const POLL_MS = 30_000
63
+ const MAX_POLL_MS = 300_000
64
+ const FOCUS_RELOAD_MS = 5_000
65
+ const RETRY_MS = 1_000
66
+ const MAX_RECONNECT_MS = 30_000
67
+ /** Failed connects in a row before frames poll instead of waiting for the socket. */
68
+ const CONNECTS_BEFORE_POLLING = 3
69
+ const MAX_SPREAD_MS = 5_000
70
+ const PAGE = "page"
71
+ const SID = /^[0-9a-f]{32}$/
72
+ /** The socket id of a seed: the grant the server rendered the page with, bound to no socket yet. */
73
+ const SEED = ""
74
+
75
+ /**
76
+ * What a live load handed the router, carried in its state beside the data it loaded. A load whose
77
+ * holes were still loading gets its grant after them: until then `trailing` is set and the fields the
78
+ * envelope fills are empty.
79
+ */
80
+ interface Loaded {
81
+ readonly routeId: string
82
+ /** The socket id the grant is bound to. */
83
+ readonly sid: string
84
+ grant: string | undefined
85
+ /** Wire tag to the owners in this load that watch its key. */
86
+ tags: ReadonlyMap<string, readonly string[]>
87
+ /** Client clock, in ms. */
88
+ expiresAt: number
89
+ ttlMs: number
90
+ /** The live tick its response arrived at. */
91
+ readonly tick: number
92
+ query: QueryKeys | undefined
93
+ trailing: boolean
94
+ }
95
+
96
+ /** What a load tells query hooks about the keys it grants. */
97
+ interface QueryKeys {
98
+ /** Absent when the app has `queryKeys` off. */
99
+ readonly salt: string | undefined
100
+ /** Digest of a granted key under `salt` to its wire tag. */
101
+ readonly tags: ReadonlyMap<string, string>
102
+ readonly dev: boolean
103
+ }
104
+
105
+ /** A live load the client itself asked the router for, read back by `fetchData` in the same call. */
106
+ interface Intent {
107
+ readonly path: string
108
+ readonly mode: "load" | "renew"
109
+ readonly held: Loaded | undefined
110
+ readonly retain: readonly string[]
111
+ /** What is on screen now: the copies a retained owner keeps. */
112
+ readonly data: unknown
113
+ readonly layoutData: readonly unknown[] | undefined
114
+ readonly boundaries: BoundaryStates | undefined
115
+ }
116
+
117
+ /** A load on screen: the page, or the target an intercept boundary holds. */
118
+ interface Frame {
119
+ /** `"page"`, or the boundary's name. */
120
+ readonly slot: string
121
+ path: string
122
+ readonly routeId: string
123
+ readonly loaded: Loaded | undefined
124
+ readonly shownAt: number
125
+ /** Owners known to have changed since their data loaded. */
126
+ dirty: Set<string>
127
+ /** Every owner reloads: there is no grant to keep any of them by. */
128
+ whole: boolean
129
+ /** The frame holds a seed and a socket is open: one load keeping every owner trades it for a grant. */
130
+ exchange: boolean
131
+ /** The watch generation that installed this frame's grant, once `ready` confirmed it. */
132
+ installed: boolean
133
+ /** Set while a watch carrying this frame's grant waits for its `ready`. */
134
+ pending: number | undefined
135
+ loading: boolean
136
+ failures: number
137
+ retryAt: number
138
+ renewAt: number
139
+ pollAt: number
140
+ }
141
+
142
+ export function liveClient(options: LiveClientOptions = {}): LiveClient {
143
+ const socketPath = options.path ?? "/_nifra/live"
144
+ const loadPath = options.loadPath ?? "/_nifra/live/load"
145
+ const send = options.fetch ?? ((input: string, init: RequestInit) => fetch(input, init))
146
+ const layoutIdsOf = options.layoutIdsOf ?? {}
147
+ // Loads made with no socket are bound to an id no socket has, so their grants are never watched.
148
+ const localSid = randomSid()
149
+ let router: ClientRouter | undefined
150
+ let sid: string | undefined
151
+ // The last load's viewer tags: one per secret the server lists.
152
+ let subjects: readonly string[] | undefined
153
+ let switched = false
154
+ let intent: Intent | undefined
155
+ // The handles this client put in router state: anything else there is not a live load.
156
+ const issued = new WeakMap<object, Loaded>()
157
+ const loadedOf = (value: unknown): Loaded | undefined =>
158
+ typeof value === "object" && value !== null ? issued.get(value) : undefined
159
+ let frames: Frame[] = []
160
+ let stopped = false
161
+ let timer: ReturnType<typeof setTimeout> | undefined
162
+ // A frame with no usable grant gets one load for one under each socket; after that it polls.
163
+ const tried = new Set<string>()
164
+ const pollFailures = new Map<string, number>()
165
+ let ws: WebSocket | undefined
166
+ let heartbeat: ReturnType<typeof setInterval> | undefined
167
+ let reconnect: ReturnType<typeof setTimeout> | undefined
168
+ let connectFailures = 0
169
+ // The spread the last `welcome` asked for: the reloads a new socket id needs go out over it.
170
+ let welcomeSpread = 0
171
+ let gen = 0
172
+ let sent:
173
+ | { readonly gen: number; readonly frames: readonly Frame[]; readonly grants: string }
174
+ | undefined
175
+ // Set by an account switch: the next socket's `welcome` reloads everything under its id.
176
+ let clearOnWelcome = false
177
+ // Query hooks: wire tag to the queries whose keys it carries, from the last match.
178
+ let matched = new Map<string, Set<LiveQuery>>()
179
+ // Tag to the live tick of the load from which an installed watch has held it without a break; a
180
+ // query that asked for its data before that tick may have missed a change.
181
+ const heldSince = new Map<string, number>()
182
+ let matching = 0
183
+ let matchScheduled = false
184
+ let matchedLoads: readonly (Loaded | undefined)[] = []
185
+ let changedWhileMatching: Set<string> | undefined
186
+ // Salt to its digester and the digests it made: a salt lives as long as its load is on screen.
187
+ const digesters = new Map<
188
+ string,
189
+ {
190
+ readonly sign: Promise<(key: string) => Promise<string>>
191
+ readonly made: Map<string, Promise<string>>
192
+ }
193
+ >()
194
+ const warned = new Set<string>()
195
+ let unwatchQueries: (() => void) | undefined
196
+
197
+ /** The owners a load of `routeId` may keep: its layouts, its page and the holes `shown` holds. */
198
+ const ownersOf = (routeId: string, shown: BoundaryStates | undefined): string[] => {
199
+ const owners = [...(layoutIdsOf[routeId] ?? []).map((id) => `L:${id}`), `P:${routeId}`]
200
+ for (const state of Object.values(shown ?? {}))
201
+ if (state.mode === "dynamic" || state.mode === "shared")
202
+ owners.push(`B:${state.name}:${routeId}`)
203
+ return owners
204
+ }
205
+
206
+ /** A load's envelope written into its handle; a new viewer clears what the last one cached. */
207
+ const settleLoaded = (loaded: Loaded, live: ServerEnvelope | undefined): void => {
208
+ loaded.trailing = false
209
+ if (live === undefined) return
210
+ const last = subjects
211
+ if (last !== undefined && !live.subjects.some((tag) => last.includes(tag))) {
212
+ switched = true
213
+ // Nothing a query loaded for the last viewer may show to this one.
214
+ clearQueryClients()
215
+ }
216
+ subjects = live.subjects
217
+ loaded.grant = live.grant
218
+ loaded.tags = new Map(Object.entries(live.tags ?? {}))
219
+ loaded.expiresAt =
220
+ live.ttl === undefined ? Number.POSITIVE_INFINITY : Date.now() + live.ttl * 1000
221
+ loaded.ttlMs = live.ttl === undefined ? Number.POSITIVE_INFINITY : live.ttl * 1000
222
+ loaded.query = live.query
223
+ }
224
+
225
+ /** A plain data GET: the router's own transport, for a navigation the live endpoint turned away. */
226
+ const plainLoad: FetchRouteData = async (path, _match, signal, navigation, _purpose, hosts) => {
227
+ const headers = new Headers({ [DATA_HEADER]: "1" })
228
+ if (hosts !== undefined && hosts.length > 0) headers.set(HOLE_FILL_HEADER, hosts.join(","))
229
+ if (navigation !== undefined && navigation.retain.length > 0) {
230
+ headers.set(RETAIN_HEADER, navigation.retain.join(","))
231
+ headers.set(NAV_FROM_HEADER, navigation.from)
232
+ }
233
+ return routeDataOf(await send(appUrl(path), { headers, signal: signal ?? null }), path, signal)
234
+ }
235
+
236
+ const fetchData: FetchRouteData = async (path, match, signal, navigation, purpose, hosts) => {
237
+ // Read before anything awaits: the client's own reload calls the router, which calls here at once.
238
+ const own = intent?.path === path ? intent : undefined
239
+ if (own !== undefined) intent = undefined
240
+ const bound = sid ?? localSid
241
+ let held = own?.held
242
+ const retain: string[] = own === undefined ? [] : [...own.retain]
243
+ if (own === undefined && navigation !== undefined && navigation.retain.length > 0) {
244
+ // A navigation offering its layouts presents the page it leaves: the server keeps those a
245
+ // router navigation would, and none whose keys changed.
246
+ const page = frames.find((frame) => frame.slot === PAGE)
247
+ held = page?.loaded
248
+ const from = layoutIdsOf[router?.snapshot().routeId ?? ""] ?? []
249
+ for (const index of navigation.retain) {
250
+ const id = from[index]
251
+ if (id !== undefined && page?.dirty.has(`L:${id}`) === false) retain.push(`L:${id}`)
252
+ }
253
+ }
254
+ const grant =
255
+ held?.grant !== undefined && (held.sid === bound || held.sid === SEED)
256
+ ? held.grant
257
+ : undefined
258
+ const res = await send(appUrl(loadPath), {
259
+ method: "POST",
260
+ headers: { "content-type": "application/json" },
261
+ body: JSON.stringify({
262
+ path,
263
+ sid: bound,
264
+ frame: PAGE,
265
+ mode: own?.mode ?? "load",
266
+ retain: grant === undefined ? [] : retain,
267
+ grants: grant === undefined ? [] : [grant],
268
+ fill: hosts !== undefined && hosts.length > 0 ? hosts : undefined,
269
+ }),
270
+ signal: signal ?? null,
271
+ })
272
+ if (res.status === 503) {
273
+ await res.body?.cancel()
274
+ // Admission bounds reloads; a navigation the user asked for is not held behind it.
275
+ if (own === undefined) return plainLoad(path, match, signal, navigation, purpose, hosts)
276
+ throw new Error(`[nifra/web] live load busy: ${path}`)
277
+ }
278
+ const payload = await routeDataOf(res, path, signal)
279
+ if (!isRouteDataEnvelope(payload)) return payload
280
+ const kept = own === undefined || grant === undefined ? payload : keptBy(payload, own)
281
+ const loaded: Loaded = {
282
+ routeId: match.routeId,
283
+ sid: bound,
284
+ grant: undefined,
285
+ tags: new Map(),
286
+ expiresAt: Number.POSITIVE_INFINITY,
287
+ ttlMs: Number.POSITIVE_INFINITY,
288
+ tick: advanceLiveTick(),
289
+ query: undefined,
290
+ trailing: false,
291
+ }
292
+ const trailing = deferredOf(payload.live)
293
+ if (trailing !== undefined) {
294
+ // The grant follows the holes: the frame waits for it, then watches or polls as any other.
295
+ loaded.trailing = true
296
+ issued.set(loaded, loaded)
297
+ trailing.then(
298
+ (value) => granted(loaded, envelopeOf(value)),
299
+ () => granted(loaded, undefined),
300
+ )
301
+ return { ...kept, live: loaded }
302
+ }
303
+ const live = envelopeOf(payload.live)
304
+ if (live === undefined) return { ...kept, live: undefined }
305
+ settleLoaded(loaded, live)
306
+ issued.set(loaded, loaded)
307
+ // The page loader did not run: show the copy already on screen.
308
+ if (own !== undefined && grant !== undefined && live.page === "retained")
309
+ return { ...kept, data: own.data, live: loaded }
310
+ return { ...kept, live: loaded }
311
+ }
312
+
313
+ /** What the server kept of what was offered, and is still current: the copies already on screen. */
314
+ const keptBy = (payload: RouteDataEnvelope, own: Intent): RouteDataEnvelope => {
315
+ const retained = new Set(payload.retained ?? [])
316
+ const shown = own.layoutData
317
+ let boundaries = payload.boundaries
318
+ for (const [name, state] of Object.entries(payload.boundaries ?? {})) {
319
+ const held = own.boundaries?.[name]
320
+ if (state.kept !== true || held === undefined) continue
321
+ boundaries = { ...boundaries, [name]: held satisfies BoundaryState }
322
+ }
323
+ const layoutData = payload.layoutData?.map((value, index) =>
324
+ retained.has(index) && shown !== undefined && index < shown.length ? shown[index] : value,
325
+ )
326
+ return {
327
+ ...payload,
328
+ ...(layoutData === undefined ? {} : { layoutData }),
329
+ ...(boundaries === undefined ? {} : { boundaries }),
330
+ }
331
+ }
332
+
333
+ /**
334
+ * The server-rendered page's seed, in the router's initial state: held as the page's load, it is
335
+ * traded for a socket's grant once one opens. Anything else there is ignored.
336
+ */
337
+ const adoptSeed = (state: { readonly routeId: string; readonly live?: unknown }): void => {
338
+ const value = state.live
339
+ if (typeof value !== "object" || value === null || issued.has(value)) return
340
+ const live = envelopeOf(value)
341
+ if (live?.grant === undefined) return
342
+ const loaded: Loaded = {
343
+ routeId: state.routeId,
344
+ sid: SEED,
345
+ grant: undefined,
346
+ tags: new Map(),
347
+ expiresAt: Number.POSITIVE_INFINITY,
348
+ ttlMs: Number.POSITIVE_INFINITY,
349
+ tick: advanceLiveTick(),
350
+ query: undefined,
351
+ trailing: false,
352
+ }
353
+ settleLoaded(loaded, live)
354
+ issued.set(value, loaded)
355
+ }
356
+
357
+ /** A trailing grant arrived, or will not: the frame holding `loaded` watches or polls from now. */
358
+ const granted = (loaded: Loaded, live: ServerEnvelope | undefined): void => {
359
+ settleLoaded(loaded, live)
360
+ for (const frame of frames) if (frame.loaded === loaded) frame.renewAt = renewAtOf(loaded)
361
+ reconcile()
362
+ }
363
+
364
+ // --- On screen -----------------------------------------------------------------------------------
365
+
366
+ const watchable = (frame: Frame): boolean =>
367
+ frame.loaded?.grant !== undefined && frame.loaded.sid === sid
368
+ /** A socket serves a frame holding a tag, or one still to get a grant; a page with neither needs none. */
369
+ const wantsSocket = (): boolean =>
370
+ frames.length === 0 ||
371
+ frames.some((frame) => frame.loaded?.grant === undefined || frame.loaded.tags.size > 0)
372
+ const hidden = (): boolean => globalThis.document?.visibilityState === "hidden"
373
+ const polling = (): boolean => sid === undefined && connectFailures >= CONNECTS_BEFORE_POLLING
374
+
375
+ const frameFor = (
376
+ slot: string,
377
+ path: string,
378
+ routeId: string,
379
+ loaded: Loaded | undefined,
380
+ now: number,
381
+ ): Frame => {
382
+ const same = frames.find(
383
+ (frame) =>
384
+ frame.slot === slot &&
385
+ frame.loaded === loaded &&
386
+ (loaded !== undefined || frame.path === path),
387
+ )
388
+ if (same !== undefined) {
389
+ // A client-only search change keeps the load and moves its URL.
390
+ same.path = path
391
+ return same
392
+ }
393
+ return {
394
+ slot,
395
+ path,
396
+ routeId: loaded?.routeId ?? routeId,
397
+ loaded,
398
+ shownAt: now,
399
+ dirty: new Set(),
400
+ whole: false,
401
+ exchange: false,
402
+ installed: false,
403
+ pending: undefined,
404
+ loading: false,
405
+ failures: 0,
406
+ retryAt: 0,
407
+ renewAt: loaded === undefined ? Number.POSITIVE_INFINITY : renewAtOf(loaded),
408
+ pollAt: now + jittered(POLL_MS * 2 ** Math.min(pollFailures.get(path) ?? 0, 4), MAX_POLL_MS),
409
+ }
410
+ }
411
+
412
+ const reconcile = (): void => {
413
+ const current = router
414
+ if (stopped || current === undefined) return
415
+ const state = current.snapshot()
416
+ const now = Date.now()
417
+ const next = [
418
+ frameFor(
419
+ PAGE,
420
+ state.intercepted?.pagePath ?? state.path,
421
+ state.routeId,
422
+ loadedOf(state.live),
423
+ now,
424
+ ),
425
+ ]
426
+ const intercepted = state.intercepted
427
+ const held = intercepted === undefined ? undefined : state.boundaries?.[intercepted.boundary]
428
+ if (intercepted !== undefined && held?.status === "ready") {
429
+ next.push(
430
+ frameFor(
431
+ intercepted.boundary,
432
+ state.path,
433
+ current.match(state.path)?.routeId ?? "",
434
+ loadedOf(held.live),
435
+ now,
436
+ ),
437
+ )
438
+ }
439
+ frames = next
440
+ // Nothing to watch let the last socket go; a frame that now holds a tag opens the next.
441
+ if (ws === undefined && reconnect === undefined && wantsSocket()) connect()
442
+ for (const frame of frames) {
443
+ // No grant to watch by yet: load one once the socket can take it. A trailing one is on its way.
444
+ if (
445
+ !watchable(frame) &&
446
+ frame.loaded?.trailing !== true &&
447
+ sid !== undefined &&
448
+ !tried.has(`${sid}\n${frame.path}`)
449
+ ) {
450
+ tried.add(`${sid}\n${frame.path}`)
451
+ // A seed needs no loader to run: the server keeps every owner and binds a grant to this socket.
452
+ if (frame.loaded?.sid === SEED) frame.exchange = true
453
+ else frame.whole = true
454
+ // A new socket is a new chance: no backoff carries over, only the spread it was given.
455
+ frame.failures = 0
456
+ frame.retryAt = welcomeSpread > 0 ? now + Math.random() * welcomeSpread : 0
457
+ }
458
+ }
459
+ welcomeSpread = 0
460
+ if (switched && !state.pending) {
461
+ // Another viewer: nothing kept, cached or watched for the last one survives.
462
+ switched = false
463
+ restart()
464
+ return
465
+ }
466
+ const loads = frames.map((frame) => frame.loaded)
467
+ if (loads.length !== matchedLoads.length || loads.some((l, i) => l !== matchedLoads[i])) {
468
+ matchedLoads = loads
469
+ scheduleMatch()
470
+ }
471
+ sendWatch()
472
+ run()
473
+ }
474
+
475
+ // A kept page shown again reloads itself, and that load brings the grant to watch it by.
476
+ const busy = (current: ClientRouter): boolean => {
477
+ const state = current.snapshot()
478
+ return state.pending || state.revalidating === true
479
+ }
480
+
481
+ const run = (): void => {
482
+ const current = router
483
+ if (stopped || current === undefined || hidden() || busy(current)) return
484
+ const now = Date.now()
485
+ for (const frame of frames) {
486
+ if (frame.loading || wakeOf(frame) > now) continue
487
+ if (frame.whole || frame.exchange || frame.dirty.size > 0) reload(frame, "load")
488
+ else if (watchable(frame)) reload(frame, "renew")
489
+ else reload(frame, "load", true)
490
+ // One load at a time: the router runs it, and its next state runs this again.
491
+ if (busy(current)) break
492
+ }
493
+ arm()
494
+ }
495
+
496
+ /**
497
+ * When `run` loads this frame next; infinite when only an event can make it due. One rule for both
498
+ * `run` and `arm`: a deadline `run` would skip must never be armed, or the timer spins until it can.
499
+ */
500
+ const wakeOf = (frame: Frame): number => {
501
+ let due: number
502
+ if (frame.whole || frame.exchange || frame.dirty.size > 0) due = 0
503
+ // Its grant trails the holes still loading; it arrives with or without one, and runs this again.
504
+ else if (frame.loaded?.trailing === true) due = Number.POSITIVE_INFINITY
505
+ // A renewal waits for the pending watch's `ready`, which runs this again.
506
+ else if (watchable(frame))
507
+ due = frame.pending === undefined ? frame.renewAt : Number.POSITIVE_INFINITY
508
+ else if (polling() || frame.loaded?.grant === undefined) due = frame.pollAt
509
+ else due = Number.POSITIVE_INFINITY
510
+ return Math.max(frame.retryAt, due)
511
+ }
512
+
513
+ /** Wake for the earliest frame due. */
514
+ const arm = (): void => {
515
+ if (timer !== undefined) clearTimeout(timer)
516
+ timer = undefined
517
+ if (stopped) return
518
+ let at = Number.POSITIVE_INFINITY
519
+ for (const frame of frames) if (!frame.loading) at = Math.min(at, wakeOf(frame))
520
+ if (at === Number.POSITIVE_INFINITY) return
521
+ timer = setTimeout(run, Math.max(0, at - Date.now()))
522
+ }
523
+
524
+ /** `polled`: no change notice can reach this frame, so its queries refetch with it. */
525
+ const reload = (frame: Frame, mode: "load" | "renew", polled = false): void => {
526
+ const current = router
527
+ if (current === undefined) return
528
+ const state = current.snapshot()
529
+ const page = frame.slot === PAGE
530
+ const whole = frame.whole
531
+ const exchange = frame.exchange
532
+ // A poll is for fresh data: no change notice reaches the frame, so no owner can be kept.
533
+ const held = whole || polled ? undefined : frame.loaded
534
+ intent = {
535
+ path: frame.path,
536
+ mode: held === undefined ? "load" : mode,
537
+ held,
538
+ retain:
539
+ held === undefined || mode === "renew"
540
+ ? []
541
+ : ownersOf(frame.routeId, page ? state.boundaries : undefined).filter(
542
+ (owner) => !frame.dirty.has(owner),
543
+ ),
544
+ data: page ? state.data : state.boundaries?.[frame.slot]?.data,
545
+ layoutData: page ? state.layoutData : undefined,
546
+ boundaries: page ? state.boundaries : undefined,
547
+ }
548
+ frame.loading = true
549
+ frame.whole = false
550
+ frame.exchange = false
551
+ let running: Promise<void>
552
+ try {
553
+ running = current.invalidate([frame.path])
554
+ } finally {
555
+ intent = undefined
556
+ }
557
+ const settle = (ok: boolean): void => {
558
+ frame.loading = false
559
+ // Applied, the load is a new frame; still on screen, nothing was applied.
560
+ if (ok && !frames.includes(frame)) {
561
+ pollFailures.delete(frame.path)
562
+ if (polled) for (const query of liveQueries()) query.refresh()
563
+ return
564
+ }
565
+ if (!ok && !watchable(frame))
566
+ pollFailures.set(frame.path, (pollFailures.get(frame.path) ?? 0) + 1)
567
+ // Still no grant to keep an owner by, or a seed it could not trade: the retry loads it whole.
568
+ if (whole || exchange) frame.whole = true
569
+ frame.failures++
570
+ // Backs off to five minutes, never further: a frame that gives up shows stale data for good.
571
+ // A change, a visibility change or a new socket tries again at once.
572
+ frame.retryAt =
573
+ Date.now() + jittered(RETRY_MS * 2 ** Math.min(frame.failures - 1, 9), MAX_POLL_MS)
574
+ if (!watchable(frame))
575
+ frame.pollAt =
576
+ Date.now() + jittered(POLL_MS * 2 ** Math.min(frame.failures, 4), MAX_POLL_MS)
577
+ run()
578
+ }
579
+ running.then(
580
+ () => settle(true),
581
+ () => settle(false),
582
+ )
583
+ }
584
+
585
+ // --- The socket ----------------------------------------------------------------------------------
586
+
587
+ const sendWatch = (): void => {
588
+ const socket = ws
589
+ if (socket === undefined || sid === undefined || socket.readyState !== 1) return
590
+ const watched = frames.filter(watchable)
591
+ const grants = watched.map((frame) => frame.loaded?.grant ?? "")
592
+ const key = grants.join("\n")
593
+ if (sent === undefined && grants.length === 0) return
594
+ if (sent?.grants === key) {
595
+ sent = { gen: sent.gen, frames: watched, grants: key }
596
+ return
597
+ }
598
+ gen++
599
+ sent = { gen, frames: watched, grants: key }
600
+ for (const frame of watched) if (!frame.installed) frame.pending = gen
601
+ socket.send(JSON.stringify({ t: "watch", gen, grants }))
602
+ }
603
+
604
+ const onReady = (value: Record<string, unknown>): void => {
605
+ if (sent === undefined || value.gen !== sent.gen || !Array.isArray(value.stale)) return
606
+ const stale: unknown[] = value.stale
607
+ sent.frames.forEach((frame, index) => {
608
+ const changed = stale[index]
609
+ frame.pending = undefined
610
+ frame.installed = true
611
+ // Every change since each owner's data loaded is in this list; nothing before `ready` counts.
612
+ frame.dirty = new Set(
613
+ Array.isArray(changed) ? changed.filter((owner) => typeof owner === "string") : [],
614
+ )
615
+ // A query may have read before one of those changes: refetch every query on a tag they own.
616
+ for (const [tag, owners] of frame.loaded?.tags ?? []) {
617
+ if (!owners.some((owner) => frame.dirty.has(owner))) continue
618
+ changedWhileMatching?.add(tag)
619
+ refreshTag(tag)
620
+ }
621
+ })
622
+ const held = new Set<string>()
623
+ for (const frame of frames) {
624
+ if (!frame.installed || frame.loaded === undefined) continue
625
+ for (const tag of frame.loaded.tags.keys()) {
626
+ held.add(tag)
627
+ if (!heldSince.has(tag)) heldSince.set(tag, frame.loaded.tick)
628
+ }
629
+ }
630
+ for (const tag of heldSince.keys()) if (!held.has(tag)) heldSince.delete(tag)
631
+ cover()
632
+ run()
633
+ }
634
+
635
+ const onChanged = (value: Record<string, unknown>): void => {
636
+ const { tag, spread } = value
637
+ if (typeof tag !== "string") return
638
+ const delay =
639
+ typeof spread === "number" && spread > 0 ? Math.random() * Math.min(spread, MAX_SPREAD_MS) : 0
640
+ for (const frame of frames) {
641
+ const owners = frame.loaded?.tags.get(tag)
642
+ // A tag this frame does not hold is another load's; a pending frame's `ready` settles it.
643
+ if (owners === undefined || frame.pending !== undefined || !watchable(frame)) continue
644
+ const mark = (): void => {
645
+ if (!frames.includes(frame) || frame.pending !== undefined) return
646
+ for (const owner of owners) frame.dirty.add(owner)
647
+ frame.failures = 0
648
+ frame.retryAt = 0
649
+ run()
650
+ }
651
+ if (delay === 0) mark()
652
+ else setTimeout(mark, delay)
653
+ }
654
+ changedWhileMatching?.add(tag)
655
+ if (!matched.has(tag)) return
656
+ if (delay === 0) refreshTag(tag)
657
+ else setTimeout(() => refreshTag(tag), delay)
658
+ }
659
+
660
+ // --- Query hooks ---------------------------------------------------------------------------------
661
+
662
+ const refreshTag = (tag: string): void => {
663
+ for (const query of matched.get(tag) ?? []) query.refresh()
664
+ }
665
+
666
+ /** Refetch each query that asked for its data before its tag's watch, or its own registration, began. */
667
+ const cover = (): void => {
668
+ for (const [tag, queries] of matched) {
669
+ const held = heldSince.get(tag)
670
+ if (held === undefined) continue
671
+ for (const query of queries) {
672
+ const since = Math.max(held, registeredAt(query) ?? Number.POSITIVE_INFINITY)
673
+ if (query.tick() < since) query.refresh()
674
+ }
675
+ }
676
+ }
677
+
678
+ const scheduleMatch = (): void => {
679
+ if (stopped) return
680
+ // From here until the match lands, a change may concern a query it has not matched yet.
681
+ changedWhileMatching ??= new Set()
682
+ if (matchScheduled) return
683
+ matchScheduled = true
684
+ queueMicrotask(() => {
685
+ matchScheduled = false
686
+ matchQueries().catch(() => {})
687
+ })
688
+ }
689
+
690
+ const digestOf = (salt: string, key: string): Promise<string> => {
691
+ let digester = digesters.get(salt)
692
+ if (digester === undefined) {
693
+ digester = { sign: queryDigester(salt), made: new Map() }
694
+ digesters.set(salt, digester)
695
+ }
696
+ let made = digester.made.get(key)
697
+ if (made === undefined) {
698
+ const { sign } = digester
699
+ made = sign.then((digest) => digest(key))
700
+ digester.made.set(key, made)
701
+ }
702
+ return made
703
+ }
704
+
705
+ const matchQueries = async (): Promise<void> => {
706
+ const runId = ++matching
707
+ const sources = frames.flatMap((frame) =>
708
+ frame.loaded?.query === undefined ? [] : [frame.loaded.query],
709
+ )
710
+ const registered = [...liveQueries()]
711
+ const next = new Map<string, Set<LiveQuery>>()
712
+ const found = new Set<string>()
713
+ for (const source of sources) {
714
+ const { salt } = source
715
+ if (salt === undefined) continue
716
+ await Promise.all(
717
+ registered.flatMap((query) =>
718
+ query.keys().map(async (key) => {
719
+ const tag = source.tags.get(await digestOf(salt, key))
720
+ if (tag === undefined) return
721
+ found.add(key)
722
+ let queries = next.get(tag)
723
+ if (queries === undefined) {
724
+ queries = new Set()
725
+ next.set(tag, queries)
726
+ }
727
+ queries.add(query)
728
+ }),
729
+ ),
730
+ )
731
+ }
732
+ if (runId !== matching || stopped) return
733
+ const salts = new Set(sources.map((source) => source.salt))
734
+ for (const salt of digesters.keys()) if (!salts.has(salt)) digesters.delete(salt)
735
+ const changed = changedWhileMatching ?? new Set<string>()
736
+ changedWhileMatching = undefined
737
+ const previous = matched
738
+ matched = next
739
+ // A change that arrived while this ran found the old match: refetch what only this one has.
740
+ for (const tag of changed) {
741
+ for (const query of next.get(tag) ?? []) if (!previous.get(tag)?.has(query)) query.refresh()
742
+ }
743
+ cover()
744
+ if (registered.length > 0 && sources.length === frames.length)
745
+ warnUnmatched(sources, registered, found)
746
+ }
747
+
748
+ /** Development only: a query key nothing on the page grants never refetches. */
749
+ const warnUnmatched = (
750
+ sources: readonly QueryKeys[],
751
+ registered: readonly LiveQuery[],
752
+ found: ReadonlySet<string>,
753
+ ): void => {
754
+ if (!sources.some((source) => source.dev)) return
755
+ if (sources.every((source) => source.salt === undefined)) {
756
+ if (warned.has("")) return
757
+ warned.add("")
758
+ console.warn(
759
+ "[nifra/web] a query names live keys, but the app's live() has queryKeys off: no change refetches it",
760
+ )
761
+ return
762
+ }
763
+ for (const query of registered) {
764
+ for (const key of query.keys()) {
765
+ if (found.has(key) || warned.has(key)) continue
766
+ warned.add(key)
767
+ console.warn(
768
+ `[nifra/web] query live key "${key}" is not granted by any loader on this page: a change to it never refetches the query`,
769
+ )
770
+ }
771
+ }
772
+ }
773
+
774
+ const connect = (): void => {
775
+ reconnect = undefined
776
+ if (stopped) return
777
+ const url = new URL(appUrl(socketPath), globalThis.location?.href ?? "http://localhost/")
778
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:"
779
+ let socket: WebSocket
780
+ try {
781
+ socket = new WebSocket(url.href)
782
+ } catch {
783
+ closed(1006, false)
784
+ return
785
+ }
786
+ ws = socket
787
+ let welcomed = false
788
+ socket.onmessage = (event: MessageEvent) => {
789
+ if (ws !== socket || typeof event.data !== "string" || event.data === HEARTBEAT) return
790
+ let value: unknown
791
+ try {
792
+ value = JSON.parse(event.data)
793
+ } catch {
794
+ return
795
+ }
796
+ if (!isRecord(value)) return
797
+ if (value.t === "welcome" && typeof value.sid === "string" && SID.test(value.sid)) {
798
+ welcomed = true
799
+ connectFailures = 0
800
+ sid = value.sid
801
+ const spread = value.spread
802
+ welcomeSpread =
803
+ typeof spread === "number" && spread > 0 ? Math.min(spread, MAX_SPREAD_MS) : 0
804
+ heartbeat = setInterval(() => {
805
+ if (socket.readyState === 1) socket.send(HEARTBEAT)
806
+ }, HEARTBEAT_MS)
807
+ if (clearOnWelcome) {
808
+ clearOnWelcome = false
809
+ router?.invalidate().catch(() => {})
810
+ }
811
+ reconcile()
812
+ } else if (value.t === "ready") onReady(value)
813
+ else if (value.t === "changed") onChanged(value)
814
+ }
815
+ socket.onclose = (event: CloseEvent) => {
816
+ if (ws !== socket) return
817
+ closed(event.code, welcomed)
818
+ }
819
+ }
820
+
821
+ const closed = (code: number, welcomed: boolean): void => {
822
+ ws = undefined
823
+ sid = undefined
824
+ sent = undefined
825
+ // No watch survives the socket: changes until the next `ready` are not known.
826
+ heldSince.clear()
827
+ if (heartbeat !== undefined) clearInterval(heartbeat)
828
+ heartbeat = undefined
829
+ for (const frame of frames) {
830
+ frame.installed = false
831
+ frame.pending = undefined
832
+ }
833
+ if (stopped) return
834
+ if (!welcomed) connectFailures++
835
+ // A full server asks every client to wait; a protocol refusal is not retried at once.
836
+ const base = code === 1013 || code === 1008 ? 5_000 : RETRY_MS
837
+ if (wantsSocket()) {
838
+ reconnect = setTimeout(
839
+ connect,
840
+ jittered(base * 2 ** Math.min(connectFailures, 5), MAX_RECONNECT_MS),
841
+ )
842
+ }
843
+ run()
844
+ }
845
+
846
+ /** A new viewer: a new socket, every cache cleared, and what is on screen loaded under it. */
847
+ const restart = (): void => {
848
+ tried.clear()
849
+ const socket = ws
850
+ if (socket === undefined) {
851
+ router?.invalidate().catch(() => {})
852
+ return
853
+ }
854
+ ws = undefined
855
+ socket.close(1000)
856
+ closed(1000, true)
857
+ if (reconnect !== undefined) clearTimeout(reconnect)
858
+ clearOnWelcome = true
859
+ connect()
860
+ }
861
+
862
+ const onVisible = (): void => {
863
+ if (hidden()) return
864
+ for (const frame of frames) {
865
+ frame.failures = 0
866
+ frame.retryAt = 0
867
+ }
868
+ run()
869
+ }
870
+ const onFocus = (): void => {
871
+ const now = Date.now()
872
+ for (const frame of frames) {
873
+ if (!watchable(frame) && now - frame.shownAt > FOCUS_RELOAD_MS) frame.pollAt = now
874
+ }
875
+ run()
876
+ }
877
+
878
+ return {
879
+ fetchData,
880
+ attach(target) {
881
+ router = target
882
+ stopped = false
883
+ adoptSeed(target.snapshot())
884
+ const unsubscribe = target.subscribe(reconcile)
885
+ unwatchQueries = watchLiveQueries(scheduleMatch)
886
+ globalThis.document?.addEventListener("visibilitychange", onVisible)
887
+ globalThis.addEventListener?.("focus", onFocus)
888
+ connect()
889
+ reconcile()
890
+ return () => {
891
+ stopped = true
892
+ unsubscribe()
893
+ unwatchQueries?.()
894
+ unwatchQueries = undefined
895
+ globalThis.document?.removeEventListener("visibilitychange", onVisible)
896
+ globalThis.removeEventListener?.("focus", onFocus)
897
+ if (timer !== undefined) clearTimeout(timer)
898
+ if (reconnect !== undefined) clearTimeout(reconnect)
899
+ const socket = ws
900
+ ws = undefined
901
+ socket?.close(1000)
902
+ closed(1000, true)
903
+ router = undefined
904
+ }
905
+ },
906
+ }
907
+ }
908
+
909
+ interface ServerEnvelope {
910
+ readonly subjects: readonly string[]
911
+ readonly grant?: string
912
+ readonly tags?: Readonly<Record<string, readonly string[]>>
913
+ readonly page?: "retained"
914
+ readonly ttl?: number
915
+ readonly query?: QueryKeys
916
+ }
917
+
918
+ const MAX_SALT_CHARS = 64
919
+ const MAX_DIGEST_CHARS = 64
920
+ /** One per listed secret, at most three. */
921
+ const MAX_SUBJECTS = 3
922
+ const MAX_SUBJECT_CHARS = 64
923
+
924
+ /** The envelope's `query` member, or `undefined` for anything else in its place. */
925
+ function queryKeysOf(value: unknown): QueryKeys | undefined {
926
+ if (!isRecord(value)) return undefined
927
+ const { salt, keys, dev } = value
928
+ if (dev !== undefined && dev !== true) return undefined
929
+ if (salt !== undefined && (typeof salt !== "string" || salt.length > MAX_SALT_CHARS))
930
+ return undefined
931
+ const tags = new Map<string, string>()
932
+ if (keys !== undefined) {
933
+ if (!isRecord(keys)) return undefined
934
+ for (const [tag, digest] of Object.entries(keys)) {
935
+ if (typeof digest !== "string" || digest.length > MAX_DIGEST_CHARS) return undefined
936
+ tags.set(digest, tag)
937
+ }
938
+ }
939
+ return { salt, tags, dev: dev === true }
940
+ }
941
+
942
+ /** The server's live envelope, or `undefined` for anything else in its place. */
943
+ function envelopeOf(value: unknown): ServerEnvelope | undefined {
944
+ if (!isRecord(value)) return undefined
945
+ const { subjects, grant, tags, page, ttl, query } = value
946
+ if (
947
+ !Array.isArray(subjects) ||
948
+ subjects.length === 0 ||
949
+ subjects.length > MAX_SUBJECTS ||
950
+ !subjects.every((tag) => typeof tag === "string" && tag.length <= MAX_SUBJECT_CHARS)
951
+ ) {
952
+ return undefined
953
+ }
954
+ const queryKeys = query === undefined ? undefined : queryKeysOf(query)
955
+ if (query !== undefined && queryKeys === undefined) return undefined
956
+ if (grant !== undefined && typeof grant !== "string") return undefined
957
+ if (ttl !== undefined && (typeof ttl !== "number" || !(ttl > 0))) return undefined
958
+ if (page !== undefined && page !== "retained") return undefined
959
+ const owned: Record<string, readonly string[]> = {}
960
+ if (tags !== undefined) {
961
+ if (!isRecord(tags)) return undefined
962
+ for (const [tag, owners] of Object.entries(tags)) {
963
+ if (!Array.isArray(owners)) return undefined
964
+ const names: string[] = []
965
+ for (const owner of owners) {
966
+ if (typeof owner !== "string") return undefined
967
+ names.push(owner)
968
+ }
969
+ owned[tag] = names
970
+ }
971
+ }
972
+ const envelope: {
973
+ subjects: readonly string[]
974
+ tags: Record<string, readonly string[]>
975
+ grant?: string
976
+ page?: "retained"
977
+ ttl?: number
978
+ query?: QueryKeys
979
+ } = { subjects, tags: owned }
980
+ if (typeof grant === "string") envelope.grant = grant
981
+ if (page === "retained") envelope.page = page
982
+ if (typeof ttl === "number") envelope.ttl = ttl
983
+ if (queryKeys !== undefined) envelope.query = queryKeys
984
+ return envelope
985
+ }
986
+
987
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
988
+ typeof value === "object" && value !== null && !Array.isArray(value)
989
+
990
+ /** A minute before `loaded` expires, less up to 30 s of jitter; a short grant renews a third early. */
991
+ const renewAtOf = (loaded: Loaded): number =>
992
+ loaded.expiresAt -
993
+ Math.min(RENEW_BEFORE_MS, loaded.ttlMs / 3) -
994
+ Math.random() * Math.min(RENEW_JITTER_MS, loaded.ttlMs / 6)
995
+
996
+ /** The promise of a value the NDJSON stream settles later, as the data reader marks one. */
997
+ const deferredOf = (value: unknown): Promise<unknown> | undefined =>
998
+ isRecord(value) && value.__nifra_deferred === true && value.promise instanceof Promise
999
+ ? value.promise
1000
+ : undefined
1001
+
1002
+ /** `ms` spread over +/-20%, capped. */
1003
+ const jittered = (ms: number, max: number): number =>
1004
+ Math.min(max, ms * (0.8 + Math.random() * 0.4))
1005
+
1006
+ function randomSid(): string {
1007
+ const bytes = crypto.getRandomValues(new Uint8Array(16))
1008
+ let out = ""
1009
+ for (const byte of bytes) out += byte.toString(16).padStart(2, "0")
1010
+ return out
1011
+ }