@panphora/clayjs 1.17.0 → 1.18.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 (467) hide show
  1. package/dist/all.js +1 -1
  2. package/dist/clay-dom.js +1 -1
  3. package/dist/clay-events.js +1 -1
  4. package/dist/clay-internals.js +1 -1
  5. package/dist/clay-options.js +1 -1
  6. package/dist/clay-router.js +1 -1
  7. package/dist/clay-ui.js +1 -1
  8. package/dist/clay-utils.js +1 -1
  9. package/dist/clay.js +1 -1
  10. package/dist/clay.standalone.js +72474 -62090
  11. package/dist/m/attrs/{save-freeze-MYNVXREN.js → save-freeze-LWGF6XUN.js} +1 -1
  12. package/dist/m/c-2GRUM77Q.js +30 -0
  13. package/dist/m/c-2KIEGNC4.js +1 -0
  14. package/dist/m/{c-AMSNPOCX.js → c-2KL3BD52.js} +1 -1
  15. package/dist/m/c-2ZZBC45O.js +1 -0
  16. package/dist/m/c-372N6H3B.js +1 -0
  17. package/dist/m/{c-4WKFKEKF.js → c-3G2DGZUP.js} +1 -1
  18. package/dist/m/c-3LDGIBMN.js +1 -0
  19. package/dist/m/c-3O7OBDCM.js +1 -0
  20. package/dist/m/c-4ASQPRQL.js +1 -0
  21. package/dist/m/c-4TXNLX4K.js +6 -0
  22. package/dist/m/c-4YU3BR4D.js +6 -0
  23. package/dist/m/c-5734MF2P.js +1 -0
  24. package/dist/m/c-57XDY6HC.js +1 -0
  25. package/dist/m/{c-KAU6E43D.js → c-5B4YBZXC.js} +1 -1
  26. package/dist/m/c-5ONTYIUI.js +1 -0
  27. package/dist/m/c-5OOVFCJM.js +1 -0
  28. package/dist/m/{c-3XIRD4NU.js → c-5T6NZHNM.js} +1 -1
  29. package/dist/m/{c-74MGYWZF.js → c-5ZQV7KC4.js} +1 -1
  30. package/dist/m/{c-7OLLJPWP.js → c-6DB2OLRJ.js} +1 -1
  31. package/dist/m/c-6DNRCPNM.js +1 -0
  32. package/dist/m/c-6UGWFMQC.js +2 -0
  33. package/dist/m/c-7K4SCQB7.js +1 -0
  34. package/dist/m/c-7MG5RU63.js +1 -0
  35. package/dist/m/c-7PRJCA3E.js +1 -0
  36. package/dist/m/c-A6L3FMUS.js +1 -0
  37. package/dist/m/c-AMU3BPPR.js +4 -0
  38. package/dist/m/{c-Y5HBBIQ4.js → c-AMYEFA2E.js} +1 -1
  39. package/dist/m/c-AQYCG7QW.js +1 -0
  40. package/dist/m/c-ATKAOH23.js +1 -0
  41. package/dist/m/c-B5CS55WR.js +1 -0
  42. package/dist/m/c-B5UJQT7W.js +1 -0
  43. package/dist/m/c-B7J4K2AE.js +3 -0
  44. package/dist/m/c-BHZYSHW4.js +1 -0
  45. package/dist/m/{c-VHM5XQHK.js → c-BIJALSRD.js} +1 -1
  46. package/dist/m/c-BJ7YQ5QZ.js +1 -0
  47. package/dist/m/{c-KIPQJX4B.js → c-BRLCTNMC.js} +1 -1
  48. package/dist/m/{c-VE6ZMYMC.js → c-BUAYFEV5.js} +1 -1
  49. package/dist/m/c-C6IPRG34.js +1 -0
  50. package/dist/m/{c-KU3ZFADW.js → c-CAS3NND4.js} +1 -1
  51. package/dist/m/{c-PS5ARVWR.js → c-CCQ25GFY.js} +1 -1
  52. package/dist/m/c-CJFDLD43.js +1 -0
  53. package/dist/m/c-COPTL5WB.js +1 -0
  54. package/dist/m/c-D3E4WJ4M.js +166 -0
  55. package/dist/m/c-D5IKEKXW.js +7 -0
  56. package/dist/m/c-DHBRA6DL.js +1 -0
  57. package/dist/m/c-DKLS74XB.js +72 -0
  58. package/dist/m/c-DVYGFTSK.js +3 -0
  59. package/dist/m/c-E3ZXARWA.js +1 -0
  60. package/dist/m/c-EK63G3CG.js +1 -0
  61. package/dist/m/c-ERQSNR6K.js +13 -0
  62. package/dist/m/c-EVYAISP3.js +1 -0
  63. package/dist/m/c-EXKHPOVC.js +3 -0
  64. package/dist/m/{c-QJ4OM3G6.js → c-FWKHM4E7.js} +1 -1
  65. package/dist/m/{c-4Q2XTQ2N.js → c-GBKB5WDL.js} +1 -1
  66. package/dist/m/c-GNDL3TKB.js +1 -0
  67. package/dist/m/c-GRWMD6O5.js +1 -0
  68. package/dist/m/c-GRXHHFPO.js +1 -0
  69. package/dist/m/c-HG2C77BM.js +4 -0
  70. package/dist/m/c-HHGJBU4D.js +1 -0
  71. package/dist/m/c-HMV6P4RX.js +1 -0
  72. package/dist/m/c-HMYWIHFG.js +1 -0
  73. package/dist/m/c-HT2DKBV2.js +1 -0
  74. package/dist/m/c-I2SCCUEW.js +1 -0
  75. package/dist/m/{c-TJ6WOJL3.js → c-ICJYHABF.js} +2 -2
  76. package/dist/m/c-IMZCWYHR.js +7 -0
  77. package/dist/m/c-IVMD3FEM.js +1 -0
  78. package/dist/m/{c-WDC5GVKU.js → c-IVTRVENZ.js} +1 -1
  79. package/dist/m/c-JKZJIUIP.js +1 -0
  80. package/dist/m/{c-UDUUJPFZ.js → c-JNLX3RJB.js} +1 -1
  81. package/dist/m/c-K3ADYNLU.js +1 -0
  82. package/dist/m/c-KGHVFRR3.js +1 -0
  83. package/dist/m/{c-5QWQ7A5L.js → c-KJJPV77R.js} +1 -1
  84. package/dist/m/{c-CZUSOB7V.js → c-KNPDOMDY.js} +1 -1
  85. package/dist/m/{c-EZU56NQV.js → c-KP64LT2A.js} +2 -2
  86. package/dist/m/c-KQ7E3PYY.js +1 -0
  87. package/dist/m/c-L3F5ZLIV.js +1 -0
  88. package/dist/m/{c-3RUU4TPX.js → c-L42OLXWM.js} +3 -2
  89. package/dist/m/c-L5VFATT6.js +1 -0
  90. package/dist/m/c-L7JL5TL3.js +3 -0
  91. package/dist/m/c-LKSL4AUJ.js +1 -0
  92. package/dist/m/c-M6FNQCTQ.js +1 -0
  93. package/dist/m/c-MCOOHCSG.js +1 -0
  94. package/dist/m/c-MHI3GYKF.js +1 -0
  95. package/dist/m/c-MWXFLW4J.js +1 -0
  96. package/dist/m/{c-5RJFTLSF.js → c-NEL7HQW3.js} +1 -1
  97. package/dist/m/{c-DCXOXBF3.js → c-NQJRVJ56.js} +1 -1
  98. package/dist/m/c-NWX3MFT4.js +1 -0
  99. package/dist/m/{c-HPURYVZS.js → c-OJYM642Q.js} +1 -1
  100. package/dist/m/c-OKXI52N7.js +1 -0
  101. package/dist/m/{c-R3L7ZJGB.js → c-OPLUZR3A.js} +1 -1
  102. package/dist/m/{c-GSFIGKOP.js → c-OTMKN2OO.js} +1 -1
  103. package/dist/m/{c-6NC5I2TV.js → c-OYRZ7XG2.js} +1 -1
  104. package/dist/m/{c-EVCQ3BKA.js → c-P3OFYUMA.js} +1 -1
  105. package/dist/m/c-PLN76GF3.js +6 -0
  106. package/dist/m/c-PLP4JKFC.js +1 -0
  107. package/dist/m/c-Q26MME6N.js +1 -0
  108. package/dist/m/{c-S7H2DRCB.js → c-Q3XOWZEV.js} +1 -1
  109. package/dist/m/{c-FRLHSY7E.js → c-QIKT6SKM.js} +1 -1
  110. package/dist/m/c-QIXT3Q7C.js +28 -0
  111. package/dist/m/c-QO4HNGCQ.js +1 -0
  112. package/dist/m/{c-JU3NK25H.js → c-R27DWQKX.js} +1 -1
  113. package/dist/m/{c-RSMOODDU.js → c-R2XUODVY.js} +1 -1
  114. package/dist/m/{c-QNKREOL6.js → c-R7LBFMF5.js} +1 -1
  115. package/dist/m/c-RND3G2S2.js +1 -0
  116. package/dist/m/{c-Q622NS2Y.js → c-S2U6NJU4.js} +1 -1
  117. package/dist/m/c-SPZPGD3R.js +1 -0
  118. package/dist/m/c-SW4YEG2D.js +8 -0
  119. package/dist/m/c-T637T7SN.js +1 -0
  120. package/dist/m/c-TJAXGIRV.js +1 -0
  121. package/dist/m/{c-RYUXUUBX.js → c-TNEM3RNM.js} +1 -1
  122. package/dist/m/c-TTB5EK4S.js +32 -0
  123. package/dist/m/c-U7SHNWDH.js +1 -0
  124. package/dist/m/c-UDHIVLYH.js +20 -0
  125. package/dist/m/{c-BBVGTQG7.js → c-UGNV5MFB.js} +1 -1
  126. package/dist/m/{c-QICP2YR5.js → c-UORXLSD3.js} +1 -1
  127. package/dist/m/c-UPD33ZPE.js +1 -0
  128. package/dist/m/c-URSMSFCB.js +1 -0
  129. package/dist/m/c-UY4QX42B.js +1 -0
  130. package/dist/m/c-V44KWI2L.js +4 -0
  131. package/dist/m/c-VILENZTD.js +1 -0
  132. package/dist/m/{c-HDDEEL77.js → c-VJYEVRYK.js} +1 -1
  133. package/dist/m/{c-SMBZG5X4.js → c-VR2BJ4AN.js} +1 -1
  134. package/dist/m/c-W4JCVLAB.js +1 -0
  135. package/dist/m/c-W6DQSHXI.js +135 -0
  136. package/dist/m/c-WBGWGOM7.js +1 -0
  137. package/dist/m/{c-4L3BTFRK.js → c-WCXLG3MV.js} +1 -1
  138. package/dist/m/c-WGW764WX.js +1 -0
  139. package/dist/m/c-WMRJKQ2V.js +5 -0
  140. package/dist/m/c-WNTYKCDG.js +1 -0
  141. package/dist/m/c-WQWIRWO3.js +72 -0
  142. package/dist/m/c-X5LNHET5.js +1 -0
  143. package/dist/m/{c-I2ZATQCD.js → c-X5TSXZYJ.js} +2 -2
  144. package/dist/m/c-XGH4GS2T.js +1 -0
  145. package/dist/m/c-Y6F2FORL.js +1 -0
  146. package/dist/m/c-YBKHUAK7.js +10 -0
  147. package/dist/m/c-YBSS55KU.js +20 -0
  148. package/dist/m/c-YCLU4UNT.js +2 -0
  149. package/dist/m/c-YVZ2L7CE.js +1 -0
  150. package/dist/m/c-ZK4GIZH2.js +1 -0
  151. package/dist/m/c-ZRXJD4G6.js +1 -0
  152. package/dist/m/c-ZWBI4HYK.js +1 -0
  153. package/dist/m/core/{admin-attrs-QHOKPJ7W.js → admin-attrs-YY7T5FGU.js} +1 -1
  154. package/dist/m/core/autosave-S6OMNDCK.js +1 -0
  155. package/dist/m/core/conflict-notice-IWHMHH43.js +1 -0
  156. package/dist/m/core/{host-permission-FNXVEFNE.js → host-permission-6EVJGNPL.js} +1 -1
  157. package/dist/m/core/is-edit-mode-GCX6767E.js +1 -0
  158. package/dist/m/core/{persist-JFYRHMRJ.js → persist-UKXV5KLA.js} +1 -1
  159. package/dist/m/core/recovery-DRFZRJED.js +1 -0
  160. package/dist/m/core/save-LYY7K6U7.js +1 -0
  161. package/dist/m/core/save-core-MKRPSEEO.js +1 -0
  162. package/dist/m/core/snapshot-NU3YM6GR.js +1 -0
  163. package/dist/m/core/unsaved-warning-7QTKEN7Z.js +1 -0
  164. package/dist/m/internals/{index-PVXGPUDA.js → index-TGLQINN3.js} +1 -1
  165. package/dist/m/loader-3BBNZZGM.js +1 -0
  166. package/dist/m/manifest-F449FF98.js +1 -0
  167. package/dist/m/manifest-edit-4396A009.js +1 -0
  168. package/dist/m/manifest.json +2111 -1573
  169. package/dist/m/page/{editor-NNJA4OKD.js → editor-6LRB3GE4.js} +123 -21
  170. package/dist/m/plugins/ai-edit-UTV7YO2I.js +6 -0
  171. package/dist/m/plugins/{cms-HP3LSTPN.js → cms-LRAOZHT7.js} +9 -9
  172. package/dist/m/plugins/{demo-RM5IR6AO.js → demo-OYO5GCAO.js} +1 -1
  173. package/dist/m/plugins/{derive-UAAA4V4S.js → derive-FRE5JJQE.js} +1 -1
  174. package/dist/m/plugins/indicator-4VDXWYTD.js +1 -0
  175. package/dist/m/plugins/live-AD4H6XSL.js +1 -0
  176. package/dist/m/plugins/menu-M5WW7DBF.js +1 -0
  177. package/dist/m/plugins/page-MD3T2ZX3.js +2 -0
  178. package/dist/m/plugins/page-view-PAWVZNFY.js +1 -0
  179. package/dist/m/plugins/{people-SGPJKANK.js → people-JCHVPF7P.js} +1 -1
  180. package/dist/m/plugins/quickcrop-BQ5YYFCH.js +26 -0
  181. package/dist/m/plugins/{richclay-NRQDSWX6.js → richclay-KLGETK77.js} +2 -2
  182. package/dist/m/plugins/sortable-4S6SQC3D.js +1 -0
  183. package/dist/m/plugins/source-TJXOIOYY.js +1 -0
  184. package/dist/m/plugins/undo-NLPGCJQY.js +1 -0
  185. package/dist/m/plugins/{unsent-HJFLQJ2O.js → unsent-OA4RGBUT.js} +1 -1
  186. package/dist/m/plugins/upgrade-HSUNDNJD.js +1 -0
  187. package/dist/m/plugins/{upload-WLYKK6TL.js → upload-NHGJXZK2.js} +1 -1
  188. package/dist/m/plugins/visitor-drafts-BW43DGY3.js +20 -0
  189. package/dist/m/plugins/visitor-drafts-owner-7ODQG7JX.js +1 -0
  190. package/dist/m/plugins/{wire-4N6GYAF6.js → wire-VBWXEXU2.js} +1 -1
  191. package/dist/m/reader-core-4W2KYEWQ.js +77 -0
  192. package/dist/m/route/route-62CHXL5B.js +1 -0
  193. package/dist/m/sync/live-sync-MO5DH33W.js +1 -0
  194. package/dist/m/ui/index-IVWN2QC4.js +1 -0
  195. package/dist/m/vendor/sap-core.vendor-RN4ZVY4X.js +6 -0
  196. package/dist/offline-manifest.json +1367 -930
  197. package/dist/page/themes/light-1.css +8 -7
  198. package/dist/server/core.cjs +1440 -288
  199. package/dist/server/offline-runtime.cjs +63 -63
  200. package/dist/server/offline-runtime.mjs +63 -63
  201. package/dist/server/page-compaction-worker.cjs +3710 -0
  202. package/dist/server/rooms.mjs +5 -0
  203. package/docs/guide.md +653 -101
  204. package/entries/sap.js +5 -5
  205. package/package.json +1 -1
  206. package/src/core/admin-contenteditable.js +5 -2
  207. package/src/core/admin-inputs.js +1 -0
  208. package/src/core/admin-onclick.js +2 -1
  209. package/src/core/conflict-notice-ui.js +4 -7
  210. package/src/core/download.js +2 -2
  211. package/src/core/host-meta.js +38 -0
  212. package/src/core/is-edit-mode.js +17 -1
  213. package/src/core/mode-key.js +125 -19
  214. package/src/core/mode-support.js +5 -0
  215. package/src/core/mode-transition.js +20 -6
  216. package/src/core/preserve-authored.js +4 -4
  217. package/src/core/recovery.js +121 -33
  218. package/src/core/save-accepted.js +33 -0
  219. package/src/core/save-core.js +106 -31
  220. package/src/core/save-job.js +108 -0
  221. package/src/core/save-prepare.js +53 -0
  222. package/src/core/save-size-notice.js +3 -1
  223. package/src/core/save-slices.js +15 -0
  224. package/src/core/save.js +557 -127
  225. package/src/core/serialize-steps.js +109 -0
  226. package/src/core/snapshot.js +170 -39
  227. package/src/core/source-map.js +83 -38
  228. package/src/core/stale-host-notice.js +1 -0
  229. package/src/core/upgrade-run.js +59 -8
  230. package/src/dom/active-element.js +5 -0
  231. package/src/lib/assets.js +1 -0
  232. package/src/lib/slice.js +65 -0
  233. package/src/lib/uimotion.js +774 -0
  234. package/src/lib/unsaved-state.js +2 -2
  235. package/src/live/fast-lane.js +10 -3
  236. package/src/live/local-lane.js +202 -11
  237. package/src/live/ops.js +16 -0
  238. package/src/live/page-compaction.js +162 -0
  239. package/src/live/server-lane.js +1 -0
  240. package/src/live/store.js +11 -3
  241. package/src/loader-logic.js +3 -1
  242. package/src/loader.js +14 -4
  243. package/src/page/adopt-boot.js +2 -1
  244. package/src/page/awareness.js +33 -2
  245. package/src/page/blocks.js +89 -27
  246. package/src/page/boot-pack.js +1 -1
  247. package/src/page/chart-tables.js +21 -3
  248. package/src/page/chart.js +406 -63
  249. package/src/page/checklist.js +10 -135
  250. package/src/page/checkpoint-proof.js +7 -1
  251. package/src/page/chrome.js +250 -51
  252. package/src/page/clipboard-shape.js +126 -0
  253. package/src/page/drag.js +33 -3
  254. package/src/page/editor.js +136 -25
  255. package/src/page/export-assets.js +357 -0
  256. package/src/page/export-html.js +209 -0
  257. package/src/page/export-markdown.js +361 -0
  258. package/src/page/features/checklist.js +428 -0
  259. package/src/page/features/export-html.js +189 -0
  260. package/src/page/features/export-markdown.js +169 -0
  261. package/src/page/features/import-markdown.js +349 -0
  262. package/src/page/features/media.js +61 -5
  263. package/src/page/features/registry.js +18 -0
  264. package/src/page/features/table.js +373 -83
  265. package/src/page/features/title-cover.js +331 -0
  266. package/src/page/features/title-icon.js +213 -0
  267. package/src/page/features/toc.js +87 -0
  268. package/src/page/format-command.js +25 -1
  269. package/src/page/format-gate.js +68 -18
  270. package/src/page/format-host.js +17 -0
  271. package/src/page/format-native-commands.js +21 -5
  272. package/src/page/image-actions.js +64 -0
  273. package/src/page/import-markdown.js +262 -0
  274. package/src/page/items.js +135 -17
  275. package/src/page/markdown.js +6 -2
  276. package/src/page/media.js +35 -119
  277. package/src/page/placeholder.js +14 -1
  278. package/src/page/projection.js +98 -10
  279. package/src/page/recover.js +10 -12
  280. package/src/page/recovery-ui.js +464 -0
  281. package/src/page/rich/chart.js +63 -7
  282. package/src/page/save-projection.js +28 -0
  283. package/src/page/save-states.js +13 -3
  284. package/src/page/schema.js +19 -3
  285. package/src/page/table-paste.js +3 -1
  286. package/src/page/table-style.js +4 -4
  287. package/src/page/table.js +44 -2
  288. package/src/page/themes/light-1.css +8 -7
  289. package/src/page/themes/light-1.source.css +8 -7
  290. package/src/page/toc-link.js +31 -0
  291. package/src/page/toc.js +5 -0
  292. package/src/page/view-hooks.js +38 -12
  293. package/src/page/view.js +13 -0
  294. package/src/page/ybind/binding.js +88 -5
  295. package/src/page/ybind/bridge.js +187 -20
  296. package/src/page/ybind/containment.js +162 -0
  297. package/src/page/ybind/doc.js +17 -9
  298. package/src/page/ybind/file-debt.js +63 -0
  299. package/src/page/ybind/layout.js +7 -2
  300. package/src/page/ybind/marks.js +1 -1
  301. package/src/page/ybind/moves.js +5 -4
  302. package/src/page/ybind/origins.js +106 -4
  303. package/src/page/ybind/parts.js +8 -1
  304. package/src/page/ybind/provider.js +113 -17
  305. package/src/page/ybind/recovery-proof.js +32 -9
  306. package/src/page/ybind/recovery-replay.js +558 -15
  307. package/src/page/ybind/recovery.js +174 -45
  308. package/src/page/ybind/table.js +67 -11
  309. package/src/page/ybind/walk-delta.js +258 -0
  310. package/src/page/ybind/walk.js +54 -29
  311. package/src/page/ybind/writer.js +1 -1
  312. package/src/plugins/ai-edit.js +13 -9
  313. package/src/plugins/indicator-chip.js +91 -0
  314. package/src/plugins/indicator.js +13 -80
  315. package/src/plugins/live.js +89 -14
  316. package/src/plugins/menu/actions.js +196 -0
  317. package/src/plugins/menu/people.js +279 -0
  318. package/src/plugins/menu/placement.js +121 -0
  319. package/src/plugins/menu/preferences.js +9 -0
  320. package/src/plugins/menu/seo.js +316 -397
  321. package/src/plugins/menu/style.js +171 -0
  322. package/src/plugins/menu/variants.js +296 -0
  323. package/src/plugins/menu.js +186 -183
  324. package/src/plugins/page-view.js +3 -2
  325. package/src/plugins/page.js +249 -44
  326. package/src/plugins/sortable.js +8 -1
  327. package/src/plugins/source.js +6 -0
  328. package/src/plugins/upgrade.js +51 -17
  329. package/src/plugins/upload-ui.js +12 -17
  330. package/src/plugins/visitor-drafts/bottom-ui.js +12 -4
  331. package/src/plugins/visitor-drafts/intro-style.js +7 -2
  332. package/src/plugins/visitor-drafts/intro.js +5 -3
  333. package/src/plugins/visitor-drafts/owner-chip.js +8 -12
  334. package/src/plugins/visitor-drafts/prompt.js +11 -9
  335. package/src/plugins/visitor-drafts/source.js +4 -3
  336. package/src/plugins/visitor-drafts/store.js +138 -9
  337. package/src/plugins/visitor-drafts/ui-memory.js +2 -2
  338. package/src/plugins/visitor-drafts-owner.js +4 -6
  339. package/src/plugins/visitor-drafts.js +5 -3
  340. package/src/recovery/store.js +1 -1
  341. package/src/route/route.js +4 -0
  342. package/src/server/rooms/index.js +5 -1
  343. package/src/server/rooms/live-data-rooms.js +302 -50
  344. package/src/server/rooms/page-compaction-worker.js +12 -0
  345. package/src/server/rooms/page-compactor.js +92 -0
  346. package/src/server/rooms/page-room-conformance.js +461 -0
  347. package/src/source/parse-steps.js +94 -0
  348. package/src/source/session.js +233 -57
  349. package/src/sync/editing-sender.js +12 -0
  350. package/src/sync/live-sync-edit.js +22 -2
  351. package/src/sync/live-sync.js +150 -23
  352. package/src/sync/page-relay.js +52 -0
  353. package/src/sync/presence-store.js +44 -4
  354. package/src/sync/presence.js +6 -3
  355. package/src/sync/saved-stream.js +11 -6
  356. package/src/ui/bevel-controls.js +1 -0
  357. package/src/ui/bevel-dialog.js +5 -2
  358. package/src/ui/editor-ui.js +77 -0
  359. package/src/ui/page-colour.js +281 -0
  360. package/src/ui/toast.js +16 -7
  361. package/src/vendor/hyper-html-api-upgrade.vendor.js +2 -2
  362. package/src/vendor/hyper-undo.vendor.js +1 -1
  363. package/src/vendor/hypercms.vendor.js +9 -9
  364. package/src/vendor/parse5.vendor.js +2 -2
  365. package/src/vendor/sap-core.vendor.js +5 -5
  366. package/dist/m/c-2SPVQSHC.js +0 -1
  367. package/dist/m/c-336SGU5W.js +0 -1
  368. package/dist/m/c-3H6BZDNJ.js +0 -6
  369. package/dist/m/c-4DFEJ2PO.js +0 -3
  370. package/dist/m/c-56QRZTZH.js +0 -1
  371. package/dist/m/c-5J4E7O4Y.js +0 -4
  372. package/dist/m/c-5LKDWQTL.js +0 -19
  373. package/dist/m/c-5V3MOVJT.js +0 -1
  374. package/dist/m/c-6AZOGAPE.js +0 -1
  375. package/dist/m/c-6CRUUXOX.js +0 -5
  376. package/dist/m/c-6HMB24EV.js +0 -1
  377. package/dist/m/c-6KEVEQ7L.js +0 -1
  378. package/dist/m/c-A3E3NIMC.js +0 -1
  379. package/dist/m/c-BQ6LOBEE.js +0 -1
  380. package/dist/m/c-BTDI4Y23.js +0 -1
  381. package/dist/m/c-CJ3A7ZKE.js +0 -1
  382. package/dist/m/c-CQHPGKX2.js +0 -1
  383. package/dist/m/c-DFS4XZJV.js +0 -2
  384. package/dist/m/c-DNENWY4O.js +0 -1
  385. package/dist/m/c-E4LIR5X5.js +0 -1
  386. package/dist/m/c-FDNHHZY5.js +0 -1
  387. package/dist/m/c-FROJUZMV.js +0 -1
  388. package/dist/m/c-G2JPKCTP.js +0 -1
  389. package/dist/m/c-GB6TU7WV.js +0 -1
  390. package/dist/m/c-GFJJEBTU.js +0 -1
  391. package/dist/m/c-GJAEGGXY.js +0 -1
  392. package/dist/m/c-GOWBDM6J.js +0 -1
  393. package/dist/m/c-HFKUVACH.js +0 -1
  394. package/dist/m/c-HHDFCNHV.js +0 -1
  395. package/dist/m/c-IEM4NTCS.js +0 -1
  396. package/dist/m/c-JDCX26UP.js +0 -25
  397. package/dist/m/c-KGVGROZT.js +0 -1
  398. package/dist/m/c-KQ6TYJAP.js +0 -135
  399. package/dist/m/c-LFPHCXY3.js +0 -1
  400. package/dist/m/c-LRPUO2MX.js +0 -1
  401. package/dist/m/c-LWLBHLVC.js +0 -3
  402. package/dist/m/c-M2L47S2X.js +0 -7
  403. package/dist/m/c-MFP2ZWD4.js +0 -1
  404. package/dist/m/c-MKSQ4Y57.js +0 -1
  405. package/dist/m/c-NUFFJVVJ.js +0 -1
  406. package/dist/m/c-NUHSPT5P.js +0 -1
  407. package/dist/m/c-O72HVYSA.js +0 -1
  408. package/dist/m/c-OIBUTDXI.js +0 -1
  409. package/dist/m/c-OW5QIGG3.js +0 -1
  410. package/dist/m/c-PGRCK73F.js +0 -1
  411. package/dist/m/c-Q7NQD65V.js +0 -1
  412. package/dist/m/c-QH63LQOS.js +0 -1
  413. package/dist/m/c-R7IWVINQ.js +0 -1
  414. package/dist/m/c-RM7J7BV2.js +0 -1
  415. package/dist/m/c-S4ZEH46B.js +0 -9
  416. package/dist/m/c-SDAXDM2D.js +0 -1
  417. package/dist/m/c-SMGU7RT3.js +0 -235
  418. package/dist/m/c-SPIORRYS.js +0 -1
  419. package/dist/m/c-SWLRT66R.js +0 -1
  420. package/dist/m/c-TCJTVAFN.js +0 -1
  421. package/dist/m/c-TQEMVJCS.js +0 -1
  422. package/dist/m/c-TYLXX4NF.js +0 -21
  423. package/dist/m/c-U62OB4BT.js +0 -1
  424. package/dist/m/c-U6YWUAR3.js +0 -8
  425. package/dist/m/c-UDCSU5G5.js +0 -2
  426. package/dist/m/c-VI5RMSVY.js +0 -1
  427. package/dist/m/c-VRZN56XY.js +0 -1
  428. package/dist/m/c-W5MCHEGA.js +0 -1
  429. package/dist/m/c-WNYBLIOZ.js +0 -1
  430. package/dist/m/c-WWXIDH4O.js +0 -28
  431. package/dist/m/c-XCNTK76U.js +0 -1
  432. package/dist/m/c-XMS6DPBR.js +0 -1
  433. package/dist/m/c-ZUCOKOHT.js +0 -1
  434. package/dist/m/c-ZVPVXTJF.js +0 -4
  435. package/dist/m/c-ZVRKXKM7.js +0 -1
  436. package/dist/m/core/autosave-HRYGUWV6.js +0 -1
  437. package/dist/m/core/conflict-notice-6MROJN2L.js +0 -1
  438. package/dist/m/core/is-edit-mode-KTWA3O5N.js +0 -1
  439. package/dist/m/core/recovery-FLTCC2OC.js +0 -1
  440. package/dist/m/core/save-HQZ4S667.js +0 -1
  441. package/dist/m/core/save-core-KGQWFUNH.js +0 -1
  442. package/dist/m/core/snapshot-EGQZNT45.js +0 -1
  443. package/dist/m/core/unsaved-warning-IDJLD66H.js +0 -1
  444. package/dist/m/loader-S4I7GKXR.js +0 -1
  445. package/dist/m/manifest-4C419D67.js +0 -1
  446. package/dist/m/manifest-edit-B077F288.js +0 -1
  447. package/dist/m/plugins/ai-edit-QN4PMMNL.js +0 -6
  448. package/dist/m/plugins/indicator-URDBBOQO.js +0 -1
  449. package/dist/m/plugins/live-HFWD4OIH.js +0 -1
  450. package/dist/m/plugins/menu-3HIEQSI6.js +0 -138
  451. package/dist/m/plugins/page-JOKB6XPR.js +0 -2
  452. package/dist/m/plugins/page-view-NXOJHNW6.js +0 -1
  453. package/dist/m/plugins/quickcrop-QNY3PKLD.js +0 -26
  454. package/dist/m/plugins/sortable-GWT4PWDV.js +0 -1
  455. package/dist/m/plugins/source-4ZRSAIYM.js +0 -1
  456. package/dist/m/plugins/undo-VDA5P7FH.js +0 -1
  457. package/dist/m/plugins/upgrade-W7FG5VFG.js +0 -1
  458. package/dist/m/plugins/visitor-drafts-LIDY7NH6.js +0 -21
  459. package/dist/m/plugins/visitor-drafts-owner-ZXCKBZE7.js +0 -1
  460. package/dist/m/reader-core-DXYCURFF.js +0 -68
  461. package/dist/m/route/route-KOCZSEGN.js +0 -1
  462. package/dist/m/sync/live-sync-J7VMYTKS.js +0 -1
  463. package/dist/m/ui/index-3JLVI27O.js +0 -1
  464. package/dist/m/vendor/sap-core.vendor-A7BMUYZT.js +0 -6
  465. /package/dist/m/{c-FILLTJA7.js → c-QTTJYJMA.js} +0 -0
  466. /package/dist/m/plugins/{quickcrop-QNY3PKLD.js.LEGAL.txt → quickcrop-BQ5YYFCH.js.LEGAL.txt} +0 -0
  467. /package/dist/m/plugins/{richclay-NRQDSWX6.js.LEGAL.txt → richclay-KLGETK77.js.LEGAL.txt} +0 -0
@@ -119,7 +119,9 @@ export function set(patch, { replace = false } = {}) {
119
119
  history.replaceState(history.state, "", bare + hash);
120
120
  apply();
121
121
  } else if (hash) {
122
+ // Applied now, as the other branches are: the hashchange that follows finds nothing new.
122
123
  location.hash = hash;
124
+ apply();
123
125
  } else {
124
126
  history.pushState(history.state, "", bare);
125
127
  apply();
@@ -161,6 +163,7 @@ export function start() {
161
163
  mirrorRoot(parseHash(location.hash));
162
164
  window.addEventListener("hashchange", apply);
163
165
  document.addEventListener("clay:sync-applied", queueApply);
166
+ document.addEventListener("clay:drafts-restored", queueApply);
164
167
  document.addEventListener("clay:ready", queueApply);
165
168
  if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", onDomReady, { once: true });
166
169
  else onDomReady();
@@ -173,6 +176,7 @@ export function stop() {
173
176
  lastUnmatched = "";
174
177
  window.removeEventListener("hashchange", apply);
175
178
  document.removeEventListener("clay:sync-applied", queueApply);
179
+ document.removeEventListener("clay:drafts-restored", queueApply);
176
180
  document.removeEventListener("clay:ready", queueApply);
177
181
  document.removeEventListener("DOMContentLoaded", onDomReady);
178
182
  if (observer) observer.disconnect();
@@ -57,4 +57,8 @@ export { makePageTransition, pageTransitionState } from './live-page-history.js'
57
57
 
58
58
  export { documentEtag, ifMatchSatisfied } from './spec-wire.js';
59
59
 
60
- export { LIVE_LIMITS, REFUSED_MS, createRooms, roomIsDirty, PersistentReservationError } from './live-data-rooms.js';
60
+ export { LIVE_LIMITS, REFUSED_MS, COMPACTOR, createRooms, roomIsDirty, PersistentReservationError } from './live-data-rooms.js';
61
+
62
+ export { COMPACTION, ROOM_ERROR_STATUS } from '../../live/page-compaction.js';
63
+
64
+ export { createPageCompactor, pageCompactionWorkerUrl } from './page-compactor.js';
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { isDeepStrictEqual } from 'node:util';
3
3
  import { parseJsonRelaxed } from 'hyper-morph/json-parse';
4
- import { validateOp, checkOp, applyOp, applyTracked, undoTracked, sizeChange, cloneJson, serializeLive, textHash, diffOps } from './live-data-ops.js';
4
+ import { validateOp, checkOp, applyOp, applyOps, applyTracked, undoTracked, sizeChange, cloneJson, serializeLive, textHash, diffOps } from './live-data-ops.js';
5
5
  import { liveTags } from './live-data-tags.js';
6
6
  import { checkPageConversion, PageConversionError } from './page-conversion.js';
7
7
  import { checkPageInitial } from './page-write-admission.js';
@@ -10,6 +10,8 @@ import { makePageTransition, pageTransitionState } from './live-page-history.js'
10
10
  export { liveTags } from './live-data-tags.js';
11
11
 
12
12
  import { frameSet, contains, descendants, receiptHas, receiptBytes, saveReceipt, postReceipt, replaceReceipt, retireReceipt, RECEIPT_LIMIT } from './live-provenance.js';
13
+ import { COMPACTION, pageShape, planCompaction, checkUpdate, admitPageOps, createRangeCache, unionFileEntries, buildCandidate } from '../../live/page-compaction.js';
14
+ import { PART_KEY, entriesOf } from '../../page/ybind/parts.js';
13
15
 
14
16
  const byteLength = (text) => Buffer.byteLength(text, 'utf8');
15
17
  const sizeOf = (value) => byteLength(serializeLive(value));
@@ -54,6 +56,7 @@ export const LIVE_LIMITS = Object.freeze({
54
56
  });
55
57
 
56
58
  const PAGE = /^page(\.[0-9]+)?$/;
59
+ export const COMPACTOR = 'compactor';
57
60
  const isContainer = (v) => v !== null && typeof v === 'object';
58
61
 
59
62
  // The stores a room starts with. A store over the store cap is left out, and so
@@ -193,7 +196,26 @@ export class PersistentReservationError extends Error {
193
196
  }
194
197
  }
195
198
 
196
- export function createRooms({ now = Date.now, newEpoch = randomUUID, liveBytes = LIVE_LIMITS.liveBytes } = {}) {
199
+ export function createRooms({ now = Date.now, newEpoch = randomUUID, liveBytes = LIVE_LIMITS.liveBytes, compaction = null } = {}) {
200
+ const hostLimits = compaction ? Object.freeze({ ...COMPACTION, ...(typeof compaction === 'object' ? compaction : {}) }) : null;
201
+ const jobs = new WeakMap();
202
+ let onDue = () => {};
203
+ const hostMode = (yjs) => (hostLimits && yjs ? { fence: 0, job: null, lastFull: null, deletesSince: 0, contentSeq: 0, failed: null, retired: new Set(), legacy: new Set(), ranges: createRangeCache() } : null);
204
+ // A copy of a room to try a file against. Its compaction state is room data plus a
205
+ // running job and a range cache, which hold functions: the copy starts with neither.
206
+ const projectRoom = (room) => {
207
+ const compaction = room.compaction;
208
+ if (!compaction) return structuredClone(room);
209
+ room.compaction = null;
210
+ let projected;
211
+ try { projected = structuredClone(room); } finally { room.compaction = compaction; }
212
+ projected.compaction = { ...structuredClone({ ...compaction, job: null, ranges: null }), ranges: createRangeCache() };
213
+ return projected;
214
+ };
215
+ const due = (room, overflow = false) => {
216
+ if (!room.compaction || room.compaction.job || room.compaction.failed) return;
217
+ try { onDue(room, { overflow }); } catch {}
218
+ };
197
219
  const rooms = new Map();
198
220
  const pageReservations = new Map();
199
221
  const reservedNodes = new Map();
@@ -218,6 +240,7 @@ export function createRooms({ now = Date.now, newEpoch = randomUUID, liveBytes =
218
240
  seen: new Map(),
219
241
  emptySince: null,
220
242
  ...(inspectPage(html).kind === 'yjs' && { pageTransition: makePageTransition() }),
243
+ compaction: hostMode(inspectPage(html).kind === 'yjs'),
221
244
  });
222
245
 
223
246
  const roomBytes = (room) => {
@@ -226,11 +249,97 @@ export function createRooms({ now = Date.now, newEpoch = randomUUID, liveBytes =
226
249
  return total;
227
250
  };
228
251
  const heldBy = (room) => {
229
- let total = (room.receiptBytes ?? 0) + (room.pageTransition?.bytes ?? 0);
252
+ let total = (room.receiptBytes ?? 0) + (room.pageTransition?.bytes ?? 0) + (room.compaction?.job?.reserved ?? 0);
230
253
  for (const s of room.stores.values()) total += s.bytes + s.logBytes;
231
254
  return total;
232
255
  };
233
256
 
257
+ const admit = (room, store, frame, ops) => {
258
+ const state = room.compaction;
259
+ const answer = admitPageOps({ y: store.value.y, ops, state: { ...room.compaction, compaction: frame.compaction }, limits: hostLimits });
260
+ if (answer.error) {
261
+ if (answer.error === 'checkpoint-required') due(room);
262
+ return answer;
263
+ }
264
+ const { added, deleted, limit, shape } = answer;
265
+ return {
266
+ limit,
267
+ over() {
268
+ if (answer.failed || (state.lastFull && state.deletesSince === 0)) return { error: 'page-full' };
269
+ due(room, true);
270
+ return { error: 'checkpoint-required', retryAfter: 0 };
271
+ },
272
+ admitted(bytes) {
273
+ state.deletesSince += deleted;
274
+ state.contentSeq = room.seq;
275
+ if (answer.failed) return;
276
+ if (bytes > hostLimits.activeBytes) due(room, true);
277
+ else if (shape.loose + added >= hostLimits.soft) due(room);
278
+ },
279
+ };
280
+ };
281
+
282
+ const mergePageFile = (room, store, value) => {
283
+ const state = room.compaction;
284
+ if (value.g !== store.value.g) return null;
285
+ const adds = unionFileEntries({
286
+ y: store.value.y,
287
+ fileY: value.y,
288
+ retired: state.retired,
289
+ rangesFor: state.ranges,
290
+ keyFor: (i) => `f${textHash(room.epoch).slice(0, 8)}-${(room.seq + 1).toString(36)}-${i.toString(36)}`,
291
+ });
292
+ const seq = room.seq + 1;
293
+ const taken = new Set();
294
+ const ops = [];
295
+ for (const [key, v] of adds) {
296
+ if (Object.hasOwn(store.value.y, key) || taken.has(key)) continue;
297
+ taken.add(key);
298
+ ops.push(validateOp({ op: 'set', path: ['y', key], value: v }));
299
+ }
300
+ if (!ops.length) return null;
301
+ const fits = (n) => n <= LIVE_LIMITS.storeBytes && roomBytes(room) - store.bytes + n <= LIVE_LIMITS.roomBytes;
302
+ let next = applyOps(cloneJson(store.value), ops);
303
+ let bytes = sizeOf(next);
304
+ if (!fits(bytes)) {
305
+ // A file from an older tab repeats most of the page, so the plain union can be twice
306
+ // its real size: fold the room and the file into one checkpoint before giving up.
307
+ const built = buildCandidate({ kind: 'full', sources: [...entriesOf(store.value.y), ...adds] });
308
+ if (!built.update || built.pending) return 'dropped';
309
+ const retire = Object.keys(store.value.y);
310
+ ops.length = 0;
311
+ for (const key of retire) ops.push(validateOp({ op: 'del', path: ['y', key] }));
312
+ ops.push(validateOp({ op: 'set', path: ['y', `c${textHash(room.epoch).slice(0, 8)}-${seq.toString(36)}`], value: built.update }));
313
+ next = applyOps(cloneJson(store.value), ops);
314
+ bytes = sizeOf(next);
315
+ if (!fits(bytes)) return 'dropped';
316
+ for (const [key] of entriesOf(store.value.y)) {
317
+ state.retired.add(key);
318
+ if (state.retired.size > hostLimits.retired) state.retired.delete(state.retired.values().next().value);
319
+ }
320
+ }
321
+ const logged = byteLength(JSON.stringify(ops));
322
+ store.value = next;
323
+ store.bytes = bytes;
324
+ room.seq = seq;
325
+ store.last = seq;
326
+ store.log.push({ seq, ops, bytes: logged, merge: true });
327
+ store.inFile.add(seq);
328
+ store.logBytes += logged;
329
+ evict(store);
330
+ state.contentSeq = seq;
331
+ state.lastFull = null;
332
+ due(room);
333
+ return true;
334
+ };
335
+
336
+ const endJob = (job) => {
337
+ if (job.room.compaction?.job !== job) return false;
338
+ job.room.compaction.job = null;
339
+ if (rooms.get(job.room.key) === job.room) used -= job.reserved;
340
+ return true;
341
+ };
342
+
234
343
  const pageSource = (key, nodeId) => {
235
344
  if (nodeId == null) throw new PageConversionError('invalid-document');
236
345
  if (pageReservations.has(key) || persistentReservations.has(key) || reservedNodes.has(String(nodeId))) throw new PageConversionError('page-converting');
@@ -308,13 +417,14 @@ export function createRooms({ now = Date.now, newEpoch = randomUUID, liveBytes =
308
417
  if (tag.name !== 'page' || !isYPageValue(value)) continue;
309
418
  } else if (isYPageValue(value, { complete: false })) continue;
310
419
  }
420
+ const host = Boolean(room.compaction && tag.name === 'page' && room.pageTransition?.format === 'yjs');
311
421
  const before = store.landed;
312
422
  const base = baseOf(room, store, tag.at);
313
423
  // A capture (stamped, and still the value its stamp names) holds no merge
314
424
  // past its stamp. Anything else descends from the file on disk, which
315
425
  // holds the merges in inFile: they are not replayed over it.
316
426
  const stamped = placed(room, store, tag.at) && tag.at.hash === textHash(serializeLive(value));
317
- if (base < store.floor) {
427
+ if (!host && base < store.floor) {
318
428
  store.landed = -1;
319
429
  if (before !== -1) landed.push(tag.name);
320
430
  continue;
@@ -322,53 +432,69 @@ export function createRooms({ now = Date.now, newEpoch = randomUUID, liveBytes =
322
432
  // A restore proves neither frame inclusion nor descent from a capture.
323
433
  const by = restored ? null : provenBy(room, tag.name, tag, stamped);
324
434
  const frames = tag.at?.epoch === room.epoch ? frameSet(by) : [];
325
- const ancestry = descendants(by);
326
- const own = (entry) => {
327
- if (!by) return false;
328
- if (!entry.merge) return contains(frames, entry.from, entry.n);
329
- const p = entry.provenance;
330
- return by.v === 2 && p && p.at.epoch === tag.at.epoch && p.at.seq === tag.at.seq && contains(ancestry, ...p.capture);
331
- };
332
- if (stamped) store.inFile.clear();
333
- const entries = [];
334
- for (const entry of store.log) {
335
- if (entry.seq <= base) continue;
336
- if (own(entry)) store.inFile.add(entry.seq);
337
- else if (stamped || !store.inFile.has(entry.seq)) entries.push(entry);
338
- }
339
- for (const entry of entries) {
340
- for (const op of entry.ops) {
341
- try {
342
- checkOp(value, op);
343
- value = applyOp(value, op);
344
- } catch {}
345
- }
346
- }
347
- const text = serializeLive(value);
348
- if (text !== serializeLive(store.value)) {
349
- const bytes = byteLength(text);
350
- if (bytes > LIVE_LIMITS.storeBytes || roomBytes(room) - store.bytes + bytes > LIVE_LIMITS.roomBytes) {
435
+ if (host) {
436
+ // No log replay: the room takes in every edit the file holds and that it
437
+ // lacks, each under a fresh key, and never deletes or overwrites its own.
438
+ const merged = mergePageFile(room, store, value);
439
+ if (merged === 'dropped') {
351
440
  room.stores.delete(tag.name);
352
441
  valueChanged = true;
353
442
  continue;
354
443
  }
355
- // Logged like a frame, so a capture stamped before this merge gets it
356
- // replayed on top (leaf ops: a capture that holds it already is unchanged).
357
- const ops = diffOps(store.value, value);
358
- store.value = value;
359
- store.bytes = bytes;
360
- room.seq += 1;
361
- store.last = room.seq;
362
- const provenance = by?.v === 2 ? { at: { epoch: tag.at.epoch, seq: tag.at.seq }, by: by.frames, capture: by.capture } : null;
363
- const logged = byteLength(JSON.stringify(provenance ? { ops, provenance } : ops));
364
- store.log.push({ seq: room.seq, ops, bytes: logged, merge: true, ...(provenance && { provenance }) });
365
- store.inFile.add(room.seq);
366
- store.logBytes += logged;
367
- evict(store);
368
- valueChanged = true;
444
+ if (merged) valueChanged = true;
445
+ if (stamped && tag.at.epoch === room.epoch) {
446
+ store.landed = tag.at.seq >= room.compaction.contentSeq ? store.last : tag.at.seq;
447
+ if (store.landed !== before) landed.push(tag.name);
448
+ }
449
+ } else {
450
+ const ancestry = descendants(by);
451
+ const own = (entry) => {
452
+ if (!by) return false;
453
+ if (!entry.merge) return contains(frames, entry.from, entry.n);
454
+ const p = entry.provenance;
455
+ return by.v === 2 && p && p.at.epoch === tag.at.epoch && p.at.seq === tag.at.seq && contains(ancestry, ...p.capture);
456
+ };
457
+ if (stamped) store.inFile.clear();
458
+ const entries = [];
459
+ for (const entry of store.log) {
460
+ if (entry.seq <= base) continue;
461
+ if (own(entry)) store.inFile.add(entry.seq);
462
+ else if (stamped || !store.inFile.has(entry.seq)) entries.push(entry);
463
+ }
464
+ for (const entry of entries) {
465
+ for (const op of entry.ops) {
466
+ try {
467
+ checkOp(value, op);
468
+ value = applyOp(value, op);
469
+ } catch {}
470
+ }
471
+ }
472
+ const text = serializeLive(value);
473
+ if (text !== serializeLive(store.value)) {
474
+ const bytes = byteLength(text);
475
+ if (bytes > LIVE_LIMITS.storeBytes || roomBytes(room) - store.bytes + bytes > LIVE_LIMITS.roomBytes) {
476
+ room.stores.delete(tag.name);
477
+ valueChanged = true;
478
+ continue;
479
+ }
480
+ // Logged like a frame, so a capture stamped before this merge gets it
481
+ // replayed on top (leaf ops: a capture that holds it already is unchanged).
482
+ const ops = diffOps(store.value, value);
483
+ store.value = value;
484
+ store.bytes = bytes;
485
+ room.seq += 1;
486
+ store.last = room.seq;
487
+ const provenance = by?.v === 2 ? { at: { epoch: tag.at.epoch, seq: tag.at.seq }, by: by.frames, capture: by.capture } : null;
488
+ const logged = byteLength(JSON.stringify(provenance ? { ops, provenance } : ops));
489
+ store.log.push({ seq: room.seq, ops, bytes: logged, merge: true, ...(provenance && { provenance }) });
490
+ store.inFile.add(room.seq);
491
+ store.logBytes += logged;
492
+ evict(store);
493
+ valueChanged = true;
494
+ }
495
+ store.landed = entries.length ? base : store.last;
496
+ if (store.landed !== before) landed.push(tag.name);
369
497
  }
370
- store.landed = entries.length ? base : store.last;
371
- if (store.landed !== before) landed.push(tag.name);
372
498
  // Saves record exact inclusion; only legacy captures advance a watermark.
373
499
  if (by?.v === 2) {
374
500
  for (const [client, ranges] of frames) {
@@ -449,6 +575,7 @@ export function createRooms({ now = Date.now, newEpoch = randomUUID, liveBytes =
449
575
  reservation.result = null;
450
576
  return null;
451
577
  }
578
+ if (room.compaction?.job) endJob(room.compaction.job);
452
579
  const before = heldBy(room);
453
580
  const { stores, transition } = reservation.target;
454
581
  for (const name of room.stores.keys()) if (PAGE.test(name)) room.stores.delete(name);
@@ -465,6 +592,7 @@ export function createRooms({ now = Date.now, newEpoch = randomUUID, liveBytes =
465
592
  bytes += store.bytes;
466
593
  }
467
594
  room.pageTransition = transition;
595
+ if (hostLimits) room.compaction = hostMode(transition.format === 'yjs');
468
596
  reservation.target = null;
469
597
  used += heldBy(room) - before - reservation.budget;
470
598
  reservation.budget = 0;
@@ -547,7 +675,7 @@ export function createRooms({ now = Date.now, newEpoch = randomUUID, liveBytes =
547
675
  if (!store || store.ephemeral) throw new PersistentReservationError('invalid-target');
548
676
  }
549
677
  const currentHeld = heldBy(record.room);
550
- const projected = structuredClone(record.room);
678
+ const projected = projectRoom(record.room);
551
679
  reconcileFile(projected, record.key, html, {});
552
680
  for (const [name, store] of record.room.stores) {
553
681
  if (!store.ephemeral && !projected.stores.has(name)) throw new PersistentReservationError('budget');
@@ -738,6 +866,8 @@ export function createRooms({ now = Date.now, newEpoch = randomUUID, liveBytes =
738
866
  }
739
867
  const nKey = frame.name + '\n' + clientId;
740
868
  const last = room.lastN.get(nKey);
869
+ const admission = room.compaction && frame.name === 'page' && !receiptHas(last, frame.n) ? admit(room, store, frame, ops) : null;
870
+ if (admission?.error) return admission;
741
871
  // Answered with the seq the sender's last frame landed at, so the tab knows
742
872
  // which applied state holds it.
743
873
  if (receiptHas(last, frame.n)) return { duplicate: true, seq: frame.n <= last.n ? last.seq : last.savedSeq, epoch: room.epoch };
@@ -793,8 +923,13 @@ export function createRooms({ now = Date.now, newEpoch = randomUUID, liveBytes =
793
923
  }
794
924
  if (bytes > LIVE_LIMITS.storeBytes || roomBytes(room) - store.bytes + bytes > LIVE_LIMITS.roomBytes) {
795
925
  undo();
926
+ if (admission) return admission.over(bytes);
796
927
  return { error: 'too-large' };
797
928
  }
929
+ if (admission && bytes > admission.limit) {
930
+ undo();
931
+ return admission.over(bytes);
932
+ }
798
933
  const logged = done.length ? byteLength(JSON.stringify(ops)) : 0;
799
934
  const grows = done.length ? bytes - store.bytes + logAfter(store, logged) - store.logBytes : 0;
800
935
  const retiredBytes = victim === null ? 0 : receiptBytes(room.lastN.get(victim));
@@ -819,19 +954,28 @@ export function createRooms({ now = Date.now, newEpoch = randomUUID, liveBytes =
819
954
  room.lastSentAt.set(clientId, now());
820
955
  room.seen.set(clientId, now());
821
956
  if (PAGE.test(frame.name) && typeof frame.adopted === 'string' && frame.adopted.length <= 64) room.adopted = frame.adopted;
957
+ if (admission && done.length) admission.admitted(bytes);
822
958
  const writerGone = room.writer && connected && !connected.includes(room.writer);
823
959
  if ((!room.writer || writerGone) && (!connected || connected.includes(clientId))) room.writer = clientId;
824
960
  return { seq: room.seq, epoch: room.epoch, name: frame.name, ops, writer: room.writer, checkpoint: pressed(store), receiptGeneration: room.receiptGeneration ?? 0, ...(transition && { pageGeneration: transition.id }) };
825
961
  },
826
962
 
827
963
  /** Everything a joining tab needs, with clientId's own receipt per store. Values are the room's own objects: serialize, never mutate. With since in this room's epoch, moves carries each store's log entries after since.seq (null where the log no longer reaches back that far). */
828
- state(room, clientId = null, since = undefined) {
964
+ state(room, clientId = null, since = undefined, { compaction: capable } = {}) {
829
965
  const stores = {};
966
+ const legacy = room.compaction?.legacy;
967
+ if (legacy && clientId && capable !== undefined) {
968
+ if (capable === hostLimits.version) legacy.delete(clientId);
969
+ else legacy.add(clientId);
970
+ }
971
+ const hidden = Boolean(legacy && (capable !== undefined ? capable !== hostLimits.version : clientId && legacy.has(clientId)));
830
972
  for (const [name, s] of room.stores) {
973
+ if (hidden && name === 'page') continue;
831
974
  const seen = {};
832
975
  const mine = clientId ? room.lastN.get(name + '\n' + clientId) : null;
833
976
  if (mine) seen[clientId] = mine.n;
834
- stores[name] = { value: s.value, ephemeral: s.ephemeral, from: s.from, landed: s.landed, floor: s.floor, last: s.last, seen, ...(mine?.saved?.length && { saved: mine.saved }) };
977
+ stores[name] = { value: s.value, ephemeral: s.ephemeral, from: s.from, landed: s.landed, floor: s.floor, last: s.last, seen, ...(mine?.saved?.length && { saved: mine.saved }),
978
+ ...(name === 'page' && room.compaction && { compaction: { v: hostLimits.version, loose: hostLimits.loose, activeBytes: hostLimits.activeBytes } }) };
835
979
  }
836
980
  // A client's own read (since is its { epoch, seq }, or null for a first
837
981
  // read) carries moves: those after its seq in its own epoch, else this
@@ -876,7 +1020,11 @@ export function createRooms({ now = Date.now, newEpoch = randomUUID, liveBytes =
876
1020
  if (room.writer !== clientId) return { writerChanged: false, writer: room.writer };
877
1021
  let best = null;
878
1022
  let bestAt = -1;
879
- for (const id of remaining.filter((candidate) => room.seen.has(candidate))) {
1023
+ // A tab too old to see the page store cannot checkpoint it: a capable tab goes first.
1024
+ const seen = remaining.filter((candidate) => room.seen.has(candidate));
1025
+ const legacy = room.compaction?.legacy;
1026
+ const capable = legacy ? seen.filter((candidate) => !legacy.has(candidate)) : seen;
1027
+ for (const id of capable.length ? capable : seen) {
880
1028
  const at = room.lastSentAt.get(id) ?? 0;
881
1029
  if (at > bestAt) {
882
1030
  best = id;
@@ -898,6 +1046,9 @@ export function createRooms({ now = Date.now, newEpoch = randomUUID, liveBytes =
898
1046
  */
899
1047
  sweep(connectedOf = () => [], { mayExpire = () => true } = {}) {
900
1048
  const t = now();
1049
+ for (const room of rooms.values()) {
1050
+ if (room.compaction?.job && t >= room.compaction.job.deadline) endJob(room.compaction.job);
1051
+ }
901
1052
  for (const [key, room] of rooms) {
902
1053
  if (!roomIsDirty(room) && mayExpire(room) && !pageReservations.has(key) && !persistentReservations.has(key) && room.emptySince !== null && t - room.emptySince >= LIVE_LIMITS.retainMs) {
903
1054
  used -= heldBy(room);
@@ -948,6 +1099,107 @@ export function createRooms({ now = Date.now, newEpoch = randomUUID, liveBytes =
948
1099
  return changed;
949
1100
  },
950
1101
 
1102
+ onCompactionDue(listener) {
1103
+ onDue = typeof listener === 'function' ? listener : () => {};
1104
+ },
1105
+
1106
+ beginCompaction(room, { overflow = false } = {}) {
1107
+ const state = room?.compaction;
1108
+ if (!state || rooms.get(room.key) !== room || state.failed) return null;
1109
+ if (state.job) {
1110
+ if (now() < state.job.deadline) return null;
1111
+ endJob(state.job);
1112
+ }
1113
+ const store = room.stores.get('page');
1114
+ if (!store || pageReservations.has(room.key) || persistentReservations.has(room.key)) return null;
1115
+ const plan = planCompaction(store.value.y, { overflow }, hostLimits);
1116
+ if (!plan) return null;
1117
+ const reserved = plan.sources.reduce((sum, [k, v]) => sum + k.length + v.length, 0);
1118
+ if (used + reserved > liveBytes) return null;
1119
+ state.fence += 1;
1120
+ const token = Object.freeze({ key: room.key, epoch: room.epoch, fence: state.fence });
1121
+ const job = { token, room, store, epoch: room.epoch, generation: room.pageTransition?.id ?? null, kind: plan.kind,
1122
+ sources: new Map(plan.sources), physical: Object.keys(store.value.y), reserved, deadline: now() + hostLimits.leaseMs, deletesAtBegin: state.deletesSince };
1123
+ state.job = job;
1124
+ used += reserved;
1125
+ jobs.set(token, job);
1126
+ return { token, kind: plan.kind, sources: plan.sources };
1127
+ },
1128
+
1129
+ abandonCompaction(token, { failed = null } = {}) {
1130
+ const job = jobs.get(token);
1131
+ if (!job) return false;
1132
+ jobs.delete(token);
1133
+ const ended = endJob(job);
1134
+ if (ended && failed) job.room.compaction.failed = { reason: failed, at: now() };
1135
+ return ended;
1136
+ },
1137
+
1138
+ commitCompaction(token, result) {
1139
+ const job = jobs.get(token);
1140
+ if (!job) return { refused: 'stale-token' };
1141
+ jobs.delete(token);
1142
+ const { room, store } = job;
1143
+ if (!endJob(job)) return { refused: 'stale-token' };
1144
+ if (rooms.get(room.key) !== room || room.epoch !== job.epoch || room.stores.get('page') !== store ||
1145
+ (room.pageTransition?.id ?? null) !== job.generation) return { refused: 'stale-token' };
1146
+ const y = store.value.y;
1147
+ const retire = job.physical.filter((k) => {
1148
+ const part = PART_KEY.exec(k);
1149
+ return job.sources.has(part ? part[1] : k) || (part && job.kind === 'full');
1150
+ });
1151
+ if (retire.some((k) => !Object.hasOwn(y, k))) return { refused: 'sources-changed' };
1152
+ const held = new Map(entriesOf(Object.fromEntries(retire.map((k) => [k, y[k]]))));
1153
+ for (const [k, v] of job.sources) if (held.get(k) !== v) return { refused: 'sources-changed' };
1154
+ if (typeof result?.update !== 'string' || !checkUpdate(result.update)) return { refused: 'bad-candidate' };
1155
+ const key = `${job.kind === 'full' ? 'c' : 't'}${textHash(room.epoch).slice(0, 8)}-${(room.seq + 1).toString(36)}`;
1156
+ if (Object.hasOwn(y, key)) return { refused: 'bad-candidate' };
1157
+ const ops = [...retire.map((k) => ({ op: 'del', path: ['y', k] })), { op: 'set', path: ['y', key], value: result.update }].map(validateOp);
1158
+ let value = store.value;
1159
+ let bytes = store.bytes;
1160
+ const done = [];
1161
+ for (const op of ops) {
1162
+ const rec = applyTracked(value, op);
1163
+ if (!rec) continue;
1164
+ done.push(rec);
1165
+ bytes += sizeChange(rec, sizeOf, bytes);
1166
+ value = rec.root;
1167
+ }
1168
+ if (bytes > hostLimits.activeBytes && bytes > store.bytes) {
1169
+ for (let i = done.length - 1; i >= 0; i--) undoTracked(done[i]);
1170
+ // A full fold of the whole page that still does not fit: the page is as small as it
1171
+ // gets, so the next write is refused as full instead of waiting for another fold.
1172
+ if (job.kind === 'full' && retire.length === Object.keys(y).length) {
1173
+ room.compaction.lastFull = { bytes: store.bytes, seq: room.seq, deletesAtBegin: job.deletesAtBegin };
1174
+ room.compaction.deletesSince -= job.deletesAtBegin;
1175
+ }
1176
+ return { refused: 'too-large' };
1177
+ }
1178
+ const clean = store.landed === store.last;
1179
+ const logged = byteLength(JSON.stringify(ops));
1180
+ used += bytes - store.bytes + logAfter(store, logged) - store.logBytes;
1181
+ store.value = value;
1182
+ store.bytes = bytes;
1183
+ room.seq += 1;
1184
+ store.last = room.seq;
1185
+ if (clean) store.landed = store.last;
1186
+ store.log.push({ seq: room.seq, ops, bytes: logged, from: COMPACTOR, n: 0 });
1187
+ store.logBytes += logged;
1188
+ evict(store);
1189
+ const state = room.compaction;
1190
+ for (const k of job.sources.keys()) {
1191
+ state.retired.add(k);
1192
+ if (state.retired.size > hostLimits.retired) state.retired.delete(state.retired.values().next().value);
1193
+ }
1194
+ if (job.kind === 'full') {
1195
+ state.lastFull = { bytes, seq: room.seq, deletesAtBegin: job.deletesAtBegin };
1196
+ state.deletesSince -= job.deletesAtBegin;
1197
+ }
1198
+ if (pageShape(store.value.y, hostLimits).loose >= hostLimits.soft || !pageShape(store.value.y, hostLimits).normal) due(room);
1199
+ return { seq: room.seq, epoch: room.epoch, name: 'page', ops, from: COMPACTOR, kind: job.kind, landed: store.landed,
1200
+ ...(room.pageTransition && { pageGeneration: room.pageTransition.id }) };
1201
+ },
1202
+
951
1203
  /** Every room's key. */
952
1204
  keys() {
953
1205
  return [...rooms.keys()];
@@ -0,0 +1,12 @@
1
+ import { parentPort } from 'node:worker_threads';
2
+ import { buildCandidate } from '../../live/page-compaction.js';
3
+
4
+ parentPort.on('message', ({ id, kind, sources }) => {
5
+ let result;
6
+ try {
7
+ result = buildCandidate({ kind, sources });
8
+ } catch (error) {
9
+ result = { failure: 'crashed', message: error?.message || String(error) };
10
+ }
11
+ parentPort.postMessage({ id, ...result });
12
+ });
@@ -0,0 +1,92 @@
1
+ import { Worker } from 'node:worker_threads';
2
+ import { pathToFileURL } from 'node:url';
3
+
4
+ const RETRY_MS = [1000, 5000, 30000];
5
+
6
+ const MODULE_URL = typeof __dirname === 'string' ? pathToFileURL(`${__dirname}/`).href : import.meta.url;
7
+ const WORKER_FILE = typeof __PAGE_COMPACTION_WORKER__ === 'string' ? __PAGE_COMPACTION_WORKER__ : './page-compaction-worker.js';
8
+
9
+ export const pageCompactionWorkerUrl = () => new URL(WORKER_FILE, MODULE_URL);
10
+
11
+ export function createPageCompactor({ rooms, onCommit = () => {}, workerUrl = pageCompactionWorkerUrl(),
12
+ timeoutMs = 30000, WorkerClass = Worker, setTimer = setTimeout, clearTimer = clearTimeout } = {}) {
13
+ const queue = new Map();
14
+ const retries = new Map();
15
+ let worker = null;
16
+ let running = null;
17
+ let ids = 0;
18
+ let closed = false;
19
+
20
+ // A worker's events count only while it is still the current one: a terminated worker
21
+ // exits on a later tick, after its replacement may already be running the next job.
22
+ const spawn = () => {
23
+ const instance = new WorkerClass(workerUrl);
24
+ worker = instance;
25
+ instance.unref?.();
26
+ instance.on('message', (message) => { if (worker === instance) finish(message); });
27
+ instance.on('error', () => { if (worker === instance) crashed(); });
28
+ instance.on('exit', () => { if (worker !== instance) return; worker = null; if (running) crashed(); });
29
+ };
30
+
31
+ function crashed() {
32
+ const job = running;
33
+ if (!job) return;
34
+ running = null;
35
+ clearTimer(job.timer);
36
+ worker?.terminate?.();
37
+ worker = null;
38
+ const tries = (retries.get(job.room) ?? 0) + 1;
39
+ retries.set(job.room, tries);
40
+ rooms.abandonCompaction(job.token, tries > RETRY_MS.length ? { failed: 'crashed' } : {});
41
+ if (tries <= RETRY_MS.length) setTimer(() => due(job.room, { overflow: job.overflow }), RETRY_MS[tries - 1]);
42
+ pump();
43
+ }
44
+
45
+ function finish({ id, ...result }) {
46
+ const job = running;
47
+ if (!job || job.id !== id) return;
48
+ running = null;
49
+ clearTimer(job.timer);
50
+ if (result.failure) {
51
+ rooms.abandonCompaction(job.token, { failed: result.failure });
52
+ } else {
53
+ const answer = rooms.commitCompaction(job.token, result);
54
+ if (answer.seq !== undefined) {
55
+ retries.delete(job.room);
56
+ try { onCommit(job.room, answer); } catch {}
57
+ }
58
+ }
59
+ pump();
60
+ }
61
+
62
+ function pump() {
63
+ if (closed || running || !queue.size) return;
64
+ const [room, { overflow }] = queue.entries().next().value;
65
+ queue.delete(room);
66
+ const begun = rooms.beginCompaction(room, { overflow });
67
+ if (!begun) { pump(); return; }
68
+ if (!worker) spawn();
69
+ const id = ++ids;
70
+ running = { id, room, overflow, token: begun.token, timer: setTimer(crashed, timeoutMs) };
71
+ worker.postMessage({ id, kind: begun.kind, sources: begun.sources });
72
+ }
73
+
74
+ function due(room, { overflow = false } = {}) {
75
+ const queued = queue.get(room);
76
+ queue.set(room, { overflow: overflow || Boolean(queued?.overflow) });
77
+ queueMicrotask(pump);
78
+ }
79
+
80
+ rooms.onCompactionDue(due);
81
+ return {
82
+ get running() { return running !== null; },
83
+ close() {
84
+ closed = true;
85
+ rooms.onCompactionDue(null);
86
+ if (running) { clearTimer(running.timer); rooms.abandonCompaction(running.token); running = null; }
87
+ const current = worker;
88
+ worker = null;
89
+ return current?.terminate?.();
90
+ },
91
+ };
92
+ }