@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,658 @@
1
+ /**
2
+ * The live hub for Cloudflare Workers: one Durable Object per app owns the live log and holds every
3
+ * live socket, hibernating between events. Request isolates run live loads themselves: each asks the
4
+ * hub for the seq and the revocations its grants need, publishes through it, and forwards the socket
5
+ * handshake to it.
6
+ *
7
+ * ```ts
8
+ * import { liveHub } from "@nifrajs/web/live-hub"
9
+ *
10
+ * // A SQLite-backed Durable Object class, bound to the app's Worker as `LIVE_HUB`.
11
+ * export const LiveHub = liveHub((env: Env) => ({ secret: env.LIVE_SECRET }))
12
+ * // The app: live({ secret: env.LIVE_SECRET, subject, hub: env.LIVE_HUB })
13
+ * ```
14
+ *
15
+ * What outlives the hub's memory: the seq, the revocation floor and every stored revocation, in
16
+ * storage; each socket's membership (subject, expiry, tags) in storage under its sid; its sid, watch
17
+ * gen and rate counters in its attachment. History does not: after a wake a watermark older than the
18
+ * last seq before it is unknown and reads as stale. The runtime answers heartbeats without waking the
19
+ * hub, and one alarm runs at the earliest deadline: a grant expiry, a first watch, a revocation's
20
+ * retention.
21
+ */
22
+ import type { NifraWebSocket, WebSocketData } from "@nifrajs/core/server"
23
+ import { type LiveBusInput, nextSeq } from "./live/bus.ts"
24
+ import { applyEvent, type LiveCore } from "./live/core.ts"
25
+ import { History, keyHasher } from "./live/history.ts"
26
+ import {
27
+ CLOSE_CAPACITY,
28
+ FIRST_WATCH_MS,
29
+ type HubStore,
30
+ LiveHub,
31
+ type LiveSocket,
32
+ type StoredSocket,
33
+ topicOf,
34
+ } from "./live/hub.ts"
35
+ import {
36
+ HUB_APPEND_PATH,
37
+ HUB_AUTHORITY_PATH,
38
+ HUB_CONFIG_HEADER,
39
+ HUB_ORIGIN,
40
+ hubConfigMatches,
41
+ MAX_SUBJECT_CHARS,
42
+ parseAppend,
43
+ parseAuthorityRequest,
44
+ } from "./live/hub-protocol.ts"
45
+ import {
46
+ assertLiveSecrets,
47
+ createKeyring,
48
+ KEY_PATTERN,
49
+ type LiveSecrets,
50
+ SID_PATTERN,
51
+ } from "./live/keyring.ts"
52
+ import { readCapped } from "./live/read-capped.ts"
53
+ import { REVOCATION_SKEW_MS, Revocations } from "./live/revocations.ts"
54
+ import {
55
+ appIdOf,
56
+ DEFAULT_RELOAD_RATE,
57
+ grantTtlOf,
58
+ positive,
59
+ spreadFloorOf,
60
+ } from "./live/settings.ts"
61
+
62
+ export interface LiveHubOptions {
63
+ /** The app's `live()` secret, or its rotation list. */
64
+ readonly secret: string | readonly string[]
65
+ /** The app's `live()` `appId`. Default `"nifra"`. */
66
+ readonly appId?: string | undefined
67
+ /** The app's `live()` `grantTtl`, at most 3600. Default 600. */
68
+ readonly grantTtl?: number | undefined
69
+ /** Origins besides the app's own that may open the socket. */
70
+ readonly trustedOrigins?: readonly string[] | undefined
71
+ /** Sockets this hub holds; past it a socket is closed with 1013. Default {@link DEFAULT_HUB_SOCKETS}. */
72
+ readonly maxSockets?: number | undefined
73
+ /**
74
+ * Socket-tag subscriptions this hub holds in all; past it a watch is refused with 1013. The default,
75
+ * {@link DEFAULT_HUB_MEMBERSHIPS}, keeps the hub's heap well inside a Durable Object's memory.
76
+ */
77
+ readonly maxMemberships?: number | undefined
78
+ /** Live loads per second the deployment absorbs after one change, for spreading reloads. Default 500. */
79
+ readonly reloadRate?: number | undefined
80
+ /** The least time, in ms, clients spread their reloads over. Default 0. */
81
+ readonly spreadFloor?: number | undefined
82
+ }
83
+
84
+ /** The part of a Durable Object's state the hub uses (no Cloudflare types dependency). */
85
+ export interface LiveHubState {
86
+ readonly storage: LiveHubStorage
87
+ blockConcurrencyWhile<T>(callback: () => Promise<T>): Promise<T>
88
+ acceptWebSocket(ws: LiveHubWebSocket): void
89
+ getWebSockets(): LiveHubWebSocket[]
90
+ setWebSocketAutoResponse(pair?: unknown): void
91
+ getWebSocketAutoResponseTimestamp(ws: LiveHubWebSocket): Date | null
92
+ }
93
+
94
+ export interface LiveHubStorage {
95
+ get(keys: string[]): Promise<Map<string, unknown>>
96
+ put(entries: Record<string, unknown>): Promise<void>
97
+ delete(keys: string[]): Promise<number>
98
+ list(options: { prefix: string; end?: string; limit?: number }): Promise<Map<string, unknown>>
99
+ getAlarm(): Promise<number | null>
100
+ setAlarm(scheduledTime: number): Promise<void>
101
+ }
102
+
103
+ /** A hibernatable server socket, as far as the hub uses one. */
104
+ export interface LiveHubWebSocket {
105
+ send(message: string | ArrayBuffer | ArrayBufferView): void
106
+ close(code?: number, reason?: string): void
107
+ readonly readyState: number
108
+ serializeAttachment(value: unknown): void
109
+ deserializeAttachment(): unknown
110
+ }
111
+
112
+ /** Per-socket limits the platform sets; the hub stays well under both (see the tests). */
113
+ export const MAX_ATTACHMENT_BYTES = 512
114
+ export const MAX_SOCKET_RECORD_BYTES = 30_720
115
+ /** The most sockets one hub was verified to hold, with every publish reaching them all well under 200 ms. */
116
+ export const DEFAULT_HUB_SOCKETS = 5_000
117
+ /**
118
+ * About 440 bytes of heap each when no two sockets share a tag, the worst case; far less when they do.
119
+ * 150,000 of them stay near 66 MB, inside the 128 MB a Durable Object's isolate may use.
120
+ */
121
+ export const DEFAULT_HUB_MEMBERSHIPS = 150_000
122
+ /** Alarms come no closer together than this: expiry is checked again before every frame anyway. */
123
+ export const ALARM_GAP_MS = 30_000
124
+
125
+ const SEQ_KEY = "seq"
126
+ const APPLIED_KEY = "applied"
127
+ const FLOOR_KEY = "floor"
128
+ const REVOKED = "r:"
129
+ const SOCKET = "s:"
130
+ const DELETE_BATCH = 128
131
+ const MAX_CALL_BYTES = 65_536
132
+ const HEARTBEAT = "p"
133
+
134
+ interface Settings {
135
+ readonly secrets: LiveSecrets
136
+ readonly appId: string
137
+ readonly ttl: number
138
+ readonly trusted: ReadonlySet<string>
139
+ readonly maxSockets: number
140
+ readonly maxMemberships: number
141
+ readonly reloadRate: number
142
+ readonly spreadFloor: number
143
+ }
144
+
145
+ /**
146
+ * The live hub's Durable Object class. Export it from the Worker's main module under the class name
147
+ * the binding names.
148
+ */
149
+ export function liveHub<Env = Record<string, string | undefined>>(
150
+ options: LiveHubOptions | ((env: Env) => LiveHubOptions),
151
+ ): new (
152
+ state: LiveHubState,
153
+ env: Env,
154
+ ) => LiveHubObject {
155
+ return class extends LiveHubObject {
156
+ constructor(state: LiveHubState, env: Env) {
157
+ super(state, typeof options === "function" ? options(env) : options)
158
+ }
159
+ }
160
+ }
161
+
162
+ export class LiveHubObject implements HubStore {
163
+ private readonly ready: Promise<LiveCore>
164
+ private readonly settings: Settings
165
+ private readonly sockets = new WeakMap<LiveHubWebSocket, HubSocket>()
166
+ private readonly topics = new Map<string, Set<HubSocket>>()
167
+ private readonly hash = keyHasher()
168
+ private core: LiveCore | undefined
169
+ /** The last seq handed out, stored before its event is applied. */
170
+ private seq = 0
171
+ /** The last seq applied: a load's watermark. */
172
+ private applied = 0
173
+ /** The stored revocation floor. */
174
+ private floor = 0
175
+ /** Appends run one at a time, so history is recorded in seq order. */
176
+ private chain: Promise<unknown> = Promise.resolve()
177
+ private alarmAt: number | undefined
178
+ private mismatchLogged = false
179
+
180
+ constructor(
181
+ private readonly state: LiveHubState,
182
+ options: LiveHubOptions,
183
+ ) {
184
+ this.settings = settingsOf(options)
185
+ this.ready = state.blockConcurrencyWhile(() => this.wake())
186
+ }
187
+
188
+ async fetch(req: Request): Promise<Response> {
189
+ const core = await this.ready
190
+ if (req.method === "GET" && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
191
+ return this.accept(core, req)
192
+ }
193
+ const url = new URL(req.url)
194
+ const call = url.pathname
195
+ if (
196
+ req.method !== "POST" ||
197
+ url.origin !== HUB_ORIGIN ||
198
+ (call !== HUB_APPEND_PATH && call !== HUB_AUTHORITY_PATH)
199
+ ) {
200
+ return Response.json({ error: "not_found" }, { status: 404 })
201
+ }
202
+ if (!hubConfigMatches(core.ring, req.headers.get(HUB_CONFIG_HEADER))) {
203
+ if (!this.mismatchLogged) {
204
+ this.mismatchLogged = true
205
+ console.error(
206
+ "[nifra/web] live hub: an app called with a different `secret`, `appId` or `grantTtl` than this hub's; every call it makes is refused",
207
+ )
208
+ }
209
+ return Response.json({ error: "config_mismatch" }, { status: 409 })
210
+ }
211
+ const body = await bodyOf(req)
212
+ if (call === HUB_APPEND_PATH) {
213
+ const input = parseAppend(body)
214
+ if (input === undefined) return Response.json({ error: "bad_request" }, { status: 400 })
215
+ return Response.json({ seq: await this.append(core, input) })
216
+ }
217
+ const ask = parseAuthorityRequest(body)
218
+ if (ask === undefined) return Response.json({ error: "bad_request" }, { status: 400 })
219
+ return Response.json({
220
+ seq: this.applied,
221
+ ...core.revocations.snapshot(ask.subjects, ask.keys),
222
+ })
223
+ }
224
+
225
+ async webSocketMessage(raw: LiveHubWebSocket, message: string | ArrayBuffer): Promise<void> {
226
+ const core = await this.ready
227
+ const socket = this.sockets.get(raw)
228
+ if (socket === undefined) return
229
+ const data: WebSocketData = typeof message === "string" ? message : new Uint8Array(message)
230
+ const installing = core.hub.message(socket, data)
231
+ saveAttachment(socket)
232
+ if (installing === undefined) return
233
+ await installing
234
+ // The watch moved its expiry and whether it holds a tag.
235
+ saveAttachment(socket)
236
+ await this.schedule(
237
+ Math.min(
238
+ socket.data.exp,
239
+ (socket.data.unwatchedSince ?? Number.POSITIVE_INFINITY) + FIRST_WATCH_MS,
240
+ ),
241
+ )
242
+ }
243
+
244
+ async webSocketClose(raw: LiveHubWebSocket): Promise<void> {
245
+ await this.left(raw)
246
+ // Complete the closing handshake; a runtime that already replied throws here.
247
+ closeQuietly(raw, 1000, "")
248
+ }
249
+
250
+ async webSocketError(raw: LiveHubWebSocket): Promise<void> {
251
+ await this.left(raw)
252
+ }
253
+
254
+ async alarm(): Promise<void> {
255
+ const core = await this.ready
256
+ this.alarmAt = undefined
257
+ const sockets = core.hub.sweep()
258
+ const retention = await this.pruneRevocations(Date.now())
259
+ await this.schedule(Math.min(sockets, retention))
260
+ }
261
+
262
+ // --- HubStore: what the shared hub tells this host to keep --------------------------------------
263
+
264
+ saved(ws: NifraWebSocket<LiveSocket>, socket: StoredSocket): void {
265
+ void this.state.storage.put({ [SOCKET + ws.data.sid]: socket }).catch(storageFailed)
266
+ }
267
+
268
+ removed(ws: NifraWebSocket<LiveSocket>): void {
269
+ void this.state.storage.delete([SOCKET + ws.data.sid]).catch(storageFailed)
270
+ }
271
+
272
+ lastBeat(ws: NifraWebSocket<LiveSocket>): number {
273
+ if (!(ws instanceof HubSocket)) return 0
274
+ return this.state.getWebSocketAutoResponseTimestamp(ws.raw)?.getTime() ?? 0
275
+ }
276
+
277
+ // --- internals ----------------------------------------------------------------------------------
278
+
279
+ /** Everything the hub knows comes back from storage and the runtime's sockets; history does not. */
280
+ private async wake(): Promise<LiveCore> {
281
+ const { state, settings } = this
282
+ const storage = state.storage
283
+ const ring = await createKeyring({
284
+ secrets: settings.secrets,
285
+ appId: settings.appId,
286
+ ttl: settings.ttl,
287
+ })
288
+ const head = await storage.get([SEQ_KEY, APPLIED_KEY, FLOOR_KEY])
289
+ const seq = head.get(SEQ_KEY)
290
+ if (seqLike(seq)) {
291
+ this.seq = seq
292
+ const floor = head.get(FLOOR_KEY)
293
+ this.floor = seqLike(floor) ? floor : seq
294
+ } else {
295
+ // A new hub, or one whose storage is gone: no grant issued before now is honored.
296
+ this.seq = nextSeq(0, Date.now())
297
+ this.floor = this.seq
298
+ await storage.put({ [SEQ_KEY]: this.seq, [APPLIED_KEY]: this.seq, [FLOOR_KEY]: this.floor })
299
+ }
300
+ // An event stored but never applied may never have reached the sockets that watch it.
301
+ const lost = head.get(APPLIED_KEY) !== this.seq && seqLike(seq)
302
+ this.applied = this.seq
303
+
304
+ const revocations = new Revocations(this.floor, settings.ttl * 1000)
305
+ for (const [name, value] of await storage.list({ prefix: REVOKED })) {
306
+ const entry = revocationOf(name, value)
307
+ if (entry !== undefined && entry.seq >= this.floor) {
308
+ revocations.apply(
309
+ { type: "revoke", sub: entry.sub, key: entry.key, seq: entry.seq },
310
+ entry.at,
311
+ )
312
+ }
313
+ }
314
+ const history = new History(this.seq)
315
+ const hub = new LiveHub(
316
+ ring,
317
+ history,
318
+ revocations,
319
+ {
320
+ appId: settings.appId,
321
+ trusted: settings.trusted,
322
+ maxSockets: settings.maxSockets,
323
+ maxMemberships: settings.maxMemberships,
324
+ maxRssBytes: undefined,
325
+ reloadRate: settings.reloadRate,
326
+ instances: 1,
327
+ spreadFloor: settings.spreadFloor,
328
+ hash: this.hash,
329
+ store: this,
330
+ },
331
+ new Set([(topic: string, frame: string) => this.deliver(topic, frame)]),
332
+ )
333
+ const core: LiveCore = { ring, history, revocations, hub }
334
+ this.core = core
335
+
336
+ const records = await storage.list({ prefix: SOCKET })
337
+ const kept = new Set<string>()
338
+ let resynced = 0
339
+ // Each record is its own copy of every tag string: one copy per tag serves every socket.
340
+ const interned = new Map<string, string>()
341
+ for (const raw of state.getWebSockets()) {
342
+ const data = socketStateOf(raw.deserializeAttachment())
343
+ const stored =
344
+ data === undefined ? undefined : storedOf(records.get(SOCKET + data.sid), interned)
345
+ // A socket that watched but lost its membership would miss every change: it reconnects.
346
+ if (lost || data === undefined || (data.received >= 0 && stored === undefined)) {
347
+ closeQuietly(raw, CLOSE_CAPACITY, "resync")
348
+ resynced++
349
+ continue
350
+ }
351
+ const socket = new HubSocket(raw, data, this.topics)
352
+ this.sockets.set(raw, socket)
353
+ hub.restore(socket, stored)
354
+ kept.add(SOCKET + data.sid)
355
+ }
356
+ const orphans = [...records.keys()].filter((name) => !kept.has(name))
357
+ for (let at = 0; at < orphans.length; at += DELETE_BATCH) {
358
+ await storage.delete(orphans.slice(at, at + DELETE_BATCH))
359
+ }
360
+ if (lost) await storage.put({ [APPLIED_KEY]: this.seq })
361
+ hub.burst(resynced)
362
+
363
+ const Pair: unknown = Reflect.get(globalThis, "WebSocketRequestResponsePair")
364
+ if (typeof Pair === "function") {
365
+ state.setWebSocketAutoResponse(Reflect.construct(Pair, [HEARTBEAT, HEARTBEAT]))
366
+ }
367
+ this.alarmAt = (await storage.getAlarm()) ?? undefined
368
+ return core
369
+ }
370
+
371
+ private async accept(core: LiveCore, req: Request): Promise<Response> {
372
+ const data = core.hub.upgrade(req)
373
+ if (data instanceof Response) return data
374
+ const pair = socketPair()
375
+ if (pair === undefined) return Response.json({ error: "unavailable" }, { status: 503 })
376
+ const [client, server] = pair
377
+ this.state.acceptWebSocket(server)
378
+ const socket = new HubSocket(server, data, this.topics)
379
+ this.sockets.set(server, socket)
380
+ saveAttachment(socket)
381
+ core.hub.open(socket)
382
+ await this.schedule(Date.now() + FIRST_WATCH_MS)
383
+ const init = { status: 101, webSocket: client }
384
+ return new Response(null, init)
385
+ }
386
+
387
+ private append(core: LiveCore, input: LiveBusInput): Promise<number> {
388
+ const run = this.chain.then(() => this.applyNext(core, input))
389
+ this.chain = run.catch(() => {})
390
+ return run
391
+ }
392
+
393
+ private async applyNext(core: LiveCore, input: LiveBusInput): Promise<number> {
394
+ const storage = this.state.storage
395
+ const now = Date.now()
396
+ const seq = nextSeq(this.seq, now)
397
+ this.seq = seq
398
+ const kept = input.type === "revoke" && (input.sub !== null || input.key !== null)
399
+ const writes: Record<string, unknown> = { [SEQ_KEY]: seq }
400
+ if (kept) writes[revokedKey(seq)] = { sub: input.sub, key: input.key, at: now }
401
+ // Stored before any socket hears of it: a revocation is never applied and then forgotten.
402
+ await storage.put(writes)
403
+ try {
404
+ await applyEvent(core, { ...input, seq }, this.hash)
405
+ } catch (err) {
406
+ console.error(
407
+ "[nifra/web] live hub: an event could not be applied; closing live sockets",
408
+ err,
409
+ )
410
+ core.history.reset(seq)
411
+ core.revocations.raise(seq)
412
+ core.hub.closeAll(CLOSE_CAPACITY, "resync")
413
+ }
414
+ this.applied = seq
415
+ const after: Record<string, unknown> = { [APPLIED_KEY]: seq }
416
+ const raised = core.revocations.floor > this.floor
417
+ if (raised) {
418
+ this.floor = core.revocations.floor
419
+ after[FLOOR_KEY] = this.floor
420
+ }
421
+ await storage.put(after)
422
+ if (kept) await this.schedule(now + this.settings.ttl * 1000 + REVOCATION_SKEW_MS)
423
+ else if (raised) await this.schedule(now)
424
+ return seq
425
+ }
426
+
427
+ /** One frame to every socket on the topic whose grants are still valid. */
428
+ private deliver(topic: string, frame: string): void {
429
+ const members = this.topics.get(topic)
430
+ const hub = this.core?.hub
431
+ if (members === undefined || hub === undefined) return
432
+ const now = Date.now()
433
+ for (const socket of members) if (!hub.expired(socket, now)) socket.send(frame)
434
+ }
435
+
436
+ private async left(raw: LiveHubWebSocket): Promise<void> {
437
+ const core = await this.ready
438
+ const socket = this.sockets.get(raw)
439
+ if (socket === undefined) return
440
+ this.sockets.delete(raw)
441
+ // The hub leaves a closed socket's topics without unsubscribing it; this registry is the host's.
442
+ for (const tag of socket.data.tags) socket.unsubscribe(topicOf(this.settings.appId, tag))
443
+ core.hub.close(socket)
444
+ }
445
+
446
+ /** Drop revocations every grant they could refuse has outlived. Returns the next one's deadline. */
447
+ private async pruneRevocations(now: number): Promise<number> {
448
+ const storage = this.state.storage
449
+ const ttlMs = this.settings.ttl * 1000
450
+ const horizon = (now - ttlMs - REVOCATION_SKEW_MS) * 1000
451
+ const end = revokedKey(Math.max(this.floor, Math.floor(horizon)))
452
+ for (;;) {
453
+ const stale = [...(await storage.list({ prefix: REVOKED, end, limit: DELETE_BATCH })).keys()]
454
+ if (stale.length === 0) break
455
+ await storage.delete(stale)
456
+ }
457
+ for (const [name, value] of await storage.list({ prefix: REVOKED, limit: 1 })) {
458
+ const entry = revocationOf(name, value)
459
+ if (entry !== undefined) return entry.at + ttlMs + REVOCATION_SKEW_MS
460
+ }
461
+ return Number.POSITIVE_INFINITY
462
+ }
463
+
464
+ private async schedule(deadline: number): Promise<void> {
465
+ if (!Number.isFinite(deadline)) return
466
+ const at = Math.max(deadline, Date.now() + ALARM_GAP_MS)
467
+ if (this.alarmAt !== undefined && this.alarmAt <= at) return
468
+ this.alarmAt = at
469
+ await this.state.storage.setAlarm(at)
470
+ }
471
+ }
472
+
473
+ /** A hibernatable socket under the hub's own topic registry. */
474
+ class HubSocket implements NifraWebSocket<LiveSocket> {
475
+ constructor(
476
+ readonly raw: LiveHubWebSocket,
477
+ public data: LiveSocket,
478
+ private readonly topics: Map<string, Set<HubSocket>>,
479
+ ) {}
480
+
481
+ get readyState(): number {
482
+ return this.raw.readyState
483
+ }
484
+
485
+ send(data: string | ArrayBufferView | ArrayBuffer): void {
486
+ try {
487
+ this.raw.send(data)
488
+ } catch {
489
+ // Closed meanwhile: its close event settles the rest.
490
+ }
491
+ }
492
+
493
+ close(code?: number, reason?: string): void {
494
+ closeQuietly(this.raw, code, reason)
495
+ }
496
+
497
+ subscribe(topic: string): void {
498
+ let members = this.topics.get(topic)
499
+ if (members === undefined) {
500
+ members = new Set()
501
+ this.topics.set(topic, members)
502
+ }
503
+ members.add(this)
504
+ }
505
+
506
+ unsubscribe(topic: string): void {
507
+ const members = this.topics.get(topic)
508
+ if (members === undefined || !members.delete(this)) return
509
+ if (members.size === 0) this.topics.delete(topic)
510
+ }
511
+ }
512
+
513
+ function settingsOf(options: LiveHubOptions): Settings {
514
+ const secret = options.secret
515
+ const secrets: readonly unknown[] = typeof secret === "string" ? [secret] : (secret ?? [])
516
+ assertLiveSecrets(secrets)
517
+ return {
518
+ secrets,
519
+ appId: appIdOf(options.appId),
520
+ ttl: grantTtlOf(options.grantTtl),
521
+ trusted: new Set(options.trustedOrigins ?? []),
522
+ maxSockets: positive(options.maxSockets, DEFAULT_HUB_SOCKETS, "maxSockets"),
523
+ maxMemberships: positive(options.maxMemberships, DEFAULT_HUB_MEMBERSHIPS, "maxMemberships"),
524
+ reloadRate: positive(options.reloadRate, DEFAULT_RELOAD_RATE, "reloadRate"),
525
+ spreadFloor: spreadFloorOf(options.spreadFloor),
526
+ }
527
+ }
528
+
529
+ /**
530
+ * Sid, gen, rate counters and since when it held no tag (-1 while it holds one): what a socket needs
531
+ * back after a wake that its record does not hold.
532
+ */
533
+ function saveAttachment(socket: HubSocket): void {
534
+ const s = socket.data
535
+ try {
536
+ socket.raw.serializeAttachment([
537
+ s.sid,
538
+ s.received,
539
+ s.window,
540
+ s.frames,
541
+ s.watches,
542
+ s.lastSeen,
543
+ s.unwatchedSince ?? -1,
544
+ ])
545
+ } catch {
546
+ // Closed meanwhile: nothing comes back for it.
547
+ }
548
+ }
549
+
550
+ function socketStateOf(value: unknown): LiveSocket | undefined {
551
+ if (!Array.isArray(value) || value.length !== 7) return undefined
552
+ const [sid, received, window, frames, watches, lastSeen, unwatchedSince] = value
553
+ if (typeof sid !== "string" || !SID_PATTERN.test(sid)) return undefined
554
+ if (!integer(received) || received < -1) return undefined
555
+ if (!integer(unwatchedSince) || unwatchedSince < -1) return undefined
556
+ if (!seqLike(window) || !seqLike(frames) || !seqLike(watches) || !seqLike(lastSeen)) {
557
+ return undefined
558
+ }
559
+ return {
560
+ sid,
561
+ sub: undefined,
562
+ received,
563
+ tags: new Set(),
564
+ exp: Number.POSITIVE_INFINITY,
565
+ unwatchedSince: unwatchedSince === -1 ? undefined : unwatchedSince,
566
+ window,
567
+ frames,
568
+ watches,
569
+ lastSeen,
570
+ }
571
+ }
572
+
573
+ function storedOf(value: unknown, interned: Map<string, string>): StoredSocket | undefined {
574
+ if (!isRecord(value)) return undefined
575
+ const { sub, exp, tags } = value
576
+ if (sub !== null && !subjectLike(sub)) return undefined
577
+ if (typeof exp !== "number" || Number.isNaN(exp) || !Array.isArray(tags)) return undefined
578
+ const list: string[] = []
579
+ for (const tag of tags) {
580
+ if (typeof tag !== "string") return undefined
581
+ const known = interned.get(tag)
582
+ if (known === undefined) interned.set(tag, tag)
583
+ list.push(known ?? tag)
584
+ }
585
+ return { sub, exp, tags: list }
586
+ }
587
+
588
+ interface StoredRevocation {
589
+ readonly seq: number
590
+ readonly sub: string | null
591
+ readonly key: string | null
592
+ readonly at: number
593
+ }
594
+
595
+ function revocationOf(name: string, value: unknown): StoredRevocation | undefined {
596
+ const seq = Number(name.slice(REVOKED.length))
597
+ if (!seqLike(seq) || !isRecord(value)) return undefined
598
+ const { sub, key, at } = value
599
+ if (sub !== null && !subjectLike(sub)) return undefined
600
+ if (key !== null && (typeof key !== "string" || !KEY_PATTERN.test(key))) return undefined
601
+ if (!seqLike(at)) return undefined
602
+ return { seq, sub, key, at }
603
+ }
604
+
605
+ /** Fixed width, so storage order is seq order. */
606
+ const revokedKey = (seq: number): string => REVOKED + String(seq).padStart(16, "0")
607
+
608
+ function socketPair(): [unknown, LiveHubWebSocket] | undefined {
609
+ const Pair: unknown = Reflect.get(globalThis, "WebSocketPair")
610
+ if (typeof Pair !== "function") return undefined
611
+ const pair: unknown = Reflect.construct(Pair, [])
612
+ if (!isRecord(pair)) return undefined
613
+ const server = pair[1]
614
+ return isHubWebSocket(server) ? [pair[0], server] : undefined
615
+ }
616
+
617
+ function isHubWebSocket(value: unknown): value is LiveHubWebSocket {
618
+ return (
619
+ isRecord(value) &&
620
+ typeof value.send === "function" &&
621
+ typeof value.close === "function" &&
622
+ typeof value.serializeAttachment === "function" &&
623
+ typeof value.deserializeAttachment === "function"
624
+ )
625
+ }
626
+
627
+ function closeQuietly(raw: LiveHubWebSocket, code?: number, reason?: string): void {
628
+ try {
629
+ raw.close(code, reason)
630
+ } catch {
631
+ // Already closed.
632
+ }
633
+ }
634
+
635
+ async function bodyOf(req: Request): Promise<unknown> {
636
+ const text = await readCapped(req, MAX_CALL_BYTES)
637
+ if (typeof text === "number") return undefined
638
+ try {
639
+ return JSON.parse(text)
640
+ } catch {
641
+ return undefined
642
+ }
643
+ }
644
+
645
+ function storageFailed(err: unknown): void {
646
+ console.error("[nifra/web] live hub: a storage write failed", err)
647
+ }
648
+
649
+ const integer = (value: unknown): value is number =>
650
+ typeof value === "number" && Number.isSafeInteger(value)
651
+
652
+ const seqLike = (value: unknown): value is number => integer(value) && value >= 0
653
+
654
+ const subjectLike = (value: unknown): value is string =>
655
+ typeof value === "string" && value.length <= MAX_SUBJECT_CHARS
656
+
657
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
658
+ typeof value === "object" && value !== null && !Array.isArray(value)