@nifrajs/web 5.5.1 → 5.6.0

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