@nifrajs/web 5.5.1 → 5.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (457) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/dist/boundary.d.ts +67 -7
  3. package/dist/boundary.d.ts.map +1 -1
  4. package/dist/boundary.js +6 -4
  5. package/dist/boundary.js.map +1 -1
  6. package/dist/build-plan.d.ts +3 -0
  7. package/dist/build-plan.d.ts.map +1 -1
  8. package/dist/build-plan.js.map +1 -1
  9. package/dist/build-vite.d.ts.map +1 -1
  10. package/dist/build-vite.js +2 -0
  11. package/dist/build-vite.js.map +1 -1
  12. package/dist/build.d.ts +9 -2
  13. package/dist/build.d.ts.map +1 -1
  14. package/dist/build.js +9 -0
  15. package/dist/build.js.map +1 -1
  16. package/dist/client.d.ts +2 -1
  17. package/dist/client.d.ts.map +1 -1
  18. package/dist/client.js +140 -12
  19. package/dist/client.js.map +1 -1
  20. package/dist/csp.d.ts +8 -0
  21. package/dist/csp.d.ts.map +1 -1
  22. package/dist/csp.js +11 -2
  23. package/dist/csp.js.map +1 -1
  24. package/dist/deferred.d.ts +6 -4
  25. package/dist/deferred.d.ts.map +1 -1
  26. package/dist/deferred.js +10 -6
  27. package/dist/deferred.js.map +1 -1
  28. package/dist/dev-feed.d.ts.map +1 -1
  29. package/dist/dev-feed.js +11 -2
  30. package/dist/dev-feed.js.map +1 -1
  31. package/dist/dev-session.d.ts.map +1 -1
  32. package/dist/dev-session.js +13 -6
  33. package/dist/dev-session.js.map +1 -1
  34. package/dist/dev.d.ts +4 -1
  35. package/dist/dev.d.ts.map +1 -1
  36. package/dist/dev.js +12 -5
  37. package/dist/dev.js.map +1 -1
  38. package/dist/diagnostic-catalog.d.ts +4 -0
  39. package/dist/diagnostic-catalog.d.ts.map +1 -1
  40. package/dist/diagnostic-catalog.js +48 -0
  41. package/dist/diagnostic-catalog.js.map +1 -1
  42. package/dist/diagnostic.d.ts +1 -1
  43. package/dist/diagnostic.d.ts.map +1 -1
  44. package/dist/diagnostic.js +1 -1
  45. package/dist/diagnostic.js.map +1 -1
  46. package/dist/fn.d.ts +5 -3
  47. package/dist/fn.d.ts.map +1 -1
  48. package/dist/fn.js +20 -5
  49. package/dist/fn.js.map +1 -1
  50. package/dist/fs.d.ts.map +1 -1
  51. package/dist/fs.js +13 -3
  52. package/dist/fs.js.map +1 -1
  53. package/dist/identity.d.ts +22 -0
  54. package/dist/identity.d.ts.map +1 -0
  55. package/dist/identity.js +60 -0
  56. package/dist/identity.js.map +1 -0
  57. package/dist/index.d.ts +7 -6
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +4 -2
  60. package/dist/index.js.map +1 -1
  61. package/dist/internal/cdn/cloudflare.d.ts +2 -1
  62. package/dist/internal/cdn/cloudflare.d.ts.map +1 -1
  63. package/dist/internal/cdn/cloudflare.js +2 -1
  64. package/dist/internal/cdn/cloudflare.js.map +1 -1
  65. package/dist/internal/certified-components.d.ts +1 -1
  66. package/dist/internal/certified-components.js +1 -1
  67. package/dist/internal/codegen.d.ts +11 -10
  68. package/dist/internal/codegen.d.ts.map +1 -1
  69. package/dist/internal/codegen.js +65 -8
  70. package/dist/internal/codegen.js.map +1 -1
  71. package/dist/internal/dev-websocket.d.ts +73 -0
  72. package/dist/internal/dev-websocket.d.ts.map +1 -0
  73. package/dist/internal/dev-websocket.js +145 -0
  74. package/dist/internal/dev-websocket.js.map +1 -0
  75. package/dist/internal/hole-runtime.d.ts +66 -0
  76. package/dist/internal/hole-runtime.d.ts.map +1 -0
  77. package/dist/internal/hole-runtime.js +211 -0
  78. package/dist/internal/hole-runtime.js.map +1 -0
  79. package/dist/internal/identity-epoch.d.ts +50 -0
  80. package/dist/internal/identity-epoch.d.ts.map +1 -0
  81. package/dist/internal/identity-epoch.js +179 -0
  82. package/dist/internal/identity-epoch.js.map +1 -0
  83. package/dist/internal/invalidation-contract.d.ts +23 -0
  84. package/dist/internal/invalidation-contract.d.ts.map +1 -0
  85. package/dist/internal/invalidation-contract.js +11 -0
  86. package/dist/internal/invalidation-contract.js.map +1 -0
  87. package/dist/internal/invalidation.d.ts +62 -0
  88. package/dist/internal/invalidation.d.ts.map +1 -0
  89. package/dist/internal/invalidation.js +149 -0
  90. package/dist/internal/invalidation.js.map +1 -0
  91. package/dist/internal/island-bindings.d.ts +51 -0
  92. package/dist/internal/island-bindings.d.ts.map +1 -0
  93. package/dist/internal/island-bindings.js +147 -0
  94. package/dist/internal/island-bindings.js.map +1 -0
  95. package/dist/internal/island-list.d.ts +26 -0
  96. package/dist/internal/island-list.d.ts.map +1 -0
  97. package/dist/internal/island-list.js +78 -0
  98. package/dist/internal/island-list.js.map +1 -0
  99. package/dist/internal/island-mount.d.ts +4 -0
  100. package/dist/internal/island-mount.d.ts.map +1 -0
  101. package/dist/internal/island-mount.js +66 -0
  102. package/dist/internal/island-mount.js.map +1 -0
  103. package/dist/internal/island-owner.d.ts +5 -0
  104. package/dist/internal/island-owner.d.ts.map +1 -0
  105. package/dist/internal/island-owner.js +20 -0
  106. package/dist/internal/island-owner.js.map +1 -0
  107. package/dist/internal/island-resource.d.ts +30 -0
  108. package/dist/internal/island-resource.d.ts.map +1 -0
  109. package/dist/internal/island-resource.js +57 -0
  110. package/dist/internal/island-resource.js.map +1 -0
  111. package/dist/internal/island-signals.d.ts +51 -0
  112. package/dist/internal/island-signals.d.ts.map +1 -0
  113. package/dist/internal/island-signals.js +220 -0
  114. package/dist/internal/island-signals.js.map +1 -0
  115. package/dist/internal/keep-alive-table.d.ts +23 -0
  116. package/dist/internal/keep-alive-table.d.ts.map +1 -0
  117. package/dist/internal/keep-alive-table.js +53 -0
  118. package/dist/internal/keep-alive-table.js.map +1 -0
  119. package/dist/internal/live-contract.d.ts +96 -0
  120. package/dist/internal/live-contract.d.ts.map +1 -0
  121. package/dist/internal/live-contract.js +5 -0
  122. package/dist/internal/live-contract.js.map +1 -0
  123. package/dist/internal/live-queries.d.ts +22 -0
  124. package/dist/internal/live-queries.d.ts.map +1 -0
  125. package/dist/internal/live-queries.js +54 -0
  126. package/dist/internal/live-queries.js.map +1 -0
  127. package/dist/internal/page-events.d.ts +10 -0
  128. package/dist/internal/page-events.d.ts.map +1 -0
  129. package/dist/internal/page-events.js +26 -0
  130. package/dist/internal/page-events.js.map +1 -0
  131. package/dist/internal/page-execution.d.ts +14 -1
  132. package/dist/internal/page-execution.d.ts.map +1 -1
  133. package/dist/internal/page-execution.js +415 -31
  134. package/dist/internal/page-execution.js.map +1 -1
  135. package/dist/internal/ppr/admission.d.ts +83 -0
  136. package/dist/internal/ppr/admission.d.ts.map +1 -0
  137. package/dist/internal/ppr/admission.js +221 -0
  138. package/dist/internal/ppr/admission.js.map +1 -0
  139. package/dist/internal/ppr/canary.d.ts +24 -0
  140. package/dist/internal/ppr/canary.d.ts.map +1 -0
  141. package/dist/internal/ppr/canary.js +102 -0
  142. package/dist/internal/ppr/canary.js.map +1 -0
  143. package/dist/internal/ppr/compile-shell.d.ts +39 -0
  144. package/dist/internal/ppr/compile-shell.d.ts.map +1 -0
  145. package/dist/internal/ppr/compile-shell.js +298 -0
  146. package/dist/internal/ppr/compile-shell.js.map +1 -0
  147. package/dist/internal/ppr/contract.d.ts +177 -0
  148. package/dist/internal/ppr/contract.d.ts.map +1 -0
  149. package/dist/internal/ppr/contract.js +80 -0
  150. package/dist/internal/ppr/contract.js.map +1 -0
  151. package/dist/internal/ppr/frames.d.ts +71 -0
  152. package/dist/internal/ppr/frames.d.ts.map +1 -0
  153. package/dist/internal/ppr/frames.js +119 -0
  154. package/dist/internal/ppr/frames.js.map +1 -0
  155. package/dist/internal/ppr/integration.d.ts +19 -0
  156. package/dist/internal/ppr/integration.d.ts.map +1 -0
  157. package/dist/internal/ppr/integration.js +106 -0
  158. package/dist/internal/ppr/integration.js.map +1 -0
  159. package/dist/internal/ppr/options.d.ts +96 -0
  160. package/dist/internal/ppr/options.d.ts.map +1 -0
  161. package/dist/internal/ppr/options.js +125 -0
  162. package/dist/internal/ppr/options.js.map +1 -0
  163. package/dist/internal/ppr/route-rules.d.ts +12 -0
  164. package/dist/internal/ppr/route-rules.d.ts.map +1 -0
  165. package/dist/internal/ppr/route-rules.js +53 -0
  166. package/dist/internal/ppr/route-rules.js.map +1 -0
  167. package/dist/internal/ppr/runtime.d.ts +29 -0
  168. package/dist/internal/ppr/runtime.d.ts.map +1 -0
  169. package/dist/internal/ppr/runtime.js +32 -0
  170. package/dist/internal/ppr/runtime.js.map +1 -0
  171. package/dist/internal/ppr/server.d.ts +29 -0
  172. package/dist/internal/ppr/server.d.ts.map +1 -0
  173. package/dist/internal/ppr/server.js +1407 -0
  174. package/dist/internal/ppr/server.js.map +1 -0
  175. package/dist/internal/ppr/shell-store.d.ts +38 -0
  176. package/dist/internal/ppr/shell-store.d.ts.map +1 -0
  177. package/dist/internal/ppr/shell-store.js +90 -0
  178. package/dist/internal/ppr/shell-store.js.map +1 -0
  179. package/dist/internal/prerendered-pages.d.ts +1 -1
  180. package/dist/internal/prerendered-pages.d.ts.map +1 -1
  181. package/dist/internal/prerendered-pages.js +7 -4
  182. package/dist/internal/prerendered-pages.js.map +1 -1
  183. package/dist/internal/query-digest.d.ts +9 -0
  184. package/dist/internal/query-digest.d.ts.map +1 -0
  185. package/dist/internal/query-digest.js +23 -0
  186. package/dist/internal/query-digest.js.map +1 -0
  187. package/dist/internal/query-runtime.d.ts +6 -1
  188. package/dist/internal/query-runtime.d.ts.map +1 -1
  189. package/dist/internal/query-runtime.js +9 -0
  190. package/dist/internal/query-runtime.js.map +1 -1
  191. package/dist/internal/query-seam.d.ts +29 -0
  192. package/dist/internal/query-seam.d.ts.map +1 -0
  193. package/dist/internal/query-seam.js +23 -0
  194. package/dist/internal/query-seam.js.map +1 -0
  195. package/dist/internal/render-document.d.ts +28 -1
  196. package/dist/internal/render-document.d.ts.map +1 -1
  197. package/dist/internal/render-document.js +83 -18
  198. package/dist/internal/render-document.js.map +1 -1
  199. package/dist/internal/request-abort.d.ts +8 -0
  200. package/dist/internal/request-abort.d.ts.map +1 -0
  201. package/dist/internal/request-abort.js +22 -0
  202. package/dist/internal/request-abort.js.map +1 -0
  203. package/dist/internal/server-boundary.d.ts +2 -0
  204. package/dist/internal/server-boundary.d.ts.map +1 -1
  205. package/dist/internal/server-boundary.js +8 -1
  206. package/dist/internal/server-boundary.js.map +1 -1
  207. package/dist/internal/share-data.d.ts +10 -0
  208. package/dist/internal/share-data.d.ts.map +1 -0
  209. package/dist/internal/share-data.js +135 -0
  210. package/dist/internal/share-data.js.map +1 -0
  211. package/dist/internal/store-signature.d.ts +15 -0
  212. package/dist/internal/store-signature.d.ts.map +1 -0
  213. package/dist/internal/store-signature.js +114 -0
  214. package/dist/internal/store-signature.js.map +1 -0
  215. package/dist/internal/vite-plugin-compat.d.ts +2 -2
  216. package/dist/internal/vite-plugin-compat.js +2 -2
  217. package/dist/internal/web-app.d.ts +25 -0
  218. package/dist/internal/web-app.d.ts.map +1 -1
  219. package/dist/internal/web-app.js +78 -3
  220. package/dist/internal/web-app.js.map +1 -1
  221. package/dist/islands-swap.d.ts +41 -0
  222. package/dist/islands-swap.d.ts.map +1 -0
  223. package/dist/islands-swap.js +270 -0
  224. package/dist/islands-swap.js.map +1 -0
  225. package/dist/islands.d.ts +86 -24
  226. package/dist/islands.d.ts.map +1 -1
  227. package/dist/islands.js +138 -78
  228. package/dist/islands.js.map +1 -1
  229. package/dist/isr.d.ts +67 -13
  230. package/dist/isr.d.ts.map +1 -1
  231. package/dist/isr.js +247 -99
  232. package/dist/isr.js.map +1 -1
  233. package/dist/keep-alive.d.ts +147 -0
  234. package/dist/keep-alive.d.ts.map +1 -0
  235. package/dist/keep-alive.js +1069 -0
  236. package/dist/keep-alive.js.map +1 -0
  237. package/dist/live/bus.d.ts +56 -0
  238. package/dist/live/bus.d.ts.map +1 -0
  239. package/dist/live/bus.js +32 -0
  240. package/dist/live/bus.js.map +1 -0
  241. package/dist/live/core.d.ts +25 -0
  242. package/dist/live/core.d.ts.map +1 -0
  243. package/dist/live/core.js +29 -0
  244. package/dist/live/core.js.map +1 -0
  245. package/dist/live/history.d.ts +38 -0
  246. package/dist/live/history.d.ts.map +1 -0
  247. package/dist/live/history.js +104 -0
  248. package/dist/live/history.js.map +1 -0
  249. package/dist/live/hub-protocol.d.ts +46 -0
  250. package/dist/live/hub-protocol.d.ts.map +1 -0
  251. package/dist/live/hub-protocol.js +108 -0
  252. package/dist/live/hub-protocol.js.map +1 -0
  253. package/dist/live/hub.d.ts +181 -0
  254. package/dist/live/hub.d.ts.map +1 -0
  255. package/dist/live/hub.js +539 -0
  256. package/dist/live/hub.js.map +1 -0
  257. package/dist/live/keyring.d.ts +87 -0
  258. package/dist/live/keyring.d.ts.map +1 -0
  259. package/dist/live/keyring.js +260 -0
  260. package/dist/live/keyring.js.map +1 -0
  261. package/dist/live/read-capped.d.ts +6 -0
  262. package/dist/live/read-capped.d.ts.map +1 -0
  263. package/dist/live/read-capped.js +42 -0
  264. package/dist/live/read-capped.js.map +1 -0
  265. package/dist/live/revocations.d.ts +52 -0
  266. package/dist/live/revocations.d.ts.map +1 -0
  267. package/dist/live/revocations.js +116 -0
  268. package/dist/live/revocations.js.map +1 -0
  269. package/dist/live/settings.d.ts +14 -0
  270. package/dist/live/settings.d.ts.map +1 -0
  271. package/dist/live/settings.js +38 -0
  272. package/dist/live/settings.js.map +1 -0
  273. package/dist/live-client.d.ts +34 -0
  274. package/dist/live-client.d.ts.map +1 -0
  275. package/dist/live-client.js +840 -0
  276. package/dist/live-client.js.map +1 -0
  277. package/dist/live-hub.d.ts +130 -0
  278. package/dist/live-hub.d.ts.map +1 -0
  279. package/dist/live-hub.js +507 -0
  280. package/dist/live-hub.js.map +1 -0
  281. package/dist/live-redis.d.ts +48 -0
  282. package/dist/live-redis.d.ts.map +1 -0
  283. package/dist/live-redis.js +201 -0
  284. package/dist/live-redis.js.map +1 -0
  285. package/dist/live.d.ts +94 -0
  286. package/dist/live.d.ts.map +1 -0
  287. package/dist/live.js +869 -0
  288. package/dist/live.js.map +1 -0
  289. package/dist/manifest.d.ts +113 -2
  290. package/dist/manifest.d.ts.map +1 -1
  291. package/dist/manifest.js +109 -5
  292. package/dist/manifest.js.map +1 -1
  293. package/dist/navigation.d.ts +9 -3
  294. package/dist/navigation.d.ts.map +1 -1
  295. package/dist/navigation.js +13 -3
  296. package/dist/navigation.js.map +1 -1
  297. package/dist/plugins/vite-zone-guard.d.ts +6 -0
  298. package/dist/plugins/vite-zone-guard.d.ts.map +1 -1
  299. package/dist/plugins/vite-zone-guard.js +8 -2
  300. package/dist/plugins/vite-zone-guard.js.map +1 -1
  301. package/dist/ppr.d.ts +73 -0
  302. package/dist/ppr.d.ts.map +1 -0
  303. package/dist/ppr.js +76 -0
  304. package/dist/ppr.js.map +1 -0
  305. package/dist/query.d.ts +8 -13
  306. package/dist/query.d.ts.map +1 -1
  307. package/dist/query.js +112 -18
  308. package/dist/query.js.map +1 -1
  309. package/dist/render-seam.d.ts +30 -0
  310. package/dist/render-seam.d.ts.map +1 -1
  311. package/dist/render-seam.js +5 -0
  312. package/dist/render-seam.js.map +1 -1
  313. package/dist/route-types.d.ts.map +1 -1
  314. package/dist/route-types.js +2 -2
  315. package/dist/route-types.js.map +1 -1
  316. package/dist/router.d.ts +72 -4
  317. package/dist/router.d.ts.map +1 -1
  318. package/dist/router.js +374 -99
  319. package/dist/router.js.map +1 -1
  320. package/dist/server-components/build.d.ts.map +1 -1
  321. package/dist/server-components/build.js +3 -3
  322. package/dist/server-components/build.js.map +1 -1
  323. package/dist/server-components/client.d.ts +23 -1
  324. package/dist/server-components/client.d.ts.map +1 -1
  325. package/dist/server-components/client.js +92 -5
  326. package/dist/server-components/client.js.map +1 -1
  327. package/dist/server-components/hole.d.ts +8 -0
  328. package/dist/server-components/hole.d.ts.map +1 -0
  329. package/dist/server-components/hole.js +20 -0
  330. package/dist/server-components/hole.js.map +1 -0
  331. package/dist/server-components/html-policy.d.ts.map +1 -1
  332. package/dist/server-components/html-policy.js +62 -4
  333. package/dist/server-components/html-policy.js.map +1 -1
  334. package/dist/server-components/html-scan.d.ts +17 -0
  335. package/dist/server-components/html-scan.d.ts.map +1 -0
  336. package/dist/server-components/html-scan.js +180 -0
  337. package/dist/server-components/html-scan.js.map +1 -0
  338. package/dist/server-components/html.d.ts.map +1 -1
  339. package/dist/server-components/html.js +19 -4
  340. package/dist/server-components/html.js.map +1 -1
  341. package/dist/server-components/router-integration.d.ts +4 -1
  342. package/dist/server-components/router-integration.d.ts.map +1 -1
  343. package/dist/server-components/router-integration.js +23 -2
  344. package/dist/server-components/router-integration.js.map +1 -1
  345. package/dist/server-components/runtime.d.ts +6 -1
  346. package/dist/server-components/runtime.d.ts.map +1 -1
  347. package/dist/server-components/runtime.js +86 -28
  348. package/dist/server-components/runtime.js.map +1 -1
  349. package/dist/server-components/state.d.ts +3 -1
  350. package/dist/server-components/state.d.ts.map +1 -1
  351. package/dist/server-components/state.js +50 -38
  352. package/dist/server-components/state.js.map +1 -1
  353. package/dist/server-components/types.d.ts +9 -0
  354. package/dist/server-components/types.d.ts.map +1 -1
  355. package/dist/vite.d.ts +5 -1
  356. package/dist/vite.d.ts.map +1 -1
  357. package/dist/vite.js +58 -4
  358. package/dist/vite.js.map +1 -1
  359. package/package.json +48 -8
  360. package/src/boundary.ts +77 -5
  361. package/src/build-plan.ts +3 -0
  362. package/src/build-vite.ts +2 -0
  363. package/src/build.ts +19 -1
  364. package/src/client.ts +147 -10
  365. package/src/csp.ts +16 -2
  366. package/src/deferred.ts +11 -5
  367. package/src/dev-feed.ts +11 -2
  368. package/src/dev-session.ts +22 -11
  369. package/src/dev.ts +29 -8
  370. package/src/diagnostic-catalog.ts +53 -0
  371. package/src/diagnostic.ts +1 -0
  372. package/src/fn.ts +24 -7
  373. package/src/fs.ts +22 -8
  374. package/src/identity.ts +61 -0
  375. package/src/index.ts +19 -0
  376. package/src/internal/cdn/cloudflare.ts +2 -1
  377. package/src/internal/certified-components.ts +1 -1
  378. package/src/internal/codegen.ts +87 -8
  379. package/src/internal/dev-websocket.ts +230 -0
  380. package/src/internal/hole-runtime.ts +286 -0
  381. package/src/internal/identity-epoch.ts +235 -0
  382. package/src/internal/invalidation-contract.ts +35 -0
  383. package/src/internal/invalidation.ts +213 -0
  384. package/src/internal/island-bindings.ts +191 -0
  385. package/src/internal/island-list.ts +92 -0
  386. package/src/internal/island-mount.ts +68 -0
  387. package/src/internal/island-owner.ts +20 -0
  388. package/src/internal/island-resource.ts +76 -0
  389. package/src/internal/island-signals.ts +249 -0
  390. package/src/internal/keep-alive-table.ts +72 -0
  391. package/src/internal/live-contract.ts +106 -0
  392. package/src/internal/live-queries.ts +60 -0
  393. package/src/internal/page-events.ts +27 -0
  394. package/src/internal/page-execution.ts +579 -40
  395. package/src/internal/ppr/admission.ts +300 -0
  396. package/src/internal/ppr/canary.ts +122 -0
  397. package/src/internal/ppr/compile-shell.ts +379 -0
  398. package/src/internal/ppr/contract.ts +265 -0
  399. package/src/internal/ppr/frames.ts +170 -0
  400. package/src/internal/ppr/integration.ts +137 -0
  401. package/src/internal/ppr/options.ts +221 -0
  402. package/src/internal/ppr/route-rules.ts +77 -0
  403. package/src/internal/ppr/runtime.ts +36 -0
  404. package/src/internal/ppr/server.ts +1764 -0
  405. package/src/internal/ppr/shell-store.ts +139 -0
  406. package/src/internal/prerendered-pages.ts +7 -4
  407. package/src/internal/query-digest.ts +34 -0
  408. package/src/internal/query-runtime.ts +14 -1
  409. package/src/internal/query-seam.ts +48 -0
  410. package/src/internal/render-document.ts +149 -22
  411. package/src/internal/request-abort.ts +26 -0
  412. package/src/internal/server-boundary.ts +10 -1
  413. package/src/internal/share-data.ts +136 -0
  414. package/src/internal/store-signature.ts +149 -0
  415. package/src/internal/vite-plugin-compat.ts +2 -2
  416. package/src/internal/web-app.ts +121 -3
  417. package/src/islands-swap.ts +283 -0
  418. package/src/islands.ts +169 -72
  419. package/src/isr.ts +348 -106
  420. package/src/keep-alive.ts +1315 -0
  421. package/src/live/bus.ts +77 -0
  422. package/src/live/core.ts +52 -0
  423. package/src/live/history.ts +109 -0
  424. package/src/live/hub-protocol.ts +133 -0
  425. package/src/live/hub.ts +615 -0
  426. package/src/live/keyring.ts +379 -0
  427. package/src/live/read-capped.ts +36 -0
  428. package/src/live/revocations.ts +155 -0
  429. package/src/live/settings.ts +46 -0
  430. package/src/live-client.ts +973 -0
  431. package/src/live-hub.ts +658 -0
  432. package/src/live-redis.ts +259 -0
  433. package/src/live.ts +1096 -0
  434. package/src/manifest.ts +274 -6
  435. package/src/navigation.ts +14 -2
  436. package/src/plugins/vite-zone-guard.ts +15 -2
  437. package/src/ppr.ts +153 -0
  438. package/src/query.ts +147 -20
  439. package/src/render-seam.ts +35 -0
  440. package/src/route-types.ts +3 -1
  441. package/src/router.ts +494 -101
  442. package/src/server-components/build.ts +9 -3
  443. package/src/server-components/client.ts +89 -5
  444. package/src/server-components/hole.ts +23 -0
  445. package/src/server-components/html-policy.ts +63 -4
  446. package/src/server-components/html-scan.ts +159 -0
  447. package/src/server-components/html.ts +18 -4
  448. package/src/server-components/router-integration.ts +21 -2
  449. package/src/server-components/runtime.ts +112 -30
  450. package/src/server-components/state.ts +66 -44
  451. package/src/server-components/types.ts +16 -0
  452. package/src/vite.ts +75 -6
  453. package/dist/nano.d.ts +0 -125
  454. package/dist/nano.d.ts.map +0 -1
  455. package/dist/nano.js +0 -190
  456. package/dist/nano.js.map +0 -1
  457. package/src/nano.ts +0 -246
@@ -0,0 +1,170 @@
1
+ import { validateComponentHTML } from "../../server-components/html.ts"
2
+ import { serializeData } from "../render-document.ts"
3
+ import { holeInvoker } from "./runtime.ts"
4
+
5
+ /**
6
+ * A hole name: it becomes a DOM id, an attribute value, a hashed script and part of a renderer's id
7
+ * prefix, so it stays this narrow. No `--` and no trailing `-`: Svelte refuses an id prefix with `--`.
8
+ */
9
+ export const HOLE_NAME: RegExp = /^(?=[a-z0-9-]{1,32}$)[a-z](?:-?[a-z0-9])*$/
10
+
11
+ /** What one hole sends: its server HTML, plus props when it hydrates, or the record of the server
12
+ * component it shows, without that HTML. An error carries the error slot's HTML only, never the
13
+ * thrown message. */
14
+ export type HoleOutcome =
15
+ | {
16
+ readonly html: string
17
+ readonly props?: unknown
18
+ readonly component?: unknown
19
+ readonly error?: undefined
20
+ }
21
+ | { readonly html: string; readonly error: true }
22
+
23
+ /** A frame's JSON, as the runtime reads it to fill a host. */
24
+ export type HoleFrameBody =
25
+ | {
26
+ readonly v: 1
27
+ readonly html: string
28
+ readonly props?: unknown
29
+ readonly component?: unknown
30
+ readonly error?: undefined
31
+ }
32
+ | { readonly v: 1; readonly html: string; readonly error: true }
33
+
34
+ export const assertHoleName = (name: string): void => {
35
+ if (!HOLE_NAME.test(name)) {
36
+ throw new TypeError(
37
+ `[nifra/web] ppr: hole name "${name}" must match ${HOLE_NAME.source} (it becomes a DOM id and a hashed script)`,
38
+ )
39
+ }
40
+ }
41
+
42
+ /** Refuse hole HTML that could run script or escape its host, with the policy server
43
+ * components already enforce. Throws; the caller sends that hole's error frame instead. */
44
+ export const validateHoleHtml = (html: string): void => validateComponentHTML(html)
45
+
46
+ /**
47
+ * JSON safe inside a `<script>` element. Script data ends only at `</script`, and only `<!--` can
48
+ * move the tokenizer into its escaped states, so escaping `</` and `<!--` is enough; both escapes are
49
+ * plain JSON. Escaping every `<` and `>` instead, as `serializeData` does, made a 100 KB hole's frame
50
+ * 53% larger than its HTML; this makes it about 10% larger.
51
+ */
52
+ const scriptJson = (value: unknown): string =>
53
+ JSON.stringify(value).replaceAll("</", "<\\/").replaceAll("<!--", "<\\u0021--")
54
+
55
+ /**
56
+ * The bytes that fill hole `name`: an inert JSON frame and the constant invoker. No `</script>` or
57
+ * comment opener in the HTML or props survives into the element. Throws a `RangeError` when the
58
+ * encoded frame exceeds `maxBytes`.
59
+ */
60
+ export function encodeHoleFrame(name: string, outcome: HoleOutcome, maxBytes: number): string {
61
+ const body: HoleFrameBody =
62
+ outcome.error === true
63
+ ? { v: 1, error: true, html: outcome.html }
64
+ : outcome.component !== undefined
65
+ ? { v: 1, html: outcome.html, component: outcome.component }
66
+ : outcome.props === undefined
67
+ ? { v: 1, html: outcome.html }
68
+ : { v: 1, html: outcome.html, props: outcome.props }
69
+ const frame = `<script type="application/json" id="nifra-ppr:${name}">${scriptJson(body)}</script><script>${holeInvoker(name)}</script>`
70
+ // UTF-8 needs at least one and at most three bytes per UTF-16 unit, so only a frame between the two
71
+ // bounds is encoded to measure it.
72
+ if (
73
+ frame.length > maxBytes ||
74
+ (frame.length * 3 > maxBytes && new TextEncoder().encode(frame).length > maxBytes)
75
+ ) {
76
+ throw new RangeError(`[nifra/web] ppr: hole "${name}" frame exceeds ${maxBytes} bytes`)
77
+ }
78
+ return frame
79
+ }
80
+
81
+ /**
82
+ * The JSON of a frame {@link encodeHoleFrame} wrote: what a data request sends to refill a host. The
83
+ * opening tag holds no `>` before its end, and the escaped JSON no `</script>`.
84
+ */
85
+ export function frameBodyOf(frame: string): HoleFrameBody {
86
+ const start = frame.indexOf(">") + 1
87
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: encodeHoleFrame wrote this JSON from a HoleFrameBody.
88
+ return JSON.parse(frame.slice(start, frame.indexOf("</script>", start))) as HoleFrameBody
89
+ }
90
+
91
+ /**
92
+ * The per-response facts the runtime needs to send a cancel beacon. Inert JSON: never cached. `shared`
93
+ * names the holes whose frames serve every visitor, which the canary check scans with the shell.
94
+ */
95
+ export const encodeRequestScript = (
96
+ cancelUrl: string,
97
+ token: string,
98
+ holes: readonly string[],
99
+ shared: readonly string[] = [],
100
+ ) =>
101
+ `<script type="application/json" id="nifra-ppr-req">${serializeData(shared.length === 0 ? { cancel: cancelUrl, token, holes } : { cancel: cancelUrl, token, holes, shared })}</script>`
102
+
103
+ const TOKEN_KEY = '"token":"'
104
+
105
+ /**
106
+ * {@link encodeRequestScript} before and after the token, so a route's script is serialized once and
107
+ * each response adds only its token. Holds for a base64url token, which serializing leaves as it is.
108
+ * An escaped JSON string never holds `"token":"` (each of its quotes follows a backslash), so the
109
+ * key found is the token's.
110
+ */
111
+ export function requestScriptAround(
112
+ cancelUrl: string,
113
+ holes: readonly string[],
114
+ shared: readonly string[] = [],
115
+ ): readonly [string, string] {
116
+ const script = encodeRequestScript(cancelUrl, "", holes, shared)
117
+ const at = script.indexOf(TOKEN_KEY) + TOKEN_KEY.length
118
+ return [script.slice(0, at), script.slice(at)]
119
+ }
120
+
121
+ const HOST = 'data-nifra-hole="'
122
+
123
+ /**
124
+ * Where {@link injectHoleFallbacks} puts each fallback, in document order. One pass over the shell
125
+ * finds every host: a search per hole, and another for its duplicate, read a large shell many times.
126
+ */
127
+ export function holeFallbackInserts(
128
+ shell: string,
129
+ fallbacks: ReadonlyMap<string, string>,
130
+ ): Array<{ readonly at: number; readonly html: string }> {
131
+ // A declared hole's host, as the index after its name's closing quote; -1 once it has two.
132
+ const hosts = new Map<string, number>()
133
+ for (let at = shell.indexOf(HOST); at !== -1; at = shell.indexOf(HOST, at + HOST.length)) {
134
+ const end = shell.indexOf('"', at + HOST.length)
135
+ if (end === -1) break
136
+ const name = shell.slice(at + HOST.length, end)
137
+ if (fallbacks.has(name)) hosts.set(name, hosts.has(name) ? -1 : end + 1)
138
+ }
139
+ const inserts: Array<{ at: number; html: string }> = []
140
+ for (const [name, html] of fallbacks) {
141
+ const after = hosts.get(name)
142
+ if (after === undefined)
143
+ throw new TypeError(`[nifra/web] ppr: hole "${name}" has no host in the shell`)
144
+ if (after === -1) {
145
+ throw new TypeError(`[nifra/web] ppr: hole "${name}" has more than one host in the shell`)
146
+ }
147
+ const close = shell.indexOf(">", after)
148
+ if (close === -1 || !shell.startsWith("</", close + 1)) {
149
+ throw new TypeError(`[nifra/web] ppr: hole "${name}"'s host must render empty`)
150
+ }
151
+ inserts.push({ at: close + 1, html })
152
+ }
153
+ return inserts.sort((a, b) => a.at - b.at)
154
+ }
155
+
156
+ /**
157
+ * Put each hole's fallback HTML into its empty host in a rendered shell, once per shell build. Every
158
+ * declared hole must have exactly one host and that host must be empty: the adapter renders
159
+ * it opaque, so anything inside would be framework output the fill would destroy.
160
+ */
161
+ export function injectHoleFallbacks(shell: string, fallbacks: ReadonlyMap<string, string>): string {
162
+ const inserts = holeFallbackInserts(shell, fallbacks)
163
+ let out = ""
164
+ let from = 0
165
+ for (const insert of inserts) {
166
+ out += shell.slice(from, insert.at) + insert.html
167
+ from = insert.at
168
+ }
169
+ return out + shell.slice(from)
170
+ }
@@ -0,0 +1,137 @@
1
+ import { readBoundedBytes, TransportCodecError } from "@nifrajs/core/transport-codec"
2
+ import type { CacheStore } from "../../isr.ts"
3
+ import type { ShellCheckRecord } from "./compile-shell.ts"
4
+ import {
5
+ PPR_CANCEL_PATH,
6
+ type PprEnvironment,
7
+ type PprIntegration,
8
+ PprShellCold,
9
+ PprShellUnavailable,
10
+ PprShellUnchecked,
11
+ variationQuery,
12
+ wantsBlockingRender,
13
+ } from "./contract.ts"
14
+ import type { ResolvedPprOptions } from "./options.ts"
15
+ import { assertPprRoute, variationsOf } from "./route-rules.ts"
16
+ import { createPprServer } from "./server.ts"
17
+ import { digest, sharedShellStore } from "./shell-store.ts"
18
+
19
+ // The stream sets its own type, caching and CSP; a middleware or gate header joins the rest.
20
+ const OWN_HEADERS: ReadonlySet<string> = new Set([
21
+ "content-type",
22
+ "cache-control",
23
+ "content-security-policy",
24
+ "content-length",
25
+ ])
26
+
27
+ const MAX_CANCEL_BYTES = 64
28
+
29
+ /** Where shells are shared beyond the process, as `partialPrerendering` checked them. */
30
+ export interface PprSharing {
31
+ readonly store: CacheStore | undefined
32
+ readonly storeSecret: string | undefined
33
+ readonly deployment: string | undefined
34
+ }
35
+
36
+ /**
37
+ * The deployment every shell key names. The client entry changes with each client build; each route's
38
+ * checked shell hash changes with its shell code, which the client entry does not see; the
39
+ * platform's own name, when given, covers the rest.
40
+ */
41
+ export function shellDeployment(
42
+ clientEntry: string,
43
+ shellChecks: ShellCheckRecord | "unchecked-development",
44
+ named: string | undefined,
45
+ ): string {
46
+ if (shellChecks === "unchecked-development" && named === undefined) return clientEntry
47
+ const routes =
48
+ shellChecks === "unchecked-development"
49
+ ? ""
50
+ : Object.keys(shellChecks.routes)
51
+ .sort()
52
+ .map((id) => `${id}=${shellChecks.routes[id]}`)
53
+ .join(",")
54
+ return `${clientEntry}~${digest(`${routes}|${named ?? ""}`)}`
55
+ }
56
+
57
+ /** One app's partial prerendering, made when `createWebApp` reaches its `experimental.ppr`. */
58
+ export function createPprIntegration(
59
+ options: ResolvedPprOptions,
60
+ shellChecks: ShellCheckRecord | "unchecked-development",
61
+ origin: string | undefined,
62
+ environment: PprEnvironment,
63
+ sharing: PprSharing = { store: undefined, storeSecret: undefined, deployment: undefined },
64
+ ): PprIntegration {
65
+ const { perRequestNonce, ...app } = environment
66
+ if (perRequestNonce) {
67
+ throw new TypeError(
68
+ "[nifra/web] experimental.ppr needs `csp: createCspPolicy(...)`, not `nonce`: a per-request nonce would make every shell unique",
69
+ )
70
+ }
71
+ const deployment = shellDeployment(app.deployment, shellChecks, sharing.deployment)
72
+ const server = createPprServer({
73
+ ...app,
74
+ deployment,
75
+ clientEntry: app.deployment,
76
+ options,
77
+ cancelUrl: PPR_CANCEL_PATH,
78
+ shellChecks,
79
+ shared:
80
+ sharing.store === undefined || sharing.storeSecret === undefined
81
+ ? undefined
82
+ : sharedShellStore(sharing.store, sharing.storeSecret, deployment),
83
+ })
84
+ const uncheckedWarned = new Set<string>()
85
+ return {
86
+ server,
87
+ // biome-ignore lint/plugin/noConditionalEmptyObjectSpread: an app with no configured origin has none, which shell meta reads as absent.
88
+ ...(origin === undefined ? {} : { origin }),
89
+ cancelPath: PPR_CANCEL_PATH,
90
+ // A beacon carries one 22-character token; anything longer is not one. The token is unguessable,
91
+ // so the answer is the same whether it named a live response or not.
92
+ async cancel(request) {
93
+ const length = Number(request.headers.get("content-length") ?? "NaN")
94
+ if (!(length > 0 && length <= MAX_CANCEL_BYTES)) return new Response(null, { status: 400 })
95
+ let token: Uint8Array
96
+ try {
97
+ // The header is a claim; the cap holds on the bytes that arrive.
98
+ token = await readBoundedBytes(new Response(request.body, { headers: request.headers }), {
99
+ maxBytes: MAX_CANCEL_BYTES,
100
+ })
101
+ } catch (error) {
102
+ if (error instanceof TransportCodecError) return new Response(null, { status: 400 })
103
+ throw error
104
+ }
105
+ server.cancel(new TextDecoder().decode(token))
106
+ return new Response(null, { status: 204 })
107
+ },
108
+ async stream(request, written) {
109
+ if (wantsBlockingRender(options, request.request)) return "settled"
110
+ let response: Response
111
+ try {
112
+ response = await server.respond(request)
113
+ } catch (error) {
114
+ if (!(error instanceof PprShellUnavailable)) throw error
115
+ if (error instanceof PprShellCold) return "deferred"
116
+ if (error instanceof PprShellUnchecked && !uncheckedWarned.has(request.routeId)) {
117
+ uncheckedWarned.add(request.routeId)
118
+ console.warn(error.message)
119
+ }
120
+ return "settled"
121
+ }
122
+ for (const [name, value] of Object.entries(written() ?? {}))
123
+ if (!OWN_HEADERS.has(name)) response.headers.set(name, value)
124
+ return response
125
+ },
126
+ shellData: (request, signal) =>
127
+ server
128
+ .shellData(request)
129
+ .catch((error: unknown) =>
130
+ error instanceof PprShellUnavailable ? request.loadShell(signal) : Promise.reject(error),
131
+ ),
132
+ variationQuery,
133
+ assertRoute: (route, layouts, boundaries) =>
134
+ assertPprRoute(route, layouts, boundaries, options),
135
+ variationsOf,
136
+ }
137
+ }
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Partial prerendering limits and defaults. Internal while the feature is experimental: every
3
+ * number the plan names lives here once, so a deployment overrides it in one object and the request
4
+ * path reads the resolved copy rather than a scattered constant.
5
+ */
6
+
7
+ /** Overrides for {@link PPR_DEFAULTS}. Every field is optional; an omitted one keeps its default. */
8
+ export interface PprOptions {
9
+ /** A hole's deadline when its boundary sets no `timeout`. */
10
+ readonly holeTimeoutMs?: number
11
+ /** The largest `timeout` a boundary may set, and the cap on a route's total deadline. */
12
+ readonly maxHoleTimeoutMs?: number
13
+ /** Added to the largest hole timeout to give the route's total deadline. */
14
+ readonly totalDeadlineSlackMs?: number
15
+ /** Dynamic plus shared boundaries one PPR route may declare. */
16
+ readonly maxHolesPerRoute?: number
17
+ /** Shell entries one route may keep in the memory store. */
18
+ readonly shellEntriesPerRoute?: number
19
+ /**
20
+ * Bytes every shell entry in the memory store may hold together: its document, an estimate of the
21
+ * heap its loader data holds, and its key. An entry over the whole budget is served but not kept.
22
+ */
23
+ readonly shellStoreBytes?: number
24
+ /** How long a `notFound()` shell is cached. */
25
+ readonly notFoundTtlSeconds?: number
26
+ /** Shell builds one route may run at once; requests over it get the blocking render. */
27
+ readonly shellBuildConcurrency?: number
28
+ /**
29
+ * Once the store is full, requests one shell key must see before its shell is built and may push
30
+ * an older one out. The ones before stream the page whole, each hole as it settles, so a path
31
+ * visited once cannot evict a popular one. While the store has room, every key builds at once.
32
+ */
33
+ readonly shellAfterRequests?: number
34
+ /** A failing rebuild keeps serving the stale shell for `revalidate` times this... */
35
+ readonly staleOnErrorFactor?: number
36
+ /** ...but never longer than this. */
37
+ readonly maxStaleOnErrorSeconds?: number
38
+ /** Requests that get the blocking render instead of a stream: a user-agent pattern or a predicate. */
39
+ readonly blockingFor?: RegExp | ((request: Request) => boolean)
40
+ /** Abandoned invocations at which one boundary stops being admitted. */
41
+ readonly boundaryAbandonedTrip?: number
42
+ /** Abandoned invocations per process at which boundaries holding any stop being admitted. */
43
+ readonly processAbandonedTrip?: number
44
+ /** Abandoned invocations per process at which no hole is admitted. */
45
+ readonly admissionStop?: number
46
+ /** How long after its abort a still-pending invocation counts as abandoned. */
47
+ readonly abandonGraceMs?: number
48
+ /** PPR responses per process with hole loaders still running. */
49
+ readonly maxHoleWork?: number
50
+ /** How long after the total deadline an undrained stream is kept before it is aborted. */
51
+ readonly writeGraceMs?: number
52
+ /** Unsent bytes one response may queue. */
53
+ readonly maxUnsentBytesPerResponse?: number
54
+ /** Unsent bytes every PPR response in the process may queue together. */
55
+ readonly maxUnsentBytes?: number
56
+ /** Serialized size of one hole frame. */
57
+ readonly frameBytes?: number
58
+ /** The shell is padded to at least this many bytes; Safari holds back smaller first chunks. */
59
+ readonly minShellBytes?: number
60
+ /**
61
+ * When hole HTML is checked for script, handlers and other escapes before it is framed:
62
+ * `"always"`, or `"development"` only. The check costs about 20 us per KB of hole HTML; turn it off
63
+ * in production only when a strict CSP already blocks inline script.
64
+ */
65
+ readonly holeHtmlValidation?: "always" | "development"
66
+ /**
67
+ * Whether a page left with holes pending posts a cancel beacon, so their loaders stop before their
68
+ * deadline. Off by default on Cloudflare Workers, where the beacon reaches another isolate.
69
+ */
70
+ readonly cancelBeacon?: boolean
71
+ /**
72
+ * How long a process serves a shell or shared hole it wrote to or read from the shared store before
73
+ * reading it there again, so a purge another process made reaches this one. 0 reads it every time.
74
+ */
75
+ readonly sharedRecheckMs?: number
76
+ }
77
+
78
+ export type ResolvedPprOptions = {
79
+ readonly [K in keyof PprOptions]-?: Exclude<PprOptions[K], undefined>
80
+ }
81
+
82
+ /**
83
+ * Crawlers that read the document without running streamed scripts, so they get the blocking render.
84
+ * The same set Next.js treats as HTML-limited bots. Googlebot itself is absent on purpose: it renders
85
+ * JavaScript. `[\w-]-Google` matches what `[\w-]+-Google` does without backtracking at every position.
86
+ */
87
+ export const HTML_LIMITED_BOTS: RegExp =
88
+ /[\w-]-Google|Google-[\w-]|Chrome-Lighthouse|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|Yeti|googleweblight/i
89
+
90
+ const MB = 1024 * 1024
91
+
92
+ export const PPR_DEFAULTS: ResolvedPprOptions = Object.freeze({
93
+ holeTimeoutMs: 5_000,
94
+ maxHoleTimeoutMs: 30_000,
95
+ totalDeadlineSlackMs: 1_000,
96
+ maxHolesPerRoute: 32,
97
+ shellEntriesPerRoute: 10_000,
98
+ shellStoreBytes: 64 * MB,
99
+ notFoundTtlSeconds: 60,
100
+ shellBuildConcurrency: 4,
101
+ shellAfterRequests: 2,
102
+ staleOnErrorFactor: 10,
103
+ maxStaleOnErrorSeconds: 86_400,
104
+ blockingFor: HTML_LIMITED_BOTS,
105
+ boundaryAbandonedTrip: 64,
106
+ processAbandonedTrip: 1_024,
107
+ admissionStop: 4_096,
108
+ abandonGraceMs: 100,
109
+ maxHoleWork: 1_024,
110
+ writeGraceMs: 5_000,
111
+ maxUnsentBytesPerResponse: MB,
112
+ maxUnsentBytes: 64 * MB,
113
+ frameBytes: 256 * 1024,
114
+ minShellBytes: 1_024,
115
+ holeHtmlValidation: "always",
116
+ cancelBeacon: true,
117
+ sharedRecheckMs: 1_000,
118
+ })
119
+
120
+ /**
121
+ * On Cloudflare Workers an isolate's memory is shared by everything it runs, and a beacon almost
122
+ * always reaches another isolate than the one running the holes.
123
+ */
124
+ const WORKERS_DEFAULTS: Partial<ResolvedPprOptions> = Object.freeze({
125
+ shellStoreBytes: 8 * MB,
126
+ cancelBeacon: false,
127
+ })
128
+
129
+ const onWorkers = (): boolean => globalThis.navigator?.userAgent === "Cloudflare-Workers"
130
+
131
+ type NumericKey = {
132
+ [K in keyof ResolvedPprOptions]: ResolvedPprOptions[K] extends number ? K : never
133
+ }[keyof ResolvedPprOptions]
134
+
135
+ const optionError = (message: string): TypeError => new TypeError(`[nifra/web] ppr: ${message}`)
136
+
137
+ function assertOrdered(options: ResolvedPprOptions, smaller: NumericKey, larger: NumericKey): void {
138
+ if (options[smaller] > options[larger]) {
139
+ throw optionError(
140
+ `${smaller} (${options[smaller]}) must not exceed ${larger} (${options[larger]})`,
141
+ )
142
+ }
143
+ }
144
+
145
+ /** The latest request a shell may wait for; the canary check requests a path this many times. */
146
+ export const MAX_SHELL_AFTER_REQUESTS = 8
147
+
148
+ /**
149
+ * Merge `overrides` over {@link PPR_DEFAULTS} and check the result. Unknown keys, non-integers and
150
+ * limits that contradict each other throw at startup, never on a request.
151
+ */
152
+ export function resolvePprOptions(overrides: PprOptions = {}): ResolvedPprOptions {
153
+ for (const key of Object.keys(overrides)) {
154
+ if (!Object.hasOwn(PPR_DEFAULTS, key)) throw optionError(`unknown option "${key}"`)
155
+ }
156
+ const defined = Object.fromEntries(
157
+ Object.entries(overrides).filter(([, value]) => value !== undefined),
158
+ )
159
+ const resolved: ResolvedPprOptions = {
160
+ ...PPR_DEFAULTS,
161
+ ...(onWorkers() ? WORKERS_DEFAULTS : undefined),
162
+ ...defined,
163
+ }
164
+ for (const [key, value] of Object.entries(resolved)) {
165
+ if (key === "blockingFor" || key === "holeHtmlValidation" || key === "cancelBeacon") continue
166
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
167
+ throw optionError(`${key} must be a non-negative integer, got ${String(value)}`)
168
+ }
169
+ }
170
+ for (const key of [
171
+ "holeTimeoutMs",
172
+ "maxHolesPerRoute",
173
+ "shellBuildConcurrency",
174
+ "shellAfterRequests",
175
+ "boundaryAbandonedTrip",
176
+ "maxHoleWork",
177
+ "maxUnsentBytesPerResponse",
178
+ "frameBytes",
179
+ ] as const) {
180
+ if (resolved[key] === 0) throw optionError(`${key} must be at least 1`)
181
+ }
182
+ if (resolved.shellAfterRequests > MAX_SHELL_AFTER_REQUESTS) {
183
+ throw optionError(
184
+ `shellAfterRequests must be at most ${MAX_SHELL_AFTER_REQUESTS}, got ${resolved.shellAfterRequests}`,
185
+ )
186
+ }
187
+ const blockingFor = resolved.blockingFor
188
+ if (!(blockingFor instanceof RegExp) && typeof blockingFor !== "function") {
189
+ throw optionError("blockingFor must be a RegExp or a function of the request")
190
+ }
191
+ if (resolved.holeHtmlValidation !== "always" && resolved.holeHtmlValidation !== "development") {
192
+ throw optionError(
193
+ `holeHtmlValidation must be "always" or "development", got ${String(resolved.holeHtmlValidation)}`,
194
+ )
195
+ }
196
+ if (typeof resolved.cancelBeacon !== "boolean") {
197
+ throw optionError(`cancelBeacon must be a boolean, got ${String(resolved.cancelBeacon)}`)
198
+ }
199
+ // A global or sticky pattern keeps `lastIndex` between `test` calls, so one bot would alternate.
200
+ if (blockingFor instanceof RegExp && (blockingFor.global || blockingFor.sticky)) {
201
+ throw optionError("blockingFor must not use the g or y flag")
202
+ }
203
+ assertOrdered(resolved, "holeTimeoutMs", "maxHoleTimeoutMs")
204
+ assertOrdered(resolved, "boundaryAbandonedTrip", "processAbandonedTrip")
205
+ assertOrdered(resolved, "processAbandonedTrip", "admissionStop")
206
+ assertOrdered(resolved, "frameBytes", "maxUnsentBytesPerResponse")
207
+ assertOrdered(resolved, "maxUnsentBytesPerResponse", "maxUnsentBytes")
208
+ return Object.freeze(resolved)
209
+ }
210
+
211
+ /** A route's total deadline: its largest hole timeout plus the slack, capped. */
212
+ export function totalDeadlineMs(
213
+ options: ResolvedPprOptions,
214
+ holeTimeouts: readonly number[],
215
+ ): number {
216
+ let largest = 0
217
+ for (const timeout of holeTimeouts) if (timeout > largest) largest = timeout
218
+ return Math.min(largest + options.totalDeadlineSlackMs, options.maxHoleTimeoutMs)
219
+ }
220
+
221
+ export { wantsBlockingRender } from "./contract.ts"
@@ -0,0 +1,77 @@
1
+ import type { BoundaryRegistration } from "../../boundary.ts"
2
+ import type { RouteEntry, RouteModule } from "../../manifest.ts"
3
+ import type { LoadedLayoutModules } from "../render-document.ts"
4
+ import { isHole, serverHoleComponent } from "./contract.ts"
5
+ import type { ResolvedPprOptions } from "./options.ts"
6
+
7
+ /**
8
+ * The declared variations `search` selects. A parameter given twice, or a value outside its set,
9
+ * selects nothing, so every spelling of one choice maps to one shell.
10
+ */
11
+ export function variationsOf(
12
+ declared: RouteModule["shellVariations"],
13
+ search: string,
14
+ ): Readonly<Record<string, string>> {
15
+ const lists = declared?.search
16
+ if (lists === undefined || search === "") return {}
17
+ const query = new URLSearchParams(search)
18
+ const chosen: Record<string, string> = {}
19
+ for (const [name, allowed] of Object.entries(lists)) {
20
+ const values = query.getAll(name)
21
+ const value = values[0]
22
+ if (values.length === 1 && value !== undefined && allowed.includes(value)) chosen[name] = value
23
+ }
24
+ return chosen
25
+ }
26
+
27
+ /** The PPR eligibility rules a route's own files cannot check: its layouts and boundary modes. */
28
+ export function assertPprRoute(
29
+ route: RouteEntry,
30
+ layouts: LoadedLayoutModules,
31
+ boundaries: readonly BoundaryRegistration[],
32
+ limits: ResolvedPprOptions,
33
+ ): void {
34
+ // `compileShells` refuses the same chain at startup; this catches a record made from other files.
35
+ layouts.forEach((layout, index) => {
36
+ if (layout.loader !== undefined && layout.gate !== true) {
37
+ throw new Error(
38
+ `[nifra/web] ppr: route "${route.id}" has a shellLoader, but its layout "${route.layoutIds[index]}" has a loader that is not a gate. A layout loader reads the request, so its output cannot be in a shared shell`,
39
+ )
40
+ }
41
+ })
42
+ let holes = 0
43
+ for (const boundary of boundaries) {
44
+ if (typeof boundary.mode === "object") {
45
+ throw new Error(
46
+ `[nifra/web] ppr: route "${route.id}" has a shellLoader and an intercepting boundary "${boundary.name}", which partial prerendering does not support yet`,
47
+ )
48
+ }
49
+ if (!isHole(boundary)) continue
50
+ holes++
51
+ if (typeof boundary.load !== "function") {
52
+ throw new Error(
53
+ `[nifra/web] ppr: boundary "${boundary.name}" of route "${route.id}" has no boundaryLoaders entry; a hole is filled from its own loader`,
54
+ )
55
+ }
56
+ if (
57
+ serverHoleComponent(boundary.render) !== undefined &&
58
+ (boundary.mode !== "dynamic" || boundary.interactive === true)
59
+ ) {
60
+ throw new Error(
61
+ `[nifra/web] ppr: boundary "${boundary.name}" of route "${route.id}" shows a server component, so it must be a dynamic hole that is not interactive: the component runs for each visitor, and its client islands hydrate`,
62
+ )
63
+ }
64
+ const { timeout } = boundary
65
+ const max = limits.maxHoleTimeoutMs
66
+ if (timeout !== undefined && !(Number.isInteger(timeout) && timeout >= 1 && timeout <= max)) {
67
+ throw new RangeError(
68
+ `[nifra/web] ppr: boundary "${boundary.name}" of route "${route.id}" has timeout ${String(timeout)}; it must be a whole number of milliseconds from 1 to ${max}`,
69
+ )
70
+ }
71
+ }
72
+ if (holes === 0 || holes > limits.maxHolesPerRoute) {
73
+ throw new RangeError(
74
+ `[nifra/web] ppr: route "${route.id}" has a shellLoader and ${holes} dynamic or shared boundaries; it needs 1 to ${limits.maxHolesPerRoute}. A page with no per-visitor or fast-changing part needs no shell: prerender it or give it \`revalidate\``,
75
+ )
76
+ }
77
+ }
@@ -0,0 +1,36 @@
1
+ import { hashSource } from "../../csp.ts"
2
+
3
+ /**
4
+ * The PPR client runtime: one inline script in the shell, constant per release, listed in the CSP by
5
+ * hash. It fills each hole host from that hole's inert JSON frame, keeps interactive holes' props for
6
+ * the adapter, with a server component's record and HTML for the islands inside it, and on leaving
7
+ * the route sends the response's cancel token. `put` is how an adapter refills a host from a data
8
+ * request's fill, through the same sink; it writes only into a host of the hole it names, so the
9
+ * global cannot carry a string into any other element under the policy.
10
+ *
11
+ * `__nifraPpr(name)` is the only entry point a frame calls, so every invoker is constant per hole name
12
+ * and hashable at build. Hole HTML goes in through `innerHTML`, where a `<script>` never runs, under a
13
+ * Trusted Types policy named `nifra-ppr` when the browser has the API. The server has validated that
14
+ * HTML before framing it; the policy only names the one sink.
15
+ */
16
+ export const PPR_RUNTIME =
17
+ '(()=>{var w=window,d=document,h={},s={},dead=0,p;var pol=function(){if(!p){try{p=w.trustedTypes?w.trustedTypes.createPolicy("nifra-ppr",{createHTML:function(x){return x}}):null}catch(_){}p=p||{createHTML:function(x){return x}}}return p};var ap=function(t,n,v){t.innerHTML=pol().createHTML(typeof v.html==="string"?v.html:"");t.setAttribute("data-nifra-filled","");t.setAttribute("aria-busy","false");if(v.error)t.setAttribute("data-nifra-hole-error","");else t.removeAttribute("data-nifra-hole-error");var o={props:v.props,error:!!v.error};if(v.component){o.component=v.component;o.html=v.html}h[n]=o;return o};var f=function(n){if(dead)return;var e=d.getElementById("nifra-ppr:"+n);if(!e)return;var v;try{v=JSON.parse(e.textContent)}catch(_){return}e.remove();var t=d.querySelector(\'[data-nifra-hole="\'+n+\'"]\');if(!t||t.hasAttribute("data-nifra-filled"))return;var o=ap(t,n,v);(s[n]||[]).slice().forEach(function(c){c(o)})};f.put=function(n,v,t){if(v&&typeof v==="object"&&t&&t.getAttribute&&t.getAttribute("data-nifra-hole")===n)ap(t,n,v)};f.get=function(n){return h[n]};f.subscribe=function(n,c){(s[n]||(s[n]=[])).push(c);return function(){s[n]=(s[n]||[]).filter(function(x){return x!==c})}};f.leave=function(){if(dead)return;dead=1;s={};var r=d.getElementById("nifra-ppr-req");if(!r)return;var q;try{q=JSON.parse(r.textContent)}catch(_){return}if(q.cancel&&q.holes.some(function(n){return!h[n]})&&navigator.sendBeacon)navigator.sendBeacon(q.cancel,q.token)};f.dead=function(){return!!dead};f.boot=function(){var c=d.currentScript,e=c&&c.previousElementSibling;if(e&&e.rel==="modulepreload"&&e.href)import(e.href)};w.__nifraPpr=f})()'
18
+
19
+ /**
20
+ * What loads each module script of a shell, after a `<link rel="modulepreload">` naming it. WebKit
21
+ * evaluates a module script, `async` or not, only once the parser finishes, which on a streamed page
22
+ * is after the slowest hole, and a dynamic import of the same URL waits with it. A dynamic import
23
+ * from a classic script with no such element evaluates at once in every engine.
24
+ */
25
+ export const PPR_BOOT = "__nifraPpr.boot()"
26
+
27
+ /** The script a frame runs to fill hole `name`. Constant per name, so it is listed by hash. */
28
+ export const holeInvoker = (name: string): string => `__nifraPpr(${JSON.stringify(name)})`
29
+
30
+ /**
31
+ * The `script-src` hash sources a PPR route needs: the runtime, the boot call and one invoker per hole. Computed once
32
+ * per route at startup, never per request.
33
+ */
34
+ export async function pprScriptHashes(holeNames: readonly string[]): Promise<readonly string[]> {
35
+ return Promise.all([PPR_RUNTIME, PPR_BOOT, ...holeNames.map(holeInvoker)].map(hashSource))
36
+ }