@nifrajs/web 5.5.0 → 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 +162 -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,379 @@
1
+ import { createRequire } from "node:module"
2
+ import { basename, join, resolve } from "node:path"
3
+ import type * as TS from "typescript"
4
+ import type { Manifest, RouteEntry } from "../../manifest.ts"
5
+ import { type BunMetafileLike, fromBunMetafile } from "../../module-graph.ts"
6
+ import { isTypeScriptCompiler } from "../typescript-api.ts"
7
+
8
+ /**
9
+ * Which routes passed the shell check for one deployment. The server shares a shell only for a route
10
+ * listed here under its own deployment; anything else renders blocking (fail closed).
11
+ */
12
+ export interface ShellCheckRecord {
13
+ readonly v: 1
14
+ readonly deployment: string
15
+ /** Route id to the sha256 of the checked shell bundle. */
16
+ readonly routes: Readonly<Record<string, string>>
17
+ /** Route id to the shared holes whose loaders were checked in that bundle: only these are stored. */
18
+ readonly sharedHoles?: Readonly<Record<string, readonly string[]>>
19
+ }
20
+
21
+ export class ShellCheckError extends Error {
22
+ override readonly name = "ShellCheckError"
23
+ constructor(
24
+ readonly routeId: string,
25
+ readonly chain: readonly string[],
26
+ ) {
27
+ super(
28
+ `[nifra/web] ppr: the shell of route "${routeId}" reaches request-scoped module "${chain.at(-1)}" through ${chain.join(" -> ")}. Move that read into a dynamic boundary's loader: a shared boundary's loader is part of the shell`,
29
+ )
30
+ }
31
+ }
32
+
33
+ export interface CompileShellsOptions {
34
+ readonly routesDir: string
35
+ readonly manifest: Manifest
36
+ /** The deployment the record is for: the server refuses a record made for another one. */
37
+ readonly deployment: string
38
+ /** True for a module (absolute path, `/` separators) whose code reads the visitor's request. */
39
+ readonly requestScoped: (moduleId: string) => boolean
40
+ /** Called for each route that fails; the route is left out of the record. Default: rethrow. */
41
+ readonly onFailure?: (routeId: string, error: unknown) => void
42
+ /** Bun plugins the server build uses for route files, such as `svelteBunPlugin("ssr")`. */
43
+ readonly plugins?: readonly import("bun").BunPlugin[]
44
+ }
45
+
46
+ const NAMESPACE = "nifra-ppr-shell"
47
+
48
+ const toPosix = (path: string): string => path.replaceAll("\\", "/")
49
+
50
+ const escapeRegExp = (text: string): string => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
51
+
52
+ /**
53
+ * nifra's own modules that read the visitor's request from async context. Packages stay external in
54
+ * the shell bundle, so these are bundled in instead: the output graph then shows whether shell code
55
+ * calls them, not merely whether a module beside it imports them.
56
+ */
57
+ const NIFRA_REQUEST_SCOPED: readonly string[] = ["@nifrajs/middleware/context-storage"]
58
+ const NIFRA_REQUEST_SCOPED_FILTER = new RegExp(
59
+ `^(?:${NIFRA_REQUEST_SCOPED.map(escapeRegExp).join("|")})$`,
60
+ )
61
+
62
+ /** Hands any module the Worker's `env`, its secrets included, with no request in sight. */
63
+ const PLATFORM_ENV = /^cloudflare:workers$/
64
+
65
+ /** The import chain from `root` to the first request-scoped module it reaches, breadth first. */
66
+ function requestScopedChain(
67
+ root: string,
68
+ next: ReadonlyMap<string, readonly string[]>,
69
+ scoped: ReadonlySet<string>,
70
+ ): string[] | undefined {
71
+ const parent = new Map<string, string | undefined>([[root, undefined]])
72
+ const queue = [root]
73
+ for (let cursor = 0; cursor < queue.length; cursor++) {
74
+ const module = queue[cursor]
75
+ if (module === undefined) break
76
+ if (scoped.has(module)) {
77
+ const chain: string[] = []
78
+ for (let at: string | undefined = module; at !== undefined; at = parent.get(at))
79
+ chain.push(at)
80
+ return chain.reverse()
81
+ }
82
+ for (const child of next.get(module) ?? []) {
83
+ if (!parent.has(child)) {
84
+ parent.set(child, module)
85
+ queue.push(child)
86
+ }
87
+ }
88
+ }
89
+ return undefined
90
+ }
91
+
92
+ /**
93
+ * Bundle each route's shell (its page and layout modules, its backend `shellLoader`, and nothing else)
94
+ * and refuse it when the emitted code reaches a request-scoped module. A module counts only
95
+ * when its code reached the output, so an import only a hole loader uses is not a leak. Routes whose
96
+ * backend half has no `shellLoader` are skipped. Bun only.
97
+ */
98
+ export async function compileShells(options: CompileShellsOptions): Promise<ShellCheckRecord> {
99
+ const dir = resolve(options.routesDir)
100
+ const routes: Record<string, string> = {}
101
+ const sharedHoles: Record<string, readonly string[]> = {}
102
+ const parser = typeScriptFor(dir)
103
+ for (const route of options.manifest.routes) {
104
+ if (route.backend === undefined) continue
105
+ try {
106
+ const checked = await compileShell(
107
+ dir,
108
+ options.manifest,
109
+ route,
110
+ route.backend,
111
+ options.requestScoped,
112
+ options.plugins ?? [],
113
+ parser,
114
+ )
115
+ if (checked === undefined) continue
116
+ routes[route.id] = checked.hash
117
+ if (checked.sharedHoles.length > 0) sharedHoles[route.id] = checked.sharedHoles
118
+ } catch (error) {
119
+ if (options.onFailure === undefined) throw error
120
+ options.onFailure(route.id, error)
121
+ }
122
+ }
123
+ return Object.keys(sharedHoles).length === 0
124
+ ? { v: 1, deployment: options.deployment, routes }
125
+ : { v: 1, deployment: options.deployment, routes, sharedHoles }
126
+ }
127
+
128
+ /** The app's TypeScript compiler, read only when a backend half has boundary loaders to sort. */
129
+ function typeScriptFor(dir: string): () => typeof TS | undefined {
130
+ let loaded: typeof TS | undefined
131
+ let tried = false
132
+ return () => {
133
+ if (tried) return loaded
134
+ tried = true
135
+ let ts: unknown
136
+ try {
137
+ ts = createRequire(join(dir, "package.json"))("typescript")
138
+ } catch {
139
+ ts = undefined
140
+ }
141
+ if (isTypeScriptCompiler(ts)) loaded = ts
142
+ else {
143
+ console.warn(
144
+ "[nifra/web] ppr: no shared hole's loader is checked, so each loads for every visitor: finding them reads boundaryLoaders with the TypeScript compiler API, which the app's TypeScript does not have (TypeScript 7 ships none; install typescript@^6 as a dev dependency)",
145
+ )
146
+ }
147
+ return loaded
148
+ }
149
+ }
150
+
151
+ /** `node` without the `satisfies`, `as` and parentheses around it. */
152
+ function unwrapped(ts: typeof TS, node: TS.Expression): TS.Expression {
153
+ let at = node
154
+ while (ts.isSatisfiesExpression(at) || ts.isAsExpression(at) || ts.isParenthesizedExpression(at))
155
+ at = at.expression
156
+ return at
157
+ }
158
+
159
+ /**
160
+ * The backend half with `boundaryLoaders` cut down to its shared entries, and their names. A shared
161
+ * entry is one written inline with `revalidate`, which only a shared boundary's entry may have; one the
162
+ * source does not spell out that way is left out, so its hole is not stored (fail closed). A per-visitor
163
+ * entry's loader may read the request, so it never joins the shell bundle.
164
+ */
165
+ function sharedLoaders(
166
+ ts: typeof TS,
167
+ module: SourceModule,
168
+ ): { readonly source: string; readonly names: readonly string[] } | undefined {
169
+ const file = ts.createSourceFile(
170
+ module.path,
171
+ module.source,
172
+ ts.ScriptTarget.Latest,
173
+ true,
174
+ module.loader === "tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
175
+ )
176
+ for (const statement of file.statements) {
177
+ if (!ts.isVariableStatement(statement)) continue
178
+ for (const declaration of statement.declarationList.declarations) {
179
+ if (!ts.isIdentifier(declaration.name) || declaration.name.text !== "boundaryLoaders")
180
+ continue
181
+ const initializer = declaration.initializer && unwrapped(ts, declaration.initializer)
182
+ if (initializer === undefined || !ts.isObjectLiteralExpression(initializer)) return undefined
183
+ const names: string[] = []
184
+ const kept: string[] = []
185
+ for (const property of initializer.properties) {
186
+ if (!ts.isPropertyAssignment(property)) continue
187
+ const { name } = property
188
+ if (!ts.isIdentifier(name) && !ts.isStringLiteral(name)) continue
189
+ const entry = unwrapped(ts, property.initializer)
190
+ if (!ts.isObjectLiteralExpression(entry)) continue
191
+ const shared = entry.properties.some(
192
+ (field) =>
193
+ field.name !== undefined &&
194
+ (ts.isIdentifier(field.name) || ts.isStringLiteral(field.name)) &&
195
+ field.name.text === "revalidate",
196
+ )
197
+ if (!shared) continue
198
+ names.push(name.text)
199
+ kept.push(module.source.slice(property.getStart(file), property.getEnd()))
200
+ }
201
+ if (names.length === 0) return undefined
202
+ const source = `${module.source.slice(0, initializer.getStart(file))}{ ${kept.join(", ")} }${module.source.slice(initializer.getEnd())}`
203
+ return { source, names }
204
+ }
205
+ }
206
+ return undefined
207
+ }
208
+
209
+ interface SourceModule {
210
+ readonly path: string
211
+ readonly loader: "ts" | "tsx"
212
+ readonly source: string
213
+ /** The names it exports, read from its source without running it. */
214
+ readonly exports: readonly string[]
215
+ }
216
+
217
+ async function readModule(dir: string, file: string): Promise<SourceModule> {
218
+ const path = toPosix(resolve(dir, file))
219
+ const loader = /\.[cm]?tsx$|\.jsx$/.test(path) ? "tsx" : "ts"
220
+ const source = await Bun.file(path).text()
221
+ return { path, loader, source, exports: new Bun.Transpiler({ loader }).scan(source).exports }
222
+ }
223
+
224
+ async function compileShell(
225
+ dir: string,
226
+ manifest: Manifest,
227
+ route: RouteEntry,
228
+ backend: string,
229
+ requestScoped: (moduleId: string) => boolean,
230
+ plugins: readonly import("bun").BunPlugin[],
231
+ parser: () => typeof TS | undefined,
232
+ ): Promise<{ readonly hash: string; readonly sharedHoles: readonly string[] } | undefined> {
233
+ const backendModule = await readModule(dir, backend)
234
+ if (!backendModule.exports.includes("shellLoader")) return undefined
235
+ const ts = backendModule.exports.includes("boundaryLoaders") ? parser() : undefined
236
+ const shared = ts === undefined ? undefined : sharedLoaders(ts, backendModule)
237
+ const kept = shared === undefined ? ["shellLoader"] : ["shellLoader", "boundaryLoaders"]
238
+ // A gate's loader runs for its redirect or refusal and its data never reaches the shell; any other
239
+ // layout loader reads the request for data the shell would show every visitor.
240
+ for (const id of route.layoutIds) {
241
+ const layoutBackend = manifest.layouts[id]?.backend
242
+ if (layoutBackend === undefined) continue
243
+ const names = (await readModule(dir, layoutBackend)).exports
244
+ if (names.includes("loader") && !names.includes("gate")) {
245
+ throw new Error(
246
+ `[nifra/web] ppr: route "${route.id}" has a shellLoader, but its layout "${id}" has a loader that is not a gate. Its output would be in a shell shared by every visitor; move that data into a dynamic boundary, or mark the layout \`gate\` if it loads only to redirect or refuse`,
247
+ )
248
+ }
249
+ }
250
+ const at = (file: string): string => JSON.stringify(toPosix(resolve(dir, file)))
251
+ // A page's and each layout's module whole: the shell runs `meta`, `handle` and `searchSchema` as
252
+ // well as the component, and an import only one of them uses must not escape the check.
253
+ const lines = [
254
+ `export { ${kept.join(", ")} } from ${at(backend)}`,
255
+ `export * as page from ${at(route.file)}`,
256
+ ]
257
+ route.layoutIds.forEach((id, index) => {
258
+ const layout = manifest.layouts[id]
259
+ if (layout !== undefined) lines.push(`export * as layout${index} from ${at(layout.file)}`)
260
+ })
261
+ const nifraScoped = new Set<string>()
262
+ const envImporters = new Set<string>()
263
+ const result = await Bun.build({
264
+ // Bun reads a `/` in an entry name as a directory, and route ids have them.
265
+ entrypoints: [`${NAMESPACE}:${encodeURIComponent(route.id)}`],
266
+ target: "bun",
267
+ packages: "external",
268
+ metafile: true,
269
+ throw: false,
270
+ plugins: [
271
+ // Ours first: its resolve and load hooks claim the entry and the backend half before theirs.
272
+ {
273
+ name: NAMESPACE,
274
+ setup(build) {
275
+ build.onResolve({ filter: new RegExp(`^${NAMESPACE}:`) }, (args) => ({
276
+ path: args.path,
277
+ namespace: NAMESPACE,
278
+ }))
279
+ build.onLoad({ filter: /.*/, namespace: NAMESPACE }, () => ({
280
+ loader: "ts",
281
+ contents: lines.join("\n"),
282
+ }))
283
+ build.onResolve({ filter: NIFRA_REQUEST_SCOPED_FILTER }, (args) => {
284
+ let path: string
285
+ try {
286
+ path = toPosix(Bun.resolveSync(args.path, args.resolveDir))
287
+ } catch {
288
+ // Not installed, so no shell code can call it.
289
+ return undefined
290
+ }
291
+ nifraScoped.add(path)
292
+ return { path }
293
+ })
294
+ build.onResolve({ filter: PLATFORM_ENV }, (args) => {
295
+ envImporters.add(toPosix(resolve(args.importer)))
296
+ return { path: args.path, external: true }
297
+ })
298
+ // The backend half's other exports are not shell code. A hole loader's `output: t.object()`
299
+ // is a call into a package, which the bundler must keep, and with it every import the
300
+ // hole uses. Eliminating them first scopes the check to `shellLoader` and the shared
301
+ // holes' loaders; their imports stay as bare imports, so a module with top-level effects
302
+ // still counts. The file is matched by its `/` form: Bun hands a plugin `\` on Windows.
303
+ build.onLoad(
304
+ { filter: new RegExp(`${escapeRegExp(basename(backendModule.path))}$`) },
305
+ (args) =>
306
+ toPosix(args.path) !== backendModule.path
307
+ ? undefined
308
+ : {
309
+ loader: "js",
310
+ contents: new Bun.Transpiler({
311
+ loader: backendModule.loader,
312
+ trimUnusedImports: true,
313
+ exports: {
314
+ eliminate: backendModule.exports.filter((name) => !kept.includes(name)),
315
+ },
316
+ }).transformSync(shared?.source ?? backendModule.source),
317
+ },
318
+ )
319
+ },
320
+ },
321
+ ...plugins,
322
+ ],
323
+ })
324
+ if (!result.success) {
325
+ throw new AggregateError(
326
+ result.logs,
327
+ `[nifra/web] ppr: the shell of "${route.id}" failed to build`,
328
+ )
329
+ }
330
+ // biome-ignore lint/plugin/noChainedTypeAssertions: Bun returns the metafile when `metafile: true`; @types/bun does not name it yet, and fromBunMetafile fails closed on a missing one.
331
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: Bun returns the metafile when `metafile: true`; @types/bun does not name it yet, and fromBunMetafile fails closed on a missing one.
332
+ const meta = (result as unknown as { readonly metafile?: BunMetafileLike }).metafile
333
+ const graph = fromBunMetafile(meta)
334
+ const entry = Object.values(meta?.outputs ?? {}).find((output) =>
335
+ output.entryPoint?.startsWith(`${NAMESPACE}:`),
336
+ )
337
+ if (entry === undefined)
338
+ throw new Error(`[nifra/web] ppr: no output for the shell of "${route.id}"`)
339
+ // Re-exports and the virtual entry have zero bytes too, so every edge stays and only the
340
+ // request-scoped test reads bytes.
341
+ const emitted = (id: string): boolean => {
342
+ const input = entry.inputs?.[id]
343
+ return (
344
+ typeof input === "object" &&
345
+ input !== null &&
346
+ "bytesInOutput" in input &&
347
+ typeof input.bytesInOutput === "number" &&
348
+ input.bytesInOutput > 0
349
+ )
350
+ }
351
+ const absolute = (id: string): string =>
352
+ id.startsWith(`${NAMESPACE}:`) ? id : toPosix(resolve(id))
353
+ const next = new Map<string, string[]>()
354
+ const scoped = new Set<string>()
355
+ let root: string | undefined
356
+ for (const [id, module] of Object.entries(graph.modules)) {
357
+ const from = absolute(id)
358
+ if (id.startsWith(`${NAMESPACE}:`)) root = from
359
+ if (emitted(id) && (nifraScoped.has(from) || envImporters.has(from) || requestScoped(from)))
360
+ scoped.add(from)
361
+ next.set(
362
+ from,
363
+ module.imports.flatMap((edge) => (edge.path === undefined ? [] : [absolute(edge.path)])),
364
+ )
365
+ }
366
+ if (root === undefined) throw new Error(`[nifra/web] ppr: no entry module for "${route.id}"`)
367
+ const chain = requestScopedChain(root, next, scoped)
368
+ if (chain !== undefined) throw new ShellCheckError(route.id, chain.slice(1))
369
+ const output = result.outputs.find((artifact) => artifact.kind === "entry-point")
370
+ if (output === undefined)
371
+ throw new Error(`[nifra/web] ppr: no bundle for the shell of "${route.id}"`)
372
+ const digest = new Uint8Array(
373
+ await crypto.subtle.digest("SHA-256", new TextEncoder().encode(await output.text())),
374
+ )
375
+ return {
376
+ hash: Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join(""),
377
+ sharedHoles: shared?.names ?? [],
378
+ }
379
+ }
@@ -0,0 +1,265 @@
1
+ /**
2
+ * What the page executor and `createWebApp` know of partial prerendering. Everything here is small and
3
+ * imports no runtime code: the server, its options and the hole HTML validator load only through
4
+ * `partialPrerendering` (`@nifrajs/web/ppr`), so an app without it carries none of them.
5
+ */
6
+ import type {
7
+ BoundaryRegistration,
8
+ BoundaryRequestCtx,
9
+ BoundaryStates,
10
+ ShellCtx,
11
+ } from "../../boundary.ts"
12
+ import type { CspPolicy } from "../../csp.ts"
13
+ import type { RevalidateTags } from "../../isr.ts"
14
+ import type { RouteEntry, RouteModule } from "../../manifest.ts"
15
+ import type { RenderAdapter } from "../../render-seam.ts"
16
+ import type { LoadedLayoutModules } from "../render-document.ts"
17
+ import type { PprAdmissionStats } from "./admission.ts"
18
+ import type { ResolvedPprOptions } from "./options.ts"
19
+
20
+ /** An app built with `experimental.ppr` carries its {@link PprServer} under this key. */
21
+ export const PPR_SERVER: unique symbol = Symbol.for("nifra.web.ppr")
22
+
23
+ /** The key of a {@link PprSetup}'s factory. */
24
+ export const PPR_SETUP: unique symbol = Symbol.for("nifra.web.ppr.setup")
25
+
26
+ /** Where the runtime's cancel beacon goes, under the app's base path. */
27
+ export const PPR_CANCEL_PATH = "/__nifra/ppr/cancel"
28
+
29
+ interface HoleFields {
30
+ readonly interactive?: boolean
31
+ readonly timeout?: number
32
+ }
33
+
34
+ /** A hole filled per visitor. */
35
+ export type DynamicHole = Extract<BoundaryRegistration, { readonly mode: "dynamic" }> & HoleFields
36
+
37
+ /** A hole built once for every visitor, with its loader entry's freshness merged in. */
38
+ export type SharedHole = Extract<BoundaryRegistration, { readonly mode: "shared" }> &
39
+ HoleFields & {
40
+ readonly revalidate: number
41
+ readonly revalidateTags?: RevalidateTags | undefined
42
+ }
43
+
44
+ /** The boundary fields partial prerendering reads beyond the neutral registration. */
45
+ export type HoleRegistration = DynamicHole | SharedHole
46
+
47
+ export const isHole = (boundary: BoundaryRegistration): boundary is HoleRegistration =>
48
+ boundary.mode === "dynamic" || boundary.mode === "shared"
49
+
50
+ /** The key `serverHole()` stores a hole's server component under, on the hole's `render`. */
51
+ export const SERVER_HOLE: unique symbol = Symbol.for("nifra.web.server-hole")
52
+
53
+ /** The server component a hole's `render` names, or `undefined` for an ordinary `render`. */
54
+ export const serverHoleComponent = (render: unknown): object | undefined => {
55
+ if (typeof render !== "function") return undefined
56
+ const component: unknown = Reflect.get(render, SERVER_HOLE)
57
+ return typeof component === "object" && component !== null ? component : undefined
58
+ }
59
+
60
+ /** What the page executor hands over for one PPR request. */
61
+ export interface PprRequest {
62
+ readonly routeId: string
63
+ /** The request's pathname, without search: the shell renders it, so it is part of the key. */
64
+ readonly path: string
65
+ readonly locale: string | undefined
66
+ /** The declared variations this request selected, by name; part of the shell key. */
67
+ readonly variations: Readonly<Record<string, string>>
68
+ readonly request: Request
69
+ /** The route's dynamic and shared boundaries, in declaration order, with their merged (guarded)
70
+ * loaders. */
71
+ readonly holes: readonly HoleRegistration[]
72
+ readonly holeContext: Omit<BoundaryRequestCtx, "signal" | "live">
73
+ /** What the shell loader and every shared hole loader get, but the build's signal. */
74
+ readonly shellContext: Omit<ShellCtx, "signal">
75
+ readonly loadShell: (signal: AbortSignal) => Promise<unknown>
76
+ /** Render the full document for `shellData` with every hole pending, under no nonce. */
77
+ readonly renderShellDocument: (shellData: unknown, states: BoundaryStates) => Promise<string>
78
+ /** The route's `revalidate`, in seconds: an older shell is served while one rebuild runs. */
79
+ readonly revalidate: number | undefined
80
+ /** The route's `revalidateTags` for this request, read only when a shell is built. */
81
+ readonly tags: () => readonly string[]
82
+ /** A shared hole's `revalidateTags` for this request, read only when its entry is built. */
83
+ readonly holeTags: (hole: SharedHole) => readonly string[]
84
+ /** Keeps the platform alive for work that outlives the response, where the platform needs it. */
85
+ readonly waitUntil?: ((promise: Promise<unknown>) => void) | undefined
86
+ /** The holes the client holds as hosts: a data request sends each one's fill. A hole that shows a
87
+ * server component always gets its fill. */
88
+ readonly fill?: ReadonlySet<string>
89
+ /** Render a hole's server component for this visitor, when the app's server components list it. */
90
+ readonly component?: (
91
+ hole: string,
92
+ component: object,
93
+ input: unknown,
94
+ signal: AbortSignal,
95
+ ) => Promise<{ readonly html: string }>
96
+ /** Holes the client keeps: none of them loads, and each state says it was kept. */
97
+ readonly kept?: ReadonlySet<string>
98
+ /** A live load: each hole's keys, and which holes loaded. */
99
+ readonly live?: HoleLive
100
+ }
101
+
102
+ /** A live load's view of one response's holes. Owners are named by hole. */
103
+ export interface HoleLive {
104
+ /** `ctx.live` for one hole's loader. */
105
+ collector(hole: string): (...keys: string[]) => void
106
+ /** Close one hole's collector once its loader settled. */
107
+ seal(hole: string): void
108
+ /** Called once, after every hole that ran settled, with the holes that loaded. */
109
+ done(loaded: readonly string[]): void
110
+ }
111
+
112
+ /** The shell cannot be served now; the caller renders the page whole for this request. */
113
+ export class PprShellUnavailable extends Error {
114
+ constructor(message: string) {
115
+ super(message)
116
+ this.name = "PprShellUnavailable"
117
+ }
118
+ }
119
+
120
+ /** Thrown when a route's shell has no passing import-check record. */
121
+ export class PprShellUnchecked extends PprShellUnavailable {
122
+ constructor(routeId: string) {
123
+ super(
124
+ `[nifra/web] ppr: route "${routeId}" has no passing shell import check; rendered blocking`,
125
+ )
126
+ }
127
+ }
128
+
129
+ /** Thrown when a route already runs `shellBuildConcurrency` shell builds: no request waits on one. */
130
+ export class PprShellBusy extends PprShellUnavailable {
131
+ constructor(routeId: string) {
132
+ super(`[nifra/web] ppr: route "${routeId}" is at its shell build limit; rendered blocking`)
133
+ }
134
+ }
135
+
136
+ /** Thrown for a key seen fewer than `shellAfterRequests` times in a full store: the page streams whole. */
137
+ export class PprShellCold extends PprShellUnavailable {
138
+ constructor(routeId: string) {
139
+ super(`[nifra/web] ppr: route "${routeId}" has no shell for this key yet; rendered whole`)
140
+ }
141
+ }
142
+
143
+ /**
144
+ * How a response that is not a streamed document gets its holes. `"deferred"`: each hole runs under
145
+ * admission and its own deadline, and its state carries the result as a deferred value (a soft
146
+ * navigation's data request). `"settled"`: the same, awaited (the blocking render). `"omitted"`: no
147
+ * hole runs and every state is an empty host (a prefetch, which must not start work its navigation
148
+ * cannot cancel).
149
+ */
150
+ export type HoleStatesMode = "deferred" | "settled" | "omitted"
151
+
152
+ export interface PprServerStats extends PprAdmissionStats {
153
+ /** Responses aborted because unsent bytes went over `maxUnsentBytes` (`ppr.slow_aborted`). */
154
+ readonly slowAborted: number
155
+ /** Frame bytes queued for clients across every live PPR stream. */
156
+ readonly unsentBytes: number
157
+ }
158
+
159
+ export interface PprServer {
160
+ /** Stream one PPR document: the shared shell at once, then each hole as it settles. */
161
+ respond(request: PprRequest): Promise<Response>
162
+ /** The shell data a soft navigation's data request carries. */
163
+ shellData(request: PprRequest): Promise<unknown>
164
+ /** The hole states of a data request or a blocking render; `signal` is the request's. */
165
+ holeStates(
166
+ request: PprRequest,
167
+ mode: HoleStatesMode,
168
+ signal: AbortSignal,
169
+ ): Promise<BoundaryStates>
170
+ /** Abort the response that printed `token`. Unknown tokens do nothing. */
171
+ cancel(token: string): boolean
172
+ /**
173
+ * Drop every shell and shared hole frame carrying `tag`. A build of either that started before the
174
+ * call is not stored when it finishes, so an entry built from data read before the purge never
175
+ * outlives it.
176
+ */
177
+ invalidateTag(tag: string): Promise<void>
178
+ stats(): PprServerStats
179
+ }
180
+
181
+ /** What `createWebApp` gives a {@link PprSetup}: the parts of the app a PPR server depends on. */
182
+ export interface PprEnvironment {
183
+ readonly adapter: RenderAdapter
184
+ readonly csp?: CspPolicy
185
+ /** Part of every shell key, so a shell built by one deployment is never served by another. */
186
+ readonly deployment: string
187
+ /** The app authorizes scripts with a per-request nonce, which would make every shell unique. */
188
+ readonly perRequestNonce: boolean
189
+ }
190
+
191
+ /** One app's partial prerendering, as the page executor uses it. */
192
+ export interface PprIntegration {
193
+ readonly server: PprServer
194
+ /** The configured public origin a shell may use; never a visitor's Host. */
195
+ readonly origin?: string
196
+ /** Where the runtime's cancel beacon posts, and its handler. */
197
+ readonly cancelPath: string
198
+ cancel(request: Request): Promise<Response>
199
+ /**
200
+ * The streamed document, with the headers middleware and gates wrote, or how this request renders
201
+ * whole instead: `"settled"` for a listed bot or a shell that is unchecked or at its build limit,
202
+ * `"deferred"`, each hole streamed as it settles, for a key with no shell yet.
203
+ */
204
+ stream(
205
+ request: PprRequest,
206
+ written: () => Readonly<Record<string, string>> | undefined,
207
+ ): Promise<Response | "settled" | "deferred">
208
+ /** The shell data, or the shell loader's own output when the shell cannot be served now. */
209
+ shellData(request: PprRequest, signal: AbortSignal): Promise<unknown>
210
+ /** `variations` as the shell's own query string. */
211
+ variationQuery(variations: Readonly<Record<string, string>>): string
212
+ /** Throws for a route the PPR rules keep off the PPR path: its layouts, holes and their limits. */
213
+ assertRoute(
214
+ route: RouteEntry,
215
+ layouts: LoadedLayoutModules,
216
+ boundaries: readonly BoundaryRegistration[],
217
+ ): void
218
+ /** The declared variations `search` selects. */
219
+ variationsOf(
220
+ declared: RouteModule["shellVariations"],
221
+ search: string,
222
+ ): Readonly<Record<string, string>>
223
+ }
224
+
225
+ /** What `partialPrerendering()` returns for `createWebApp({ experimental: { ppr } })`. */
226
+ export interface PprSetup {
227
+ readonly [PPR_SETUP]: (environment: PprEnvironment) => PprIntegration
228
+ }
229
+
230
+ /** `variations` as a query string, sorted by name: the shell key part and the shell's own search. */
231
+ export const variationQuery = (variations: Readonly<Record<string, string>>): string => {
232
+ let query = ""
233
+ for (const name of Object.keys(variations).sort()) {
234
+ query += `${query === "" ? "" : "&"}${encodeURIComponent(name)}=${encodeURIComponent(variations[name] ?? "")}`
235
+ }
236
+ return query
237
+ }
238
+
239
+ // A browser's agent took the default rule about 0.5 us to test, and agents repeat. The bounds keep a
240
+ // client that sends a new agent per request from growing the map.
241
+ const MAX_REMEMBERED_AGENT = 256
242
+ const MAX_REMEMBERED_AGENTS = 1024
243
+ const agentAnswers = new WeakMap<RegExp, Map<string, boolean>>()
244
+
245
+ /** Whether `request` gets the blocking render under `options`. */
246
+ export function wantsBlockingRender(options: ResolvedPprOptions, request: Request): boolean {
247
+ const rule = options.blockingFor
248
+ if (typeof rule === "function") return rule(request)
249
+ const agent = request.headers.get("user-agent")
250
+ if (agent === null) return false
251
+ if (agent.length > MAX_REMEMBERED_AGENT) return rule.test(agent)
252
+ let answers = agentAnswers.get(rule)
253
+ if (answers === undefined) {
254
+ answers = new Map()
255
+ agentAnswers.set(rule, answers)
256
+ }
257
+ let answer = answers.get(agent)
258
+ if (answer === undefined) {
259
+ if (answers.size >= MAX_REMEMBERED_AGENTS) answers.clear()
260
+ // No `g` or `y` flag (options refuse them), so a test reads no state and an answer holds.
261
+ answer = rule.test(agent)
262
+ answers.set(agent, answer)
263
+ }
264
+ return answer
265
+ }