@nifrajs/web 5.5.1 → 5.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. package/CHANGELOG.md +243 -0
  2. package/README.md +21 -11
  3. package/dist/boundary.d.ts +67 -7
  4. package/dist/boundary.d.ts.map +1 -1
  5. package/dist/boundary.js +6 -4
  6. package/dist/boundary.js.map +1 -1
  7. package/dist/build-plan.d.ts +3 -0
  8. package/dist/build-plan.d.ts.map +1 -1
  9. package/dist/build-plan.js.map +1 -1
  10. package/dist/build-vite.d.ts.map +1 -1
  11. package/dist/build-vite.js +2 -0
  12. package/dist/build-vite.js.map +1 -1
  13. package/dist/build.d.ts +9 -2
  14. package/dist/build.d.ts.map +1 -1
  15. package/dist/build.js +10 -0
  16. package/dist/build.js.map +1 -1
  17. package/dist/client.d.ts +3 -1
  18. package/dist/client.d.ts.map +1 -1
  19. package/dist/client.js +174 -23
  20. package/dist/client.js.map +1 -1
  21. package/dist/csp.d.ts +8 -0
  22. package/dist/csp.d.ts.map +1 -1
  23. package/dist/csp.js +11 -2
  24. package/dist/csp.js.map +1 -1
  25. package/dist/deferred.d.ts +6 -4
  26. package/dist/deferred.d.ts.map +1 -1
  27. package/dist/deferred.js +10 -6
  28. package/dist/deferred.js.map +1 -1
  29. package/dist/dev-feed.d.ts.map +1 -1
  30. package/dist/dev-feed.js +11 -2
  31. package/dist/dev-feed.js.map +1 -1
  32. package/dist/dev-session.d.ts.map +1 -1
  33. package/dist/dev-session.js +13 -6
  34. package/dist/dev-session.js.map +1 -1
  35. package/dist/dev.d.ts +4 -1
  36. package/dist/dev.d.ts.map +1 -1
  37. package/dist/dev.js +12 -5
  38. package/dist/dev.js.map +1 -1
  39. package/dist/diagnostic-catalog.d.ts +4 -0
  40. package/dist/diagnostic-catalog.d.ts.map +1 -1
  41. package/dist/diagnostic-catalog.js +48 -0
  42. package/dist/diagnostic-catalog.js.map +1 -1
  43. package/dist/diagnostic.d.ts +1 -1
  44. package/dist/diagnostic.d.ts.map +1 -1
  45. package/dist/diagnostic.js +1 -1
  46. package/dist/diagnostic.js.map +1 -1
  47. package/dist/fn.d.ts +5 -3
  48. package/dist/fn.d.ts.map +1 -1
  49. package/dist/fn.js +20 -5
  50. package/dist/fn.js.map +1 -1
  51. package/dist/fs.d.ts.map +1 -1
  52. package/dist/fs.js +13 -3
  53. package/dist/fs.js.map +1 -1
  54. package/dist/identity.d.ts +22 -0
  55. package/dist/identity.d.ts.map +1 -0
  56. package/dist/identity.js +60 -0
  57. package/dist/identity.js.map +1 -0
  58. package/dist/index.d.ts +8 -6
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +6 -3
  61. package/dist/index.js.map +1 -1
  62. package/dist/internal/cdn/cloudflare.d.ts +2 -1
  63. package/dist/internal/cdn/cloudflare.d.ts.map +1 -1
  64. package/dist/internal/cdn/cloudflare.js +2 -1
  65. package/dist/internal/cdn/cloudflare.js.map +1 -1
  66. package/dist/internal/certified-components.d.ts +1 -1
  67. package/dist/internal/certified-components.js +1 -1
  68. package/dist/internal/codegen.d.ts +11 -10
  69. package/dist/internal/codegen.d.ts.map +1 -1
  70. package/dist/internal/codegen.js +73 -10
  71. package/dist/internal/codegen.js.map +1 -1
  72. package/dist/internal/dev-websocket.d.ts +73 -0
  73. package/dist/internal/dev-websocket.d.ts.map +1 -0
  74. package/dist/internal/dev-websocket.js +145 -0
  75. package/dist/internal/dev-websocket.js.map +1 -0
  76. package/dist/internal/hole-runtime.d.ts +66 -0
  77. package/dist/internal/hole-runtime.d.ts.map +1 -0
  78. package/dist/internal/hole-runtime.js +211 -0
  79. package/dist/internal/hole-runtime.js.map +1 -0
  80. package/dist/internal/identity-epoch.d.ts +50 -0
  81. package/dist/internal/identity-epoch.d.ts.map +1 -0
  82. package/dist/internal/identity-epoch.js +180 -0
  83. package/dist/internal/identity-epoch.js.map +1 -0
  84. package/dist/internal/invalidation-contract.d.ts +23 -0
  85. package/dist/internal/invalidation-contract.d.ts.map +1 -0
  86. package/dist/internal/invalidation-contract.js +11 -0
  87. package/dist/internal/invalidation-contract.js.map +1 -0
  88. package/dist/internal/invalidation.d.ts +62 -0
  89. package/dist/internal/invalidation.d.ts.map +1 -0
  90. package/dist/internal/invalidation.js +149 -0
  91. package/dist/internal/invalidation.js.map +1 -0
  92. package/dist/internal/island-bindings.d.ts +51 -0
  93. package/dist/internal/island-bindings.d.ts.map +1 -0
  94. package/dist/internal/island-bindings.js +147 -0
  95. package/dist/internal/island-bindings.js.map +1 -0
  96. package/dist/internal/island-list.d.ts +26 -0
  97. package/dist/internal/island-list.d.ts.map +1 -0
  98. package/dist/internal/island-list.js +78 -0
  99. package/dist/internal/island-list.js.map +1 -0
  100. package/dist/internal/island-mount.d.ts +4 -0
  101. package/dist/internal/island-mount.d.ts.map +1 -0
  102. package/dist/internal/island-mount.js +66 -0
  103. package/dist/internal/island-mount.js.map +1 -0
  104. package/dist/internal/island-owner.d.ts +5 -0
  105. package/dist/internal/island-owner.d.ts.map +1 -0
  106. package/dist/internal/island-owner.js +20 -0
  107. package/dist/internal/island-owner.js.map +1 -0
  108. package/dist/internal/island-resource.d.ts +30 -0
  109. package/dist/internal/island-resource.d.ts.map +1 -0
  110. package/dist/internal/island-resource.js +57 -0
  111. package/dist/internal/island-resource.js.map +1 -0
  112. package/dist/internal/island-signals.d.ts +51 -0
  113. package/dist/internal/island-signals.d.ts.map +1 -0
  114. package/dist/internal/island-signals.js +220 -0
  115. package/dist/internal/island-signals.js.map +1 -0
  116. package/dist/internal/keep-alive-table.d.ts +23 -0
  117. package/dist/internal/keep-alive-table.d.ts.map +1 -0
  118. package/dist/internal/keep-alive-table.js +53 -0
  119. package/dist/internal/keep-alive-table.js.map +1 -0
  120. package/dist/internal/live-contract.d.ts +101 -0
  121. package/dist/internal/live-contract.d.ts.map +1 -0
  122. package/dist/internal/live-contract.js +5 -0
  123. package/dist/internal/live-contract.js.map +1 -0
  124. package/dist/internal/live-queries.d.ts +22 -0
  125. package/dist/internal/live-queries.d.ts.map +1 -0
  126. package/dist/internal/live-queries.js +54 -0
  127. package/dist/internal/live-queries.js.map +1 -0
  128. package/dist/internal/page-events.d.ts +10 -0
  129. package/dist/internal/page-events.d.ts.map +1 -0
  130. package/dist/internal/page-events.js +26 -0
  131. package/dist/internal/page-events.js.map +1 -0
  132. package/dist/internal/page-execution.d.ts +16 -1
  133. package/dist/internal/page-execution.d.ts.map +1 -1
  134. package/dist/internal/page-execution.js +432 -31
  135. package/dist/internal/page-execution.js.map +1 -1
  136. package/dist/internal/parity.d.ts +3 -0
  137. package/dist/internal/parity.d.ts.map +1 -1
  138. package/dist/internal/parity.js +28 -5
  139. package/dist/internal/parity.js.map +1 -1
  140. package/dist/internal/ppr/admission.d.ts +83 -0
  141. package/dist/internal/ppr/admission.d.ts.map +1 -0
  142. package/dist/internal/ppr/admission.js +221 -0
  143. package/dist/internal/ppr/admission.js.map +1 -0
  144. package/dist/internal/ppr/canary.d.ts +24 -0
  145. package/dist/internal/ppr/canary.d.ts.map +1 -0
  146. package/dist/internal/ppr/canary.js +102 -0
  147. package/dist/internal/ppr/canary.js.map +1 -0
  148. package/dist/internal/ppr/compile-shell.d.ts +39 -0
  149. package/dist/internal/ppr/compile-shell.d.ts.map +1 -0
  150. package/dist/internal/ppr/compile-shell.js +298 -0
  151. package/dist/internal/ppr/compile-shell.js.map +1 -0
  152. package/dist/internal/ppr/contract.d.ts +177 -0
  153. package/dist/internal/ppr/contract.d.ts.map +1 -0
  154. package/dist/internal/ppr/contract.js +80 -0
  155. package/dist/internal/ppr/contract.js.map +1 -0
  156. package/dist/internal/ppr/frames.d.ts +71 -0
  157. package/dist/internal/ppr/frames.d.ts.map +1 -0
  158. package/dist/internal/ppr/frames.js +119 -0
  159. package/dist/internal/ppr/frames.js.map +1 -0
  160. package/dist/internal/ppr/integration.d.ts +19 -0
  161. package/dist/internal/ppr/integration.d.ts.map +1 -0
  162. package/dist/internal/ppr/integration.js +106 -0
  163. package/dist/internal/ppr/integration.js.map +1 -0
  164. package/dist/internal/ppr/options.d.ts +96 -0
  165. package/dist/internal/ppr/options.d.ts.map +1 -0
  166. package/dist/internal/ppr/options.js +125 -0
  167. package/dist/internal/ppr/options.js.map +1 -0
  168. package/dist/internal/ppr/route-rules.d.ts +12 -0
  169. package/dist/internal/ppr/route-rules.d.ts.map +1 -0
  170. package/dist/internal/ppr/route-rules.js +53 -0
  171. package/dist/internal/ppr/route-rules.js.map +1 -0
  172. package/dist/internal/ppr/runtime.d.ts +29 -0
  173. package/dist/internal/ppr/runtime.d.ts.map +1 -0
  174. package/dist/internal/ppr/runtime.js +35 -0
  175. package/dist/internal/ppr/runtime.js.map +1 -0
  176. package/dist/internal/ppr/server.d.ts +29 -0
  177. package/dist/internal/ppr/server.d.ts.map +1 -0
  178. package/dist/internal/ppr/server.js +1409 -0
  179. package/dist/internal/ppr/server.js.map +1 -0
  180. package/dist/internal/ppr/shell-store.d.ts +39 -0
  181. package/dist/internal/ppr/shell-store.d.ts.map +1 -0
  182. package/dist/internal/ppr/shell-store.js +106 -0
  183. package/dist/internal/ppr/shell-store.js.map +1 -0
  184. package/dist/internal/prerendered-pages.d.ts +1 -1
  185. package/dist/internal/prerendered-pages.d.ts.map +1 -1
  186. package/dist/internal/prerendered-pages.js +7 -4
  187. package/dist/internal/prerendered-pages.js.map +1 -1
  188. package/dist/internal/present.d.ts +11 -0
  189. package/dist/internal/present.d.ts.map +1 -0
  190. package/dist/internal/present.js +13 -0
  191. package/dist/internal/present.js.map +1 -0
  192. package/dist/internal/query-digest.d.ts +9 -0
  193. package/dist/internal/query-digest.d.ts.map +1 -0
  194. package/dist/internal/query-digest.js +23 -0
  195. package/dist/internal/query-digest.js.map +1 -0
  196. package/dist/internal/query-runtime.d.ts +6 -1
  197. package/dist/internal/query-runtime.d.ts.map +1 -1
  198. package/dist/internal/query-runtime.js +9 -0
  199. package/dist/internal/query-runtime.js.map +1 -1
  200. package/dist/internal/query-seam.d.ts +29 -0
  201. package/dist/internal/query-seam.d.ts.map +1 -0
  202. package/dist/internal/query-seam.js +23 -0
  203. package/dist/internal/query-seam.js.map +1 -0
  204. package/dist/internal/render-document.d.ts +34 -64
  205. package/dist/internal/render-document.d.ts.map +1 -1
  206. package/dist/internal/render-document.js +82 -64
  207. package/dist/internal/render-document.js.map +1 -1
  208. package/dist/internal/request-abort.d.ts +8 -0
  209. package/dist/internal/request-abort.d.ts.map +1 -0
  210. package/dist/internal/request-abort.js +22 -0
  211. package/dist/internal/request-abort.js.map +1 -0
  212. package/dist/internal/server-boundary.d.ts +2 -0
  213. package/dist/internal/server-boundary.d.ts.map +1 -1
  214. package/dist/internal/server-boundary.js +8 -1
  215. package/dist/internal/server-boundary.js.map +1 -1
  216. package/dist/internal/share-data.d.ts +10 -0
  217. package/dist/internal/share-data.d.ts.map +1 -0
  218. package/dist/internal/share-data.js +135 -0
  219. package/dist/internal/share-data.js.map +1 -0
  220. package/dist/internal/status-page.d.ts +68 -0
  221. package/dist/internal/status-page.d.ts.map +1 -0
  222. package/dist/internal/status-page.js +66 -0
  223. package/dist/internal/status-page.js.map +1 -0
  224. package/dist/internal/store-signature.d.ts +15 -0
  225. package/dist/internal/store-signature.d.ts.map +1 -0
  226. package/dist/internal/store-signature.js +114 -0
  227. package/dist/internal/store-signature.js.map +1 -0
  228. package/dist/internal/vite-plugin-compat.d.ts +2 -2
  229. package/dist/internal/vite-plugin-compat.js +2 -2
  230. package/dist/internal/web-app.d.ts +26 -0
  231. package/dist/internal/web-app.d.ts.map +1 -1
  232. package/dist/internal/web-app.js +79 -3
  233. package/dist/internal/web-app.js.map +1 -1
  234. package/dist/islands-swap.d.ts +41 -0
  235. package/dist/islands-swap.d.ts.map +1 -0
  236. package/dist/islands-swap.js +270 -0
  237. package/dist/islands-swap.js.map +1 -0
  238. package/dist/islands.d.ts +86 -24
  239. package/dist/islands.d.ts.map +1 -1
  240. package/dist/islands.js +138 -78
  241. package/dist/islands.js.map +1 -1
  242. package/dist/isr.d.ts +67 -13
  243. package/dist/isr.d.ts.map +1 -1
  244. package/dist/isr.js +247 -99
  245. package/dist/isr.js.map +1 -1
  246. package/dist/keep-alive.d.ts +147 -0
  247. package/dist/keep-alive.d.ts.map +1 -0
  248. package/dist/keep-alive.js +1102 -0
  249. package/dist/keep-alive.js.map +1 -0
  250. package/dist/live/bus.d.ts +70 -0
  251. package/dist/live/bus.d.ts.map +1 -0
  252. package/dist/live/bus.js +55 -0
  253. package/dist/live/bus.js.map +1 -0
  254. package/dist/live/core.d.ts +25 -0
  255. package/dist/live/core.d.ts.map +1 -0
  256. package/dist/live/core.js +29 -0
  257. package/dist/live/core.js.map +1 -0
  258. package/dist/live/history.d.ts +38 -0
  259. package/dist/live/history.d.ts.map +1 -0
  260. package/dist/live/history.js +104 -0
  261. package/dist/live/history.js.map +1 -0
  262. package/dist/live/hub-protocol.d.ts +46 -0
  263. package/dist/live/hub-protocol.d.ts.map +1 -0
  264. package/dist/live/hub-protocol.js +108 -0
  265. package/dist/live/hub-protocol.js.map +1 -0
  266. package/dist/live/hub.d.ts +185 -0
  267. package/dist/live/hub.d.ts.map +1 -0
  268. package/dist/live/hub.js +544 -0
  269. package/dist/live/hub.js.map +1 -0
  270. package/dist/live/keyring.d.ts +92 -0
  271. package/dist/live/keyring.d.ts.map +1 -0
  272. package/dist/live/keyring.js +265 -0
  273. package/dist/live/keyring.js.map +1 -0
  274. package/dist/live/read-capped.d.ts +6 -0
  275. package/dist/live/read-capped.d.ts.map +1 -0
  276. package/dist/live/read-capped.js +42 -0
  277. package/dist/live/read-capped.js.map +1 -0
  278. package/dist/live/revocations.d.ts +52 -0
  279. package/dist/live/revocations.d.ts.map +1 -0
  280. package/dist/live/revocations.js +116 -0
  281. package/dist/live/revocations.js.map +1 -0
  282. package/dist/live/settings.d.ts +14 -0
  283. package/dist/live/settings.d.ts.map +1 -0
  284. package/dist/live/settings.js +38 -0
  285. package/dist/live/settings.js.map +1 -0
  286. package/dist/live-client.d.ts +34 -0
  287. package/dist/live-client.d.ts.map +1 -0
  288. package/dist/live-client.js +878 -0
  289. package/dist/live-client.js.map +1 -0
  290. package/dist/live-hub.d.ts +130 -0
  291. package/dist/live-hub.d.ts.map +1 -0
  292. package/dist/live-hub.js +507 -0
  293. package/dist/live-hub.js.map +1 -0
  294. package/dist/live-redis.d.ts +60 -0
  295. package/dist/live-redis.d.ts.map +1 -0
  296. package/dist/live-redis.js +212 -0
  297. package/dist/live-redis.js.map +1 -0
  298. package/dist/live.d.ts +150 -0
  299. package/dist/live.d.ts.map +1 -0
  300. package/dist/live.js +972 -0
  301. package/dist/live.js.map +1 -0
  302. package/dist/manifest.d.ts +113 -2
  303. package/dist/manifest.d.ts.map +1 -1
  304. package/dist/manifest.js +109 -5
  305. package/dist/manifest.js.map +1 -1
  306. package/dist/navigation.d.ts +9 -3
  307. package/dist/navigation.d.ts.map +1 -1
  308. package/dist/navigation.js +13 -3
  309. package/dist/navigation.js.map +1 -1
  310. package/dist/plugins/vite-zone-guard.d.ts +6 -0
  311. package/dist/plugins/vite-zone-guard.d.ts.map +1 -1
  312. package/dist/plugins/vite-zone-guard.js +8 -2
  313. package/dist/plugins/vite-zone-guard.js.map +1 -1
  314. package/dist/ppr.d.ts +73 -0
  315. package/dist/ppr.d.ts.map +1 -0
  316. package/dist/ppr.js +76 -0
  317. package/dist/ppr.js.map +1 -0
  318. package/dist/query.d.ts +8 -13
  319. package/dist/query.d.ts.map +1 -1
  320. package/dist/query.js +112 -18
  321. package/dist/query.js.map +1 -1
  322. package/dist/render-seam.d.ts +32 -0
  323. package/dist/render-seam.d.ts.map +1 -1
  324. package/dist/render-seam.js +7 -0
  325. package/dist/render-seam.js.map +1 -1
  326. package/dist/route-types.d.ts.map +1 -1
  327. package/dist/route-types.js +2 -2
  328. package/dist/route-types.js.map +1 -1
  329. package/dist/router.d.ts +72 -4
  330. package/dist/router.d.ts.map +1 -1
  331. package/dist/router.js +416 -129
  332. package/dist/router.js.map +1 -1
  333. package/dist/server-components/build.d.ts.map +1 -1
  334. package/dist/server-components/build.js +3 -3
  335. package/dist/server-components/build.js.map +1 -1
  336. package/dist/server-components/client.d.ts +23 -1
  337. package/dist/server-components/client.d.ts.map +1 -1
  338. package/dist/server-components/client.js +92 -5
  339. package/dist/server-components/client.js.map +1 -1
  340. package/dist/server-components/definition.d.ts.map +1 -1
  341. package/dist/server-components/definition.js +2 -1
  342. package/dist/server-components/definition.js.map +1 -1
  343. package/dist/server-components/hole.d.ts +8 -0
  344. package/dist/server-components/hole.d.ts.map +1 -0
  345. package/dist/server-components/hole.js +20 -0
  346. package/dist/server-components/hole.js.map +1 -0
  347. package/dist/server-components/html-policy.d.ts.map +1 -1
  348. package/dist/server-components/html-policy.js +62 -4
  349. package/dist/server-components/html-policy.js.map +1 -1
  350. package/dist/server-components/html-scan.d.ts +17 -0
  351. package/dist/server-components/html-scan.d.ts.map +1 -0
  352. package/dist/server-components/html-scan.js +180 -0
  353. package/dist/server-components/html-scan.js.map +1 -0
  354. package/dist/server-components/html.d.ts.map +1 -1
  355. package/dist/server-components/html.js +19 -4
  356. package/dist/server-components/html.js.map +1 -1
  357. package/dist/server-components/router-integration.d.ts +4 -1
  358. package/dist/server-components/router-integration.d.ts.map +1 -1
  359. package/dist/server-components/router-integration.js +23 -2
  360. package/dist/server-components/router-integration.js.map +1 -1
  361. package/dist/server-components/runtime.d.ts +6 -1
  362. package/dist/server-components/runtime.d.ts.map +1 -1
  363. package/dist/server-components/runtime.js +88 -29
  364. package/dist/server-components/runtime.js.map +1 -1
  365. package/dist/server-components/state.d.ts +3 -1
  366. package/dist/server-components/state.d.ts.map +1 -1
  367. package/dist/server-components/state.js +50 -38
  368. package/dist/server-components/state.js.map +1 -1
  369. package/dist/server-components/types.d.ts +9 -0
  370. package/dist/server-components/types.d.ts.map +1 -1
  371. package/dist/server-components/vite.d.ts.map +1 -1
  372. package/dist/server-components/vite.js +26 -9
  373. package/dist/server-components/vite.js.map +1 -1
  374. package/dist/vite.d.ts +5 -1
  375. package/dist/vite.d.ts.map +1 -1
  376. package/dist/vite.js +58 -4
  377. package/dist/vite.js.map +1 -1
  378. package/package.json +48 -8
  379. package/src/boundary.ts +77 -5
  380. package/src/build-plan.ts +3 -0
  381. package/src/build-vite.ts +2 -0
  382. package/src/build.ts +20 -1
  383. package/src/client.ts +183 -23
  384. package/src/csp.ts +16 -2
  385. package/src/deferred.ts +11 -5
  386. package/src/dev-feed.ts +11 -2
  387. package/src/dev-session.ts +22 -11
  388. package/src/dev.ts +29 -8
  389. package/src/diagnostic-catalog.ts +53 -0
  390. package/src/diagnostic.ts +1 -0
  391. package/src/fn.ts +24 -7
  392. package/src/fs.ts +22 -8
  393. package/src/identity.ts +61 -0
  394. package/src/index.ts +20 -4
  395. package/src/internal/cdn/cloudflare.ts +2 -1
  396. package/src/internal/certified-components.ts +1 -1
  397. package/src/internal/codegen.ts +95 -9
  398. package/src/internal/dev-websocket.ts +230 -0
  399. package/src/internal/hole-runtime.ts +286 -0
  400. package/src/internal/identity-epoch.ts +239 -0
  401. package/src/internal/invalidation-contract.ts +35 -0
  402. package/src/internal/invalidation.ts +213 -0
  403. package/src/internal/island-bindings.ts +191 -0
  404. package/src/internal/island-list.ts +92 -0
  405. package/src/internal/island-mount.ts +68 -0
  406. package/src/internal/island-owner.ts +20 -0
  407. package/src/internal/island-resource.ts +76 -0
  408. package/src/internal/island-signals.ts +249 -0
  409. package/src/internal/keep-alive-table.ts +72 -0
  410. package/src/internal/live-contract.ts +111 -0
  411. package/src/internal/live-queries.ts +60 -0
  412. package/src/internal/page-events.ts +27 -0
  413. package/src/internal/page-execution.ts +605 -45
  414. package/src/internal/parity.ts +35 -6
  415. package/src/internal/ppr/admission.ts +300 -0
  416. package/src/internal/ppr/canary.ts +122 -0
  417. package/src/internal/ppr/compile-shell.ts +379 -0
  418. package/src/internal/ppr/contract.ts +265 -0
  419. package/src/internal/ppr/frames.ts +170 -0
  420. package/src/internal/ppr/integration.ts +137 -0
  421. package/src/internal/ppr/options.ts +221 -0
  422. package/src/internal/ppr/route-rules.ts +77 -0
  423. package/src/internal/ppr/runtime.ts +39 -0
  424. package/src/internal/ppr/server.ts +1766 -0
  425. package/src/internal/ppr/shell-store.ts +152 -0
  426. package/src/internal/prerendered-pages.ts +7 -4
  427. package/src/internal/present.ts +20 -0
  428. package/src/internal/query-digest.ts +34 -0
  429. package/src/internal/query-runtime.ts +14 -1
  430. package/src/internal/query-seam.ts +48 -0
  431. package/src/internal/render-document.ts +158 -113
  432. package/src/internal/request-abort.ts +26 -0
  433. package/src/internal/server-boundary.ts +10 -1
  434. package/src/internal/share-data.ts +136 -0
  435. package/src/internal/status-page.ts +95 -0
  436. package/src/internal/store-signature.ts +149 -0
  437. package/src/internal/vite-plugin-compat.ts +2 -2
  438. package/src/internal/web-app.ts +123 -3
  439. package/src/islands-swap.ts +283 -0
  440. package/src/islands.ts +169 -72
  441. package/src/isr.ts +348 -106
  442. package/src/keep-alive.ts +1354 -0
  443. package/src/live/bus.ts +104 -0
  444. package/src/live/core.ts +52 -0
  445. package/src/live/history.ts +109 -0
  446. package/src/live/hub-protocol.ts +133 -0
  447. package/src/live/hub.ts +620 -0
  448. package/src/live/keyring.ts +384 -0
  449. package/src/live/read-capped.ts +36 -0
  450. package/src/live/revocations.ts +155 -0
  451. package/src/live/settings.ts +46 -0
  452. package/src/live-client.ts +1011 -0
  453. package/src/live-hub.ts +658 -0
  454. package/src/live-redis.ts +284 -0
  455. package/src/live.ts +1257 -0
  456. package/src/manifest.ts +274 -6
  457. package/src/navigation.ts +14 -2
  458. package/src/plugins/vite-zone-guard.ts +15 -2
  459. package/src/ppr.ts +153 -0
  460. package/src/query.ts +147 -20
  461. package/src/render-seam.ts +37 -0
  462. package/src/route-types.ts +3 -1
  463. package/src/router.ts +534 -132
  464. package/src/server-components/build.ts +9 -3
  465. package/src/server-components/client.ts +89 -5
  466. package/src/server-components/definition.ts +2 -1
  467. package/src/server-components/hole.ts +23 -0
  468. package/src/server-components/html-policy.ts +63 -4
  469. package/src/server-components/html-scan.ts +159 -0
  470. package/src/server-components/html.ts +18 -4
  471. package/src/server-components/router-integration.ts +21 -2
  472. package/src/server-components/runtime.ts +114 -31
  473. package/src/server-components/state.ts +66 -44
  474. package/src/server-components/types.ts +16 -0
  475. package/src/server-components/vite.ts +33 -14
  476. package/src/vite.ts +75 -6
  477. package/dist/nano.d.ts +0 -125
  478. package/dist/nano.d.ts.map +0 -1
  479. package/dist/nano.js +0 -190
  480. package/dist/nano.js.map +0 -1
  481. package/src/nano.ts +0 -246
@@ -0,0 +1,213 @@
1
+ /**
2
+ * One invalidation pipeline for everything a write makes stale, in a fixed order: data caches (tags),
3
+ * then the page store (paths and tags), then the live publish (keys and tags), then the CDN. A failure
4
+ * in either cache stage stops before the publish: clients told to reload would read the stale data and
5
+ * move their watermarks past the change. Every stage is idempotent, so a retry repeats from the stage
6
+ * that failed.
7
+ *
8
+ * `withISR` owns the pipeline when it wraps the app; without it, `createWebApp` runs the publish
9
+ * stage alone. Never both.
10
+ */
11
+ import type { CacheStore, CdnPurgeTarget, ISRPlatform } from "../isr.ts"
12
+ import type { InvalidationOp } from "./invalidation-contract.ts"
13
+
14
+ export type { InvalidationOp } from "./invalidation-contract.ts"
15
+
16
+ export type InvalidationStage = "data" | "pages" | "publish" | "cdn"
17
+
18
+ export interface StageReport {
19
+ readonly stage: InvalidationStage
20
+ readonly ok: boolean
21
+ readonly retryable: boolean
22
+ readonly error?: string
23
+ /** The CDN took the purge but has not applied it yet (rate limit or retry). */
24
+ readonly queued?: true
25
+ }
26
+
27
+ export interface InvalidationReport {
28
+ readonly id: string
29
+ readonly ok: boolean
30
+ readonly stages: readonly StageReport[]
31
+ }
32
+
33
+ /** What the pipeline calls on a page store: any {@link CacheStore} fits. */
34
+ export interface PagePurge {
35
+ delete(key: string): Promise<void>
36
+ readonly invalidateTag?: ((tag: string) => Promise<void>) | undefined
37
+ }
38
+
39
+ export interface InvalidationTargets {
40
+ readonly dataCaches?: readonly { invalidateTag(tag: string): unknown }[] | undefined
41
+ readonly store?: PagePurge | undefined
42
+ /** The page store key of a path, as the cache wrapper computes it; `null` when never stored. */
43
+ readonly keyOf?: ((path: string, req: Request) => string | null) | undefined
44
+ readonly publish?: ((keys: readonly string[]) => Promise<void>) | undefined
45
+ readonly cdn?: CdnPurgeTarget | undefined
46
+ }
47
+
48
+ const ORDER: readonly InvalidationStage[] = ["data", "pages", "publish", "cdn"]
49
+ /** Retry delays after a retryable failure; inline retries stop at {@link INLINE_DEADLINE_MS}. */
50
+ const RETRY_MS = [200, 600, 1800]
51
+ export const INLINE_DEADLINE_MS = 2000
52
+
53
+ /** Run the stages from `from` once. */
54
+ export async function runInvalidation(
55
+ op: InvalidationOp,
56
+ targets: InvalidationTargets,
57
+ req: Request,
58
+ platform: ISRPlatform | undefined,
59
+ from: InvalidationStage = "data",
60
+ ): Promise<InvalidationReport> {
61
+ const stages: StageReport[] = []
62
+ for (const stage of ORDER.slice(ORDER.indexOf(from))) {
63
+ // Decided before any await: a stage with nothing to do costs no turn of the event loop.
64
+ const running = startStage(stage, op, targets, req, platform)
65
+ if (running === undefined) continue
66
+ const report = await running
67
+ stages.push(report)
68
+ // A cache stage that failed leaves data a reload would read: nothing after it may run.
69
+ if (!report.ok && (stage === "data" || stage === "pages")) break
70
+ }
71
+ return { id: op.id, ok: stages.every((report) => report.ok), stages }
72
+ }
73
+
74
+ /** Start a stage's work for this operation; `undefined` when it has none. Never rejects. */
75
+ function startStage(
76
+ stage: InvalidationStage,
77
+ op: InvalidationOp,
78
+ targets: InvalidationTargets,
79
+ req: Request,
80
+ platform: ISRPlatform | undefined,
81
+ ): Promise<StageReport> | undefined {
82
+ const done: StageReport = { stage, ok: true, retryable: false }
83
+ let work: Promise<StageReport>
84
+ switch (stage) {
85
+ case "data": {
86
+ const caches = targets.dataCaches
87
+ if (op.tags.length === 0 || caches === undefined) return undefined
88
+ work = (async () => {
89
+ for (const cache of caches) {
90
+ for (const tag of op.tags) await cache.invalidateTag(tag)
91
+ }
92
+ return done
93
+ })()
94
+ break
95
+ }
96
+ case "pages": {
97
+ const store = targets.store
98
+ if (store === undefined || op.paths.length + op.tags.length === 0) return undefined
99
+ work = (async (): Promise<StageReport> => {
100
+ if (op.tags.length > 0 && store.invalidateTag === undefined) {
101
+ return { stage, ok: false, retryable: false, error: "tag_invalidation_not_supported" }
102
+ }
103
+ for (const path of op.paths) {
104
+ const key = targets.keyOf?.(path, req) ?? null
105
+ if (key !== null) await store.delete(key)
106
+ }
107
+ for (const tag of op.tags) await store.invalidateTag?.(tag)
108
+ return done
109
+ })()
110
+ break
111
+ }
112
+ case "publish": {
113
+ const publish = targets.publish
114
+ if (publish === undefined || op.keys.length + op.tags.length === 0) return undefined
115
+ work = (async () => {
116
+ await publish([...new Set([...op.keys, ...op.tags])])
117
+ return done
118
+ })()
119
+ break
120
+ }
121
+ case "cdn": {
122
+ const cdn = targets.cdn
123
+ if (cdn === undefined || op.paths.length + op.tags.length === 0) return undefined
124
+ work = (async (): Promise<StageReport> => {
125
+ const outcome = await cdn.purge({ tags: op.tags, paths: op.paths }, platform)
126
+ return outcome.cdn === "failed"
127
+ ? {
128
+ stage,
129
+ ok: false,
130
+ retryable: outcome.retryable,
131
+ error: outcome.error ?? "cdn_purge_failed",
132
+ }
133
+ : outcome.cdn === "queued"
134
+ ? { stage, ok: true, retryable: false, queued: true }
135
+ : done
136
+ })()
137
+ break
138
+ }
139
+ }
140
+ return work.catch(() => ({ stage, ok: false, retryable: true, error: `${stage}_failed` }))
141
+ }
142
+
143
+ export interface SettleOptions {
144
+ readonly onError?: ((report: InvalidationReport) => void) | undefined
145
+ /** Tests drive time; production waits on timers. */
146
+ readonly clock?: { now(): number; sleep(ms: number): Promise<void> }
147
+ }
148
+
149
+ const realClock = {
150
+ now: () => Date.now(),
151
+ sleep: (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)),
152
+ }
153
+
154
+ /**
155
+ * Run the pipeline for a write; never throws and never changes the response. A retryable failure
156
+ * retries from the failed stage, under `waitUntil` when the platform has it, else inline until
157
+ * {@link INLINE_DEADLINE_MS}. A failure that remains is reported once.
158
+ */
159
+ export async function settleInvalidation(
160
+ op: InvalidationOp,
161
+ targets: InvalidationTargets,
162
+ req: Request,
163
+ platform: ISRPlatform | undefined,
164
+ options: SettleOptions = {},
165
+ ): Promise<InvalidationReport> {
166
+ const onError = options.onError ?? logInvalidation
167
+ const clock = options.clock ?? realClock
168
+ const started = clock.now()
169
+ const first = await runInvalidation(op, targets, req, platform)
170
+ if (first.ok) return first
171
+ const failed = first.stages.find((report) => !report.ok)
172
+ if (failed === undefined || !failed.retryable) {
173
+ report(onError, first)
174
+ return first
175
+ }
176
+ const background = typeof platform?.waitUntil === "function"
177
+ const retry = async (): Promise<InvalidationReport> => {
178
+ let last = first
179
+ let stage = failed.stage
180
+ for (const delay of RETRY_MS) {
181
+ if (!background && clock.now() - started + delay >= INLINE_DEADLINE_MS) break
182
+ await clock.sleep(delay)
183
+ last = await runInvalidation(op, targets, req, platform, stage)
184
+ const next = last.stages.find((report) => !report.ok)
185
+ if (next === undefined) return last
186
+ if (!next.retryable) break
187
+ stage = next.stage
188
+ }
189
+ report(onError, last)
190
+ return last
191
+ }
192
+ if (background) {
193
+ platform?.waitUntil?.(retry())
194
+ return first
195
+ }
196
+ return retry()
197
+ }
198
+
199
+ function report(onError: (report: InvalidationReport) => void, value: InvalidationReport): void {
200
+ try {
201
+ onError(value)
202
+ } catch {
203
+ // A failing reporter must not reach the response.
204
+ }
205
+ }
206
+
207
+ /** The default report: the operation id and the stage, never a path, tag or key. */
208
+ export function logInvalidation(report: InvalidationReport): void {
209
+ const failed = report.stages.find((stage) => !stage.ok)
210
+ console.error(
211
+ `[nifra/web] invalidation ${report.id} gave up at the ${failed?.stage ?? "unknown"} stage (${failed?.error ?? "failed"})`,
212
+ )
213
+ }
@@ -0,0 +1,191 @@
1
+ /**
2
+ * The declarative binding walker - attaches signal-driven behavior to SERVER-RENDERED markup.
3
+ * No VDOM, no hydration re-render: the HTML the server sent (e.g. via `@nifrajs/web-vanilla`) is
4
+ * already the initial state; bindings update it surgically when signals change. `bindIsland` runs it.
5
+ *
6
+ * The attribute set is CLOSED and documented - six bindings, no expression language. Values are
7
+ * signal/handler NAMES looked up in the island's scope, never evaluated code, so markup can't
8
+ * inject behavior:
9
+ *
10
+ * data-bind-text="count" textContent ← String(signal())
11
+ * data-bind-show="isOpen" hidden ← !signal()
12
+ * data-bind-class="active:isOpen,b:sigB" classList.toggle per pair
13
+ * data-bind-attr="aria-expanded:isOpen" setAttribute / removeAttribute (false/null remove);
14
+ * never an `on*` handler or `srcdoc`, and a URL attribute
15
+ * takes only an http(s)/mailto/tel or relative URL
16
+ * data-bind-value="query" two-way <input>/<select>/<textarea> (input and change)
17
+ * data-bind-on="click:inc,submit:save" addEventListener per pair
18
+ *
19
+ * Nothing inside a `data-island-ignore` element binds or mounts: render user HTML there, so its
20
+ * markup cannot reach the island's handlers or signals.
21
+ *
22
+ * A binding belongs to its nearest island: an island never binds inside a nested island's host. The
23
+ * nested host element itself is still the outer island's markup, so the outer island may bind it.
24
+ */
25
+
26
+ import { type Signal, watch } from "./island-signals.ts"
27
+
28
+ export type IslandScope = {
29
+ readonly signals: Readonly<Record<string, Signal<unknown>>>
30
+ readonly handlers: Readonly<Record<string, (event: Event) => void>>
31
+ }
32
+
33
+ const warned = new Set<string>()
34
+ /** Unknown names warn once (loud in dev, resilient in prod - progressive enhancement must not
35
+ * throw over a typo'd attribute) and the binding is skipped. */
36
+ function warnOnce(kind: string, name: string): void {
37
+ const key = `${kind}:${name}`
38
+ if (warned.has(key)) return
39
+ warned.add(key)
40
+ console.warn(`[nifra/islands] unknown ${kind} ${JSON.stringify(name)} - binding skipped`)
41
+ }
42
+
43
+ // Own keys only: a name such as `constructor` must not resolve to what a plain object inherits.
44
+ function named<V>(kind: string, record: Readonly<Record<string, V>>, name: string): V | undefined {
45
+ if (Object.hasOwn(record, name)) return record[name]
46
+ warnOnce(kind, name)
47
+ return undefined
48
+ }
49
+
50
+ /** Parse a `"key:name,key2:name2"` pair list (whitespace-tolerant). Malformed entries skip. */
51
+ function pairs(spec: string): Array<[string, string]> {
52
+ const out: Array<[string, string]> = []
53
+ for (const part of spec.split(",")) {
54
+ const sep = part.indexOf(":")
55
+ if (sep <= 0) continue
56
+ const key = part.slice(0, sep).trim()
57
+ const name = part.slice(sep + 1).trim()
58
+ if (key && name) out.push([key, name])
59
+ }
60
+ return out
61
+ }
62
+
63
+ // A bound value is data (often from island state the markup carries), so an attribute that would run
64
+ // it as code, or navigate to it as a script URL, is refused.
65
+ const CODE_ATTRIBUTE = /^(?:on|srcdoc$)/i
66
+ const URL_ATTRIBUTE =
67
+ /^(?:href|src|action|formaction|xlink:href|poster|data|background|cite|ping)$/i
68
+ const SAFE_URL = /^(?:(?:https?|mailto|tel):|[^:/?#]*(?:[/?#]|$))/i
69
+
70
+ /** The element surface the walker needs - structural, so tests can drive it without a real DOM. */
71
+ export interface BindableElement {
72
+ getAttribute(name: string): string | null
73
+ setAttribute(name: string, value: string): void
74
+ removeAttribute(name: string): void
75
+ textContent: string | null
76
+ hidden: boolean
77
+ value?: string
78
+ readonly classList: { toggle(name: string, force?: boolean): unknown }
79
+ addEventListener(type: string, listener: (event: Event) => void): void
80
+ removeEventListener(type: string, listener: (event: Event) => void): void
81
+ }
82
+
83
+ export interface BindableRoot {
84
+ querySelectorAll(selector: string): Iterable<BindableElement>
85
+ }
86
+
87
+ /** Walk `root` and attach every `data-bind-*` binding against `scope`. Each binding's disposer
88
+ * (reactive effect or event listener) goes onto `stops` as it attaches, so a binding that throws
89
+ * midway leaves the earlier ones disposable; `stops` is returned. */
90
+ export function bindScope(
91
+ root: BindableRoot,
92
+ scope: IslandScope,
93
+ stops: Array<() => void> = [],
94
+ ): Array<() => void> {
95
+ // Each element carrying `data-bind-<kind>` outside any ignored subtree and any nested island, with
96
+ // that attribute's value.
97
+ const each = (kind: string): Array<[BindableElement, string]> => {
98
+ const attribute = `data-bind-${kind}`
99
+ return [
100
+ ...root.querySelectorAll(
101
+ `[${attribute}]:not([data-island-ignore],[data-island-ignore] *,:scope [data-island] *)`,
102
+ ),
103
+ ].map((el) => [el, el.getAttribute(attribute) ?? ""])
104
+ }
105
+
106
+ for (const [el, name] of each("text")) {
107
+ const s = named("signal", scope.signals, name)
108
+ if (s) {
109
+ stops.push(
110
+ watch(s, (v) => {
111
+ el.textContent = String(v)
112
+ }),
113
+ )
114
+ }
115
+ }
116
+
117
+ for (const [el, name] of each("show")) {
118
+ const s = named("signal", scope.signals, name)
119
+ if (s) {
120
+ stops.push(
121
+ watch(s, (v) => {
122
+ el.hidden = !v
123
+ }),
124
+ )
125
+ }
126
+ }
127
+
128
+ for (const [el, spec] of each("class")) {
129
+ for (const [className, name] of pairs(spec)) {
130
+ const s = named("signal", scope.signals, name)
131
+ if (s) {
132
+ stops.push(
133
+ watch(s, (v) => {
134
+ el.classList.toggle(className, !!v)
135
+ }),
136
+ )
137
+ }
138
+ }
139
+ }
140
+
141
+ for (const [el, spec] of each("attr")) {
142
+ for (const [attr, name] of pairs(spec)) {
143
+ if (CODE_ATTRIBUTE.test(attr)) {
144
+ warnOnce("attribute", attr)
145
+ continue
146
+ }
147
+ const s = named("signal", scope.signals, name)
148
+ if (s) {
149
+ const isUrl = URL_ATTRIBUTE.test(attr)
150
+ stops.push(
151
+ watch(s, (v) => {
152
+ if (v === false || v == null || (isUrl && !SAFE_URL.test(String(v).trim())))
153
+ el.removeAttribute(attr)
154
+ else el.setAttribute(attr, String(v))
155
+ }),
156
+ )
157
+ }
158
+ }
159
+ }
160
+
161
+ for (const [el, name] of each("value")) {
162
+ const s = named("signal", scope.signals, name)
163
+ if (s) {
164
+ stops.push(
165
+ watch(s, (v) => {
166
+ const next = String(v)
167
+ // Skip same-value writes: resetting an input's value mid-typing moves the caret.
168
+ if (el.value !== next) el.value = next
169
+ }),
170
+ )
171
+ const input = (): void => s.set(el.value ?? "")
172
+ // A <select> commits on `change`, and browsers disagree on whether `input` comes first. The
173
+ // second event of a pair writes the same value, which the signal skips.
174
+ for (const type of ["input", "change"]) {
175
+ el.addEventListener(type, input)
176
+ stops.push(() => el.removeEventListener(type, input))
177
+ }
178
+ }
179
+ }
180
+
181
+ for (const [el, spec] of each("on")) {
182
+ for (const [event, name] of pairs(spec)) {
183
+ const handler = named("handler", scope.handlers, name)
184
+ if (handler === undefined) continue
185
+ el.addEventListener(event, handler)
186
+ stops.push(() => el.removeEventListener(event, handler))
187
+ }
188
+ }
189
+
190
+ return stops
191
+ }
@@ -0,0 +1,92 @@
1
+ import { effect, untrack } from "./island-signals.ts"
2
+
3
+ /** The node surface `bindList` uses; an `Element` has it. */
4
+ export interface ListNode {
5
+ remove(): void
6
+ }
7
+
8
+ /** The container surface `bindList` uses; an `Element` has it. */
9
+ export interface ListContainer<E extends ListNode> {
10
+ replaceChildren(...nodes: E[]): void
11
+ insertBefore(node: E, before: E | null): unknown
12
+ }
13
+
14
+ /** How `bindList` turns items into keyed DOM. */
15
+ export interface BindListOptions<T, E extends ListNode = HTMLElement> {
16
+ /** Stable and unique per item - never the array index, or a removal reuses the wrong node. */
17
+ key(item: T): string | number
18
+ create(item: T): E
19
+ /** Brings a node in line with its item: runs after `create` and on every later change. */
20
+ update?(el: E, item: T): void
21
+ }
22
+
23
+ /**
24
+ * Render a list signal (or any getter) into `container` with keyed reconciliation. A surviving item
25
+ * keeps its node, so focus, selection and input state survive a reorder. A render with no earlier
26
+ * nodes replaces the container's children; later renders move only the nodes outside the longest
27
+ * run already in order. `key`, `create` and `update` run untracked: a signal they read does not
28
+ * re-render the list. Returns the disposer; inside an island the binding also stops on unmount.
29
+ */
30
+ export function bindList<T, E extends ListNode = HTMLElement>(
31
+ source: () => readonly T[],
32
+ // A DOM container's generic methods would drive E to `ListNode`; the row type comes from `create`.
33
+ container: ListContainer<NoInfer<E>>,
34
+ options: BindListOptions<T, E>,
35
+ ): () => void {
36
+ let nodes = new Map<string | number, E>()
37
+ // Each node's position in the last render; empty until a render leaves nodes to keep.
38
+ let at = new Map<E, number>()
39
+ return effect(() => {
40
+ const items = source()
41
+ untrack(() => {
42
+ const next = new Map<string | number, E>()
43
+ for (const item of items) {
44
+ const key = options.key(item)
45
+ if (next.has(key)) console.warn("[nifra/islands] bindList: duplicate key skipped", key)
46
+ else {
47
+ const el = nodes.get(key) ?? options.create(item)
48
+ options.update?.(el, item)
49
+ next.set(key, el)
50
+ }
51
+ }
52
+ const els = [...next.values()]
53
+ if (at.size === 0) container.replaceChildren(...els)
54
+ else {
55
+ for (const [key, el] of nodes) if (!next.has(key)) el.remove()
56
+ const stays = inOrder(els.map((el) => at.get(el) ?? -1))
57
+ let anchor: E | null = null
58
+ for (let i = els.length - 1; i >= 0; i--) {
59
+ const el = els[i]
60
+ if (el === undefined) continue
61
+ if (!stays.has(i)) container.insertBefore(el, anchor)
62
+ anchor = el
63
+ }
64
+ }
65
+ at = new Map(els.map((el, i) => [el, i]))
66
+ nodes = next
67
+ })
68
+ })
69
+ }
70
+
71
+ // The indexes of `old` (each node's previous position, -1 for a new node) on a longest increasing
72
+ // run: those nodes already sit in order, and every other node is moved.
73
+ function inOrder(old: readonly number[]): Set<number> {
74
+ // tails[n] is the index ending the best run of length n + 1; back links each run.
75
+ const tails: number[] = []
76
+ const back: number[] = []
77
+ old.forEach((o, i) => {
78
+ if (o < 0) return
79
+ let lo = 0
80
+ let hi = tails.length
81
+ while (lo < hi) {
82
+ const mid = (lo + hi) >> 1
83
+ if ((old[tails[mid] ?? 0] ?? 0) < o) lo = mid + 1
84
+ else hi = mid
85
+ }
86
+ back[i] = tails[lo - 1] ?? -1
87
+ tails[lo] = i
88
+ })
89
+ const stays = new Set<number>()
90
+ for (let i = tails.at(-1) ?? -1; i >= 0; i = back[i] ?? -1) stays.add(i)
91
+ return stays
92
+ }
@@ -0,0 +1,68 @@
1
+ import {
2
+ type IslandStrategy,
3
+ MAX_MEDIA_QUERY_LENGTH,
4
+ scheduleTrigger,
5
+ } from "@nifrajs/island-trigger"
6
+ import type { IslandCleanup, IslandEnhancer } from "../islands.ts"
7
+ import { owned } from "./island-owner.ts"
8
+
9
+ function readProps(el: HTMLElement): unknown {
10
+ const raw = el.dataset.islandProps
11
+ if (raw === undefined || raw === "") return undefined
12
+ try {
13
+ return JSON.parse(raw)
14
+ } catch {
15
+ return undefined
16
+ }
17
+ }
18
+
19
+ /** Own one host so removing a subtree can stop its islands without disturbing retained siblings. */
20
+ export function mountIsland(
21
+ el: HTMLElement,
22
+ enhancers: Readonly<Record<string, IslandEnhancer<never>>>,
23
+ ): IslandCleanup | undefined {
24
+ const id = el.dataset.island
25
+ if (id === undefined || el.dataset.islandMounted !== undefined) return undefined
26
+ const enhancer = Object.hasOwn(enhancers, id) ? enhancers[id] : undefined
27
+ if (enhancer === undefined) return undefined
28
+ const strategyAttr = el.dataset.islandStrategy
29
+ const media = el.dataset.islandMedia
30
+ const strategy: IslandStrategy | undefined =
31
+ strategyAttr === "idle" || strategyAttr === "visible" || strategyAttr === "load"
32
+ ? strategyAttr
33
+ : strategyAttr === "media" &&
34
+ media !== undefined &&
35
+ media.length > 0 &&
36
+ media.length <= MAX_MEDIA_QUERY_LENGTH
37
+ ? { media }
38
+ : strategyAttr === undefined
39
+ ? "load"
40
+ : undefined
41
+ // Invalid markers must stay inert, including untrusted oversized media queries.
42
+ if (strategy === undefined) return undefined
43
+ const disposers: Array<() => void> = []
44
+ el.dataset.islandMounted = ""
45
+ disposers.push(() => {
46
+ delete el.dataset.islandMounted
47
+ })
48
+ const props = readProps(el)
49
+ const run = (): void => {
50
+ try {
51
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: enhancer props are the authored SSR contract, as in defineIsland
52
+ const cleanup = owned(disposers, () => enhancer(el, props as never))
53
+ if (typeof cleanup === "function") disposers.push(cleanup)
54
+ } catch (err) {
55
+ console.error(`[nifra/islands] enhancer "${id}" failed:`, err)
56
+ }
57
+ }
58
+ disposers.push(scheduleTrigger(strategy, run, { target: el }))
59
+ return () => {
60
+ for (const dispose of disposers.splice(0)) {
61
+ try {
62
+ dispose()
63
+ } catch {
64
+ // Best-effort teardown: one failing cleanup must not block the rest.
65
+ }
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,20 @@
1
+ // The disposer list of the island whose enhancer is running, so every effect, computed, list binding
2
+ // and resource it creates stops when the island unmounts. A separate module so an imperative-only
3
+ // island bundle carries no signal code.
4
+ let owner: Array<() => void> | undefined
5
+
6
+ /** Run `fn` with every disposer it registers pushed onto `kids`. */
7
+ export function owned<T>(kids: Array<() => void>, fn: () => T): T {
8
+ const prev = owner
9
+ owner = kids
10
+ try {
11
+ return fn()
12
+ } finally {
13
+ owner = prev
14
+ }
15
+ }
16
+
17
+ /** Register `stop` with the running island, if any. Outside an enhancer it stays the caller's. */
18
+ export function adopt(stop: () => void): void {
19
+ owner?.push(stop)
20
+ }
@@ -0,0 +1,76 @@
1
+ import { adopt } from "./island-owner.ts"
2
+ import { effect, signal, untrack } from "./island-signals.ts"
3
+
4
+ /** An async value's state. `status` narrows `value` and `error`, so no branch can be skipped. */
5
+ export type ResourceState<T> =
6
+ | { readonly status: "pending"; readonly value: undefined; readonly error: undefined }
7
+ | { readonly status: "ready"; readonly value: T; readonly error: undefined }
8
+ | { readonly status: "error"; readonly value: undefined; readonly error: unknown }
9
+
10
+ /** Call it to read the current state (tracking, like a signal). */
11
+ export type Resource<T> = {
12
+ (): ResourceState<T>
13
+ /** Fetch the current key again; once disposed, nothing. */
14
+ refetch(): void
15
+ /** Stop refetching and abort the request in flight; its result is dropped. */
16
+ dispose(): void
17
+ }
18
+
19
+ const PENDING = { status: "pending", value: undefined, error: undefined } as const
20
+
21
+ /**
22
+ * An async value keyed by `source`. `source` runs tracked, so a write to any signal it reads starts a
23
+ * new fetch; `fetcher` runs untracked with that key, so nothing it reads (before or after an `await`)
24
+ * is a dependency. A newer fetch aborts the older one and only the newest result lands. Inside an
25
+ * island, unmounting disposes it: the request in flight is aborted and its late result dropped.
26
+ */
27
+ export function resource<K, T>(
28
+ source: () => K,
29
+ fetcher: (key: K, abort: AbortSignal) => Promise<T>,
30
+ ): Resource<T> {
31
+ const state = signal<ResourceState<T>>(PENDING)
32
+ let generation = 0
33
+ let live = true
34
+ let controller: AbortController | undefined
35
+ let key: K
36
+ const run = (): void => {
37
+ const mine = ++generation
38
+ controller?.abort()
39
+ const active = new AbortController()
40
+ controller = active
41
+ state.set(PENDING)
42
+ let pending: Promise<T>
43
+ try {
44
+ pending = fetcher(key, active.signal)
45
+ } catch (error) {
46
+ pending = Promise.reject(error)
47
+ }
48
+ pending.then(
49
+ (value) => {
50
+ if (mine === generation) state.set({ status: "ready", value, error: undefined })
51
+ },
52
+ (error) => {
53
+ // A superseded or aborted fetch is not an error the reader should see.
54
+ if (mine === generation && !active.signal.aborted)
55
+ state.set({ status: "error", value: undefined, error })
56
+ },
57
+ )
58
+ }
59
+ const stop = effect(() => {
60
+ key = source()
61
+ untrack(run)
62
+ })
63
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: `refetch` and `dispose` are assigned below
64
+ const read = (() => state()) as Resource<T>
65
+ read.refetch = () => {
66
+ if (live) untrack(run)
67
+ }
68
+ read.dispose = () => {
69
+ live = false
70
+ stop()
71
+ generation++
72
+ controller?.abort()
73
+ }
74
+ adopt(read.dispose)
75
+ return read
76
+ }