@panphora/clayjs 1.17.0 → 1.18.1

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-26C55578.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
@@ -52,9 +52,9 @@ export function hasUnsavedState() {
52
52
  // Ask every pending source that can finish on its own (a checkpoint, a queued
53
53
  // upload) to do so, and wait. Sources without a flush (an open composition)
54
54
  // stay pending; they are listed in the answer, never waited on.
55
- export async function flushUnsaved() {
55
+ export async function flushUnsaved(context = {}) {
56
56
  const flushes = [...sources.values()].filter((s) => typeof s.flush === 'function' && isPending(s));
57
- const results = await Promise.all(flushes.map((s) => Promise.resolve().then(s.flush).catch(() => ({ ok: false }))));
57
+ const results = await Promise.all(flushes.map((s) => Promise.resolve().then(() => s.flush(context)).catch(() => ({ ok: false }))));
58
58
  return { ok: results.every((r) => r?.ok !== false), pending: pendingIds() };
59
59
  }
60
60
 
@@ -231,7 +231,7 @@ export function createFastLane({
231
231
  lastSentAt = now();
232
232
  let result;
233
233
  try {
234
- result = await transport.send(clientId, { name, n: frame.n, ops: frame.ops, epoch: sentEpoch, ...(conn !== undefined && { conn }), ...(frame.receiptGeneration !== null && { receiptGeneration: frame.receiptGeneration }), ...(frame.pageGeneration !== null && PAGE_STORE.test(name) && { pageGeneration: frame.pageGeneration }), ...(frame.adopted && { adopted: frame.adopted }) });
234
+ result = await transport.send(clientId, { name, n: frame.n, ops: frame.ops, epoch: sentEpoch, ...(conn !== undefined && { conn }), ...(frame.receiptGeneration !== null && { receiptGeneration: frame.receiptGeneration }), ...(frame.pageGeneration !== null && PAGE_STORE.test(name) && { pageGeneration: frame.pageGeneration }), ...(frame.adopted && { adopted: frame.adopted }), ...(PAGE_STORE.test(name) && lane.compaction?.v === 1 && { compaction: 1 }) });
235
235
  } catch {
236
236
  result = null;
237
237
  }
@@ -386,6 +386,12 @@ export function createFastLane({
386
386
 
387
387
  function apply(frame) {
388
388
  seq = frame.seq;
389
+ if (frame.from === "compactor") {
390
+ holdUntil = 0;
391
+ if (timer !== null) clearTimer(timer);
392
+ timer = null;
393
+ schedule();
394
+ }
389
395
  const store = entries.get(frame.name)?.store;
390
396
  if (!store || abandoned.has(frame.name)) return;
391
397
  if (!roomNames.has(frame.name)) {
@@ -510,6 +516,7 @@ export function createFastLane({
510
516
  prune(name, lane);
511
517
  const held = lane.room;
512
518
  lane.room = !!room;
519
+ lane.compaction = room?.compaction ?? null;
513
520
  if (!room) {
514
521
  // Not in the room (too large, too many stores, or opened after it):
515
522
  // there are no frames of it to miss. Already out: its value still
@@ -569,7 +576,7 @@ export function createFastLane({
569
576
  frame.pageGeneration = pageGeneration;
570
577
  }
571
578
  lane.awaiting = resend;
572
- entry.store.rebase(room.value, { source: "state", dropIds, room: { seq, fresh: true, moves: state.moves?.[name] ?? null, floor: Number.isInteger(room.floor) ? room.floor : null } });
579
+ entry.store.rebase(room.value, { source: "state", dropIds, room: { seq, fresh: true, moves: state.moves?.[name] ?? null, floor: Number.isInteger(room.floor) ? room.floor : null, compaction: lane.compaction } });
573
580
  lane.unsent = unframed(name, lane);
574
581
  } else {
575
582
  const seen = room.seen ? room.seen[clientId] : undefined;
@@ -599,7 +606,7 @@ export function createFastLane({
599
606
  }
600
607
  }
601
608
  lane.awaiting = next;
602
- entry.store.rebase(room.value, { source: "state", dropIds, room: { seq, fresh: false, moves: state.moves?.[name] ?? null, floor: Number.isInteger(room.floor) ? room.floor : null } });
609
+ entry.store.rebase(room.value, { source: "state", dropIds, room: { seq, fresh: false, moves: state.moves?.[name] ?? null, floor: Number.isInteger(room.floor) ? room.floor : null, compaction: lane.compaction } });
603
610
  // Back in the room after a spell at save speed: frame what was made meanwhile.
604
611
  if (rejoined) {
605
612
  lane.unsent = unframed(name, lane);
@@ -17,13 +17,25 @@
17
17
  // stamp (or, without one, the last version known to be in the file) picks
18
18
  // where in the log it starts, the moves after that go back on top, and the
19
19
  // merge itself is logged as a diff so a later file stamped before it keeps it.
20
+ //
21
+ // In the room scope the page store is a host-mode room, compacted by the tab that
22
+ // sequences it: an update is one whole client key, a refusal answers with the
23
+ // host's code, and a due room folds its keys in an idle task (a tail merge or a
24
+ // full, each proven before it is written) and sends the commit as a move of its
25
+ // own. A save merges as a union of keys, under names this room makes. The scope is
26
+ // part of the lane key, so a tab of the lane before it never shares a sequencer.
20
27
 
21
28
  import { localChannel } from "./local-channel.js";
22
29
  import { applyOp, applyTracked, checkOp, cloneJson, undoTracked, validateOp, serializeLive, textHash, diffOps } from "./ops.js";
23
30
  import { parseJsonRelaxed } from "../vendor/hyper-morph-json.vendor.js";
24
31
  import { movesSince } from "./moves.js";
32
+ import { PART_KEY, entriesOf } from "../page/ybind/parts.js";
33
+ import { COMPACTION, ROOM_ERROR_STATUS, admitPageOps, planCompaction, buildCandidate, checkUpdate, pageShape, createRangeCache, unionFileEntries } from "./page-compaction.js";
25
34
 
26
- const VERSION = 4;
35
+ // The lane's own message version, carried by every message it posts: a tab of
36
+ // another version drops its frames even if a scope were ever shared. The shared
37
+ // channel keeps its wire version, which presence rides beside.
38
+ const VERSION = 5;
27
39
  export const REQUEST_MS = 2000;
28
40
  export const CLOSE_WAIT_MS = 500;
29
41
  export const LOG_ENTRIES = 1024;
@@ -58,11 +70,22 @@ const encoder = new TextEncoder();
58
70
  // host floor. The message protocol inside the scope is unchanged.
59
71
  export const FORMAT_SCOPE = "page-format:1";
60
72
 
73
+ // The room scope: this lane's sequencer is a host-mode room, so a tab of the lane
74
+ // before it, which cannot read that room, is never on the same channel or lock.
75
+ export const ROOM_SCOPE = "page-rooms:2";
76
+
61
77
  export function localLaneKey(floor = 0, path = window.location.pathname) {
62
78
  const base = "clay-live:" + path;
63
- return floor >= 1 ? `${base}:${FORMAT_SCOPE}` : base;
79
+ return `${floor >= 1 ? `${base}:${FORMAT_SCOPE}` : base}:${ROOM_SCOPE}`;
80
+ }
81
+
82
+ function idle(fn) {
83
+ if (typeof requestIdleCallback === "function") requestIdleCallback(fn, { timeout: 1000 });
84
+ else setTimeout(fn, 0);
64
85
  }
65
86
 
87
+ const isYValue = (value) => !!value && typeof value === "object" && typeof value.y === "object" && value.y !== null && !Array.isArray(value.y);
88
+
66
89
  export function localLaneSupported() {
67
90
  return typeof BroadcastChannel === "function" && typeof navigator !== "undefined" && typeof navigator.locks?.request === "function";
68
91
  }
@@ -79,12 +102,16 @@ function randomId() {
79
102
  * @param {string} [options.key]
80
103
  * @param {object} [options.channel] - the document's channel, when a presence
81
104
  * service in this tab already owns one: the two share it rather than open a second
105
+ * @param {boolean} [options.rooms] - the page's room scope: the channel and the
106
+ * lock of the host-mode lane, which a tab of the lane before it never joins
82
107
  */
83
- // `key` is the room's channel without a floor suffix; `floor` scopes it.
84
- export function localTransport({ seed, key = "clay-live:" + window.location.pathname, channel: shared = null, floor = 0 }) {
108
+ // `key` is the room's channel without a floor suffix; `floor` scopes it, and
109
+ // `rooms` puts it in the room scope.
110
+ export function localTransport({ seed, key = "clay-live:" + window.location.pathname, channel: shared = null, floor = 0, rooms = false }) {
85
111
  const clientId = randomId();
86
112
  let scopeFloor = floor >= 1 ? 1 : 0;
87
- let laneKey = scopeFloor ? `${key}:${FORMAT_SCOPE}` : key;
113
+ const scopeOf = (f) => `${key}${f >= 1 ? `:${FORMAT_SCOPE}` : ""}${rooms ? `:${ROOM_SCOPE}` : ""}`;
114
+ let laneKey = scopeOf(scopeFloor);
88
115
  let channel = null;
89
116
  let unsubscribe = null;
90
117
  let handlers = null;
@@ -118,7 +145,7 @@ export function localTransport({ seed, key = "clay-live:" + window.location.path
118
145
  let mirror = { epoch: null, seq: 0, sequencer: null, closedBy: null, stale: false, seen: new Map(), stores: new Map() };
119
146
 
120
147
  function post(message) {
121
- channel?.post(message);
148
+ channel?.post({ ...message, version: VERSION });
122
149
  }
123
150
 
124
151
  // Over either cap the oldest entries go, and a file older than them can no
@@ -166,6 +193,7 @@ export function localTransport({ seed, key = "clay-live:" + window.location.path
166
193
  const meta = {};
167
194
  for (const [name, m] of mirror.stores) {
168
195
  stores[name] = { value: cloneJson(m.value), ephemeral: m.ephemeral, seen: receiptsFor(name), last: m.last, landed: landedIn(m) };
196
+ if (name === "page" && isYValue(m.value)) stores[name].compaction = { v: COMPACTION.version, loose: COMPACTION.loose, activeBytes: COMPACTION.activeBytes };
169
197
  meta[name] = { log: m.log, last: m.last, landed: m.landed, floor: m.floor, behind: m.behind, file: m.file, inFile: m.inFile };
170
198
  }
171
199
  const state = { epoch: room.epoch, seq: room.seq, writer: clientId, stores, mirror: { seen: [...mirror.seen], stores: meta } };
@@ -272,7 +300,7 @@ export function localTransport({ seed, key = "clay-live:" + window.location.path
272
300
  mirror.closedBy = null;
273
301
  const lastN = new Map();
274
302
  for (const [nKey, n] of mirror.seen) lastN.set(nKey, { n, seq: 0 });
275
- room = { epoch: mirror.epoch, seq: mirror.seq, lastN };
303
+ room = { epoch: mirror.epoch, seq: mirror.seq, lastN, host: pageHost() };
276
304
  for (const [name, frame] of kept) {
277
305
  const m = mirror.stores.get(name);
278
306
  if (!m || m.ephemeral) continue;
@@ -351,6 +379,111 @@ export function localTransport({ seed, key = "clay-live:" + window.location.path
351
379
  });
352
380
  }
353
381
 
382
+ // A room's host mode: the sources it retired, the last full fold and the
383
+ // deletions since it (which tell a room that is out of room from one that can
384
+ // still be compacted), a build failure that stops it, the range cache the union
385
+ // merge reads, and the idle task the room has asked for.
386
+ const pageHost = () => ({ retired: new Set(), lastFull: null, deletesSince: 0, failed: null, ranges: createRangeCache(), task: null, overflow: false });
387
+
388
+ const pageStore = () => {
389
+ const m = mirror.stores.get("page");
390
+ return m && !m.ephemeral && isYValue(m.value) ? m : null;
391
+ };
392
+
393
+ const pageBytes = (value) => encoder.encode(serializeLive(value)).length;
394
+
395
+ // A host-mode refusal answers as the server's does: the status its code has and
396
+ // the code itself, which is what a tab acts on.
397
+ const refused = (error, retryAfter = 0) => ({ status: ROOM_ERROR_STATUS[error] ?? 409, body: { code: error }, retryAfter });
398
+
399
+ // The room is due a compaction: one idle task, in this tab. The plan, the build
400
+ // and the commit all run inside it without yielding, so no job outlives the
401
+ // sequencer that made it and a hand-off cannot carry one to its successor.
402
+ function due(overflow = false) {
403
+ if (!room) return;
404
+ const host = room.host;
405
+ host.overflow ||= overflow;
406
+ if (host.task) return;
407
+ const target = room;
408
+ host.task = idle(() => {
409
+ host.task = null;
410
+ const over = host.overflow;
411
+ host.overflow = false;
412
+ if (stopped || room !== target || host.failed) return;
413
+ compact(host, { overflow: over });
414
+ });
415
+ }
416
+
417
+ function compact(host, { overflow }) {
418
+ const page = pageStore();
419
+ if (!page) return;
420
+ const plan = planCompaction(page.value.y, { overflow }, COMPACTION);
421
+ if (!plan) return;
422
+ const built = buildCandidate({ kind: plan.kind, sources: plan.sources });
423
+ if (typeof built.update !== "string") {
424
+ host.failed = { reason: built.failure };
425
+ return;
426
+ }
427
+ commitPage(host, page, plan, built.update);
428
+ }
429
+
430
+ // Every source still there with the identical string, and the candidate proven:
431
+ // then the room retires the sources for one checkpoint key of its own, named for
432
+ // this epoch and position, and the commit goes out as a move like any other.
433
+ function commitPage(host, m, plan, update) {
434
+ const y = m.value.y;
435
+ const sources = new Map(plan.sources);
436
+ const physical = Object.keys(y).filter((k) => {
437
+ const part = PART_KEY.exec(k);
438
+ return sources.has(part ? part[1] : k) || (part && plan.kind === "full");
439
+ });
440
+ const held = new Map(entriesOf(Object.fromEntries(physical.map((k) => [k, y[k]]))));
441
+ for (const [key, value] of sources) if (held.get(key) !== value) return;
442
+ if (!checkUpdate(update)) return;
443
+ const key = `${plan.kind === "full" ? "c" : "t"}${textHash(room.epoch).slice(0, 8)}-${(room.seq + 1).toString(36)}`;
444
+ if (Object.hasOwn(y, key)) return;
445
+ const ops = [...physical.map((source) => validateOp({ op: "del", path: ["y", source] })), validateOp({ op: "set", path: ["y", key], value: update })];
446
+ const before = pageBytes(m.value);
447
+ let trial;
448
+ try {
449
+ trial = applyAll(m.value, ops);
450
+ } catch {
451
+ return;
452
+ }
453
+ const bytes = pageBytes(trial.value);
454
+ trial.undo();
455
+ if (bytes > COMPACTION.activeBytes && bytes > before) {
456
+ // A full fold of the whole page that still does not fit: the page is as small as it
457
+ // gets, so the next write is refused as full instead of waiting for another fold.
458
+ if (plan.kind === "full" && physical.length === Object.keys(y).length) {
459
+ host.lastFull = { bytes: before, seq: room.seq };
460
+ host.deletesSince = 0;
461
+ }
462
+ return;
463
+ }
464
+ const clean = !m.behind && m.landed === m.last;
465
+ for (const [source] of plan.sources) {
466
+ host.retired.add(source);
467
+ if (host.retired.size > COMPACTION.retired) host.retired.delete(host.retired.values().next().value);
468
+ }
469
+ room.seq += 1;
470
+ if (plan.kind === "full") {
471
+ host.lastFull = { bytes, seq: room.seq };
472
+ host.deletesSince = 0;
473
+ }
474
+ const data = { epoch: room.epoch, seq: room.seq, name: "page", ops, from: "compactor", n: 0, kind: plan.kind };
475
+ advance(data);
476
+ if (clean) m.landed = room.seq;
477
+ post({ type: "data", data });
478
+ handlers.frame(cloneJson(data));
479
+ if (clean) {
480
+ tellLanded(["page"]);
481
+ post({ type: "landed", name: "page", at: { epoch: room.epoch, seq: room.seq } });
482
+ }
483
+ const shape = pageShape(m.value.y, COMPACTION);
484
+ if (shape.loose >= COMPACTION.soft || !shape.normal) due();
485
+ }
486
+
354
487
  function roomApply(from, frame) {
355
488
  if (!frame || typeof frame.name !== "string" || !Number.isInteger(frame.n) || frame.n < 1 || !Array.isArray(frame.ops) || !frame.ops.length) {
356
489
  return { status: 400, body: { error: "bad-frame" }, retryAfter: 0 };
@@ -365,16 +498,39 @@ export function localTransport({ seed, key = "clay-live:" + window.location.path
365
498
  let ops;
366
499
  try {
367
500
  ops = frame.ops.map(validateOp);
368
- applyAll(m.value, ops).undo();
369
501
  } catch (err) {
370
502
  return { status: 422, body: { error: err.message }, retryAfter: 0 };
371
503
  }
504
+ const host = room.host;
505
+ const admission = frame.name === "page" && isYValue(m.value) ? admitPageOps({ y: m.value.y, ops, state: { ...host, compaction: frame.compaction }, limits: COMPACTION }) : null;
506
+ if (admission?.error) {
507
+ if (admission.error === "checkpoint-required") due();
508
+ return refused(admission.error, admission.retryAfter);
509
+ }
510
+ let trial;
511
+ try {
512
+ trial = applyAll(m.value, ops);
513
+ } catch (err) {
514
+ return { status: 422, body: { error: err.message }, retryAfter: 0 };
515
+ }
516
+ const bytes = admission ? pageBytes(trial.value) : 0;
517
+ trial.undo();
518
+ if (admission && bytes > admission.limit) {
519
+ if (admission.failed || (host.lastFull && host.deletesSince === 0)) return refused("page-full");
520
+ due(true);
521
+ return refused("checkpoint-required");
522
+ }
372
523
  room.seq += 1;
373
524
  room.lastN.set(nKey, { n: frame.n, seq: room.seq });
374
525
  const data = { epoch: room.epoch, seq: room.seq, name: frame.name, ops, from, n: frame.n };
375
526
  advance(data);
376
527
  post({ type: "data", data });
377
528
  handlers.frame(cloneJson(data));
529
+ if (admission) {
530
+ host.deletesSince += admission.deleted;
531
+ if (!admission.failed && bytes > COMPACTION.activeBytes) due(true);
532
+ else if (!admission.failed && admission.shape.loose + admission.added >= COMPACTION.soft) due();
533
+ }
378
534
  return { status: 200, body: { seq: room.seq, epoch: room.epoch }, retryAfter: 0 };
379
535
  }
380
536
 
@@ -416,6 +572,7 @@ export function localTransport({ seed, key = "clay-live:" + window.location.path
416
572
  }
417
573
  if (value === null || typeof value !== "object" || Array.isArray(value) !== Array.isArray(m.value)) return null;
418
574
  const file = { seq: seq ?? null, hash: textHash(serializeLive(value)) };
575
+ if (name === "page" && isYValue(m.value) && isYValue(value)) return mergePageFile(m, value, file);
419
576
  // A stamp of this room places the file. Still the value its stamp names, it
420
577
  // is a capture and holds no merge past the stamp; edited, or unstamped, it
421
578
  // descends from the file before it, which holds the merges in inFile.
@@ -456,6 +613,40 @@ export function localTransport({ seed, key = "clay-live:" + window.location.path
456
613
  return { changed, moved: true };
457
614
  }
458
615
 
616
+ // A save of the page merges as a union: every key whose content the room cannot
617
+ // rebuild from its own keys goes in under a fresh name of this room's, never the
618
+ // file's, so a key the room retired cannot come back through it. The merge is a
619
+ // move like any other, and the room is due a compaction after it.
620
+ function mergePageFile(m, value, file) {
621
+ if (value.g !== m.value.g) return null;
622
+ const host = room.host;
623
+ const adds = unionFileEntries({
624
+ y: m.value.y,
625
+ fileY: value.y,
626
+ retired: host.retired,
627
+ rangesFor: host.ranges,
628
+ keyFor: (i) => `f${textHash(room.epoch).slice(0, 8)}-${(room.seq + 1).toString(36)}-${i.toString(36)}`,
629
+ });
630
+ m.file = file;
631
+ const taken = new Set();
632
+ const ops = [];
633
+ for (const [key, v] of adds) {
634
+ if (Object.hasOwn(m.value.y, key) || taken.has(key)) continue;
635
+ taken.add(key);
636
+ ops.push(validateOp({ op: "set", path: ["y", key], value: v }));
637
+ }
638
+ if (!ops.length) return { changed: false, moved: false };
639
+ m.value = applyAll(m.value, ops).value;
640
+ room.seq += 1;
641
+ mirror.seq = room.seq;
642
+ m.last = room.seq;
643
+ pushLog(m, { seq: room.seq, ops });
644
+ m.inFile = [...m.inFile.filter((seq) => seq > m.floor), room.seq];
645
+ host.lastFull = null;
646
+ due();
647
+ return { changed: true, moved: true };
648
+ }
649
+
459
650
  // A file frame (another tab's or device's save, or this browser's own
460
651
  // checkpoint coming back), per saved store: { text, at, seq }. The sequencer
461
652
  // merges it; every other tab keeps the newest, for the room it may take.
@@ -529,7 +720,7 @@ export function localTransport({ seed, key = "clay-live:" + window.location.path
529
720
  }
530
721
 
531
722
  function onMessage({ data: m }) {
532
- if (stopped || !m || m.v !== VERSION) return;
723
+ if (stopped || !m || m.version !== VERSION) return;
533
724
  if (room && ((m.type === "data" && m.data?.epoch === room.epoch) || ((m.type === "data-state" || m.type === "closed") && m.state?.epoch === room.epoch && m.state.writer !== clientId))) {
534
725
  forked();
535
726
  return;
@@ -590,7 +781,7 @@ export function localTransport({ seed, key = "clay-live:" + window.location.path
590
781
  if (!covered) return { status: "refused", code: "uncovered", ...held };
591
782
  closeRoom();
592
783
  unsubscribe?.();
593
- laneKey = want ? `${key}:${FORMAT_SCOPE}` : key;
784
+ laneKey = scopeOf(want);
594
785
  scopeFloor = want;
595
786
  channel = localChannel(laneKey);
596
787
  unsubscribe = channel.subscribe("durable", (data) => onMessage({ data }));
@@ -632,7 +823,7 @@ export function localTransport({ seed, key = "clay-live:" + window.location.path
632
823
  };
633
824
  },
634
825
  async send(_clientId, frame) {
635
- if (room) return roomApply(clientId, frame);
826
+ if (room) return roomApply(clientId, frame);
636
827
  return (await request({ type: "post", frame, epoch: mirror.epoch })) || { status: 0, body: null, retryAfter: 0 };
637
828
  },
638
829
  // The sequencer's state, installed into the mirror first. An answer to a
package/src/live/ops.js CHANGED
@@ -318,6 +318,22 @@ export function serializeLive(value) {
318
318
  return JSON.stringify(value).replace(/</g, "\\u003c");
319
319
  }
320
320
 
321
+ // True exactly when serializeLive gives both values the same text: the same keys in the
322
+ // same order and equal leaves, read without writing either value out.
323
+ export function sameLive(a, b) {
324
+ if (a === b) return true;
325
+ if (!isContainer(a) || !isContainer(b) || Array.isArray(a) !== Array.isArray(b)) return false;
326
+ if (Array.isArray(a)) {
327
+ if (a.length !== b.length) return false;
328
+ for (let i = 0; i < a.length; i++) if (!sameLive(a[i], b[i])) return false;
329
+ return true;
330
+ }
331
+ const keys = Object.keys(a), other = Object.keys(b);
332
+ if (keys.length !== other.length) return false;
333
+ for (let i = 0; i < keys.length; i++) if (keys[i] !== other[i] || !sameLive(a[keys[i]], b[keys[i]])) return false;
334
+ return true;
335
+ }
336
+
321
337
  // A live tag's lineage, `live-at="<epoch>:<seq>:<hash>"`: the room position its
322
338
  // value was written at, and a hash of the exact text, so a room can tell its own
323
339
  // unedited value from an edit of it.
@@ -0,0 +1,162 @@
1
+ import * as Y from '../vendor/yjs.vendor.js';
2
+ import { toBase64, fromBase64 } from '../vendor/yjs.vendor.js';
3
+ import { PART_KEY, isBase64, entriesOf } from '../page/ybind/parts.js';
4
+ import { rangesOf, unite, within, docRanges } from '../page/ybind/containment.js';
5
+
6
+ export const COMPACTION = Object.freeze({
7
+ version: 1,
8
+ loose: 32,
9
+ soft: 16,
10
+ activeBytes: 3840 * 1024,
11
+ deleteReserve: 64 * 1024,
12
+ newPerFrame: 4,
13
+ tailFloor: 64 * 1024,
14
+ leaseMs: 60 * 1000,
15
+ retired: 8192,
16
+ });
17
+
18
+ export const ROOM_ERROR_STATUS = Object.freeze({
19
+ 'checkpoint-required': 503, 'page-full': 409, 'compaction-failed': 409,
20
+ 'page-immutable': 409, 'page-batch': 409, 'page-update-invalid': 422, 'page-upgrade': 404,
21
+ });
22
+
23
+ export const CLIENT_KEY = /^[ur][0-9a-z]+(-[0-9a-z]+){2}$/;
24
+ export const HOST_KEY = /^[ctf][0-9a-z]{8}-[0-9a-z]+(-[0-9a-z]+)?$/;
25
+ const CHECKPOINT_KEY = /^(seed|c[0-9a-z]{8}-[0-9a-z]+)$/;
26
+
27
+ export const isCheckpointKey = (key) => CHECKPOINT_KEY.test(key);
28
+
29
+ export function pageShape(y, limits = COMPACTION) {
30
+ let checkpoints = 0, loose = 0, parts = 0, legacy = 0;
31
+ for (const key of Object.keys(y)) {
32
+ if (PART_KEY.test(key)) { parts++; continue; }
33
+ if (isCheckpointKey(key)) checkpoints++;
34
+ else loose++;
35
+ if (!CLIENT_KEY.test(key) && !HOST_KEY.test(key) && key !== 'seed') legacy++;
36
+ }
37
+ return { checkpoints, loose, parts, legacy, normal: parts === 0 && checkpoints <= 1 && loose <= limits.loose };
38
+ }
39
+
40
+ export function planCompaction(y, { overflow = false } = {}, limits = COMPACTION) {
41
+ const shape = pageShape(y, limits);
42
+ const entries = entriesOf(y);
43
+ const checkpoint = entries.filter(([key]) => isCheckpointKey(key));
44
+ const loose = entries.filter(([key]) => !isCheckpointKey(key));
45
+ const looseBytes = loose.reduce((sum, [, v]) => sum + v.length, 0);
46
+ const checkpointBytes = checkpoint.reduce((sum, [, v]) => sum + v.length, 0);
47
+ const full = !shape.normal || overflow || looseBytes > Math.max(limits.tailFloor, checkpointBytes / 4);
48
+ if (full) return { kind: 'full', sources: entries };
49
+ if (loose.length < 2 || shape.loose < limits.soft) return null;
50
+ return { kind: 'tail', sources: loose };
51
+ }
52
+
53
+ export function buildCandidate({ kind, sources, encode = Y.encodeStateAsUpdateV2 }) {
54
+ const updates = sources.map(([, v]) => fromBase64(v));
55
+ if (kind === 'tail') {
56
+ const merged = Y.mergeUpdatesV2(updates);
57
+ const index = rangesOf(merged);
58
+ for (const update of updates) if (!within(index, rangesOf(update))) return { failure: 'not-covered' };
59
+ return { update: toBase64(merged) };
60
+ }
61
+ const doc = new Y.Doc({ gc: true });
62
+ try {
63
+ for (const update of updates) Y.applyUpdateV2(doc, update);
64
+ if (doc.store.pendingStructs || doc.store.pendingDs) {
65
+ const merged = Y.mergeUpdatesV2(updates);
66
+ const index = rangesOf(merged);
67
+ for (const update of updates) if (!within(index, rangesOf(update))) return { failure: 'not-covered' };
68
+ return { update: toBase64(merged), pending: true };
69
+ }
70
+ const index = docRanges(doc);
71
+ for (const update of updates) if (!within(index, rangesOf(update))) return { failure: 'not-covered' };
72
+ const out = toBase64(encode(doc));
73
+ const check = new Y.Doc({ gc: true });
74
+ try {
75
+ Y.applyUpdateV2(check, fromBase64(out));
76
+ const stored = docRanges(check);
77
+ if (!stored.complete) return { failure: 'not-covered' };
78
+ for (const update of updates) if (!within(stored, rangesOf(update))) return { failure: 'not-covered' };
79
+ } finally { check.destroy(); }
80
+ return { update: out };
81
+ } catch (error) {
82
+ return { failure: 'invalid-update', message: error?.message || String(error) };
83
+ } finally {
84
+ doc.destroy();
85
+ }
86
+ }
87
+
88
+ export function checkUpdate(text) {
89
+ if (typeof text !== 'string' || !isBase64(text)) return null;
90
+ try {
91
+ const { items, deletes } = rangesOf(fromBase64(text));
92
+ let deleted = 0;
93
+ for (const spans of deletes.values()) for (const [from, to] of spans) deleted += to - from;
94
+ return { deleted, inserts: items.size > 0 };
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+
100
+ export function admitPageOps({ y, ops, state, limits }) {
101
+ if (state.compaction !== limits.version) return { error: 'page-upgrade' };
102
+ let added = 0;
103
+ let deleted = 0;
104
+ let inserts = false;
105
+ for (const op of ops) {
106
+ const key = op.path?.[1];
107
+ if (op.op !== 'set' || typeof key !== 'string' || !CLIENT_KEY.test(key)) return { error: 'page-immutable' };
108
+ if (Object.hasOwn(y, key)) {
109
+ if (y[key] !== op.value) return { error: 'page-immutable' };
110
+ continue;
111
+ }
112
+ if (state.retired.has(key)) return { error: 'page-immutable' };
113
+ const checked = checkUpdate(op.value);
114
+ if (!checked) return { error: 'page-update-invalid' };
115
+ added += 1;
116
+ deleted += checked.deleted;
117
+ inserts ||= checked.inserts;
118
+ }
119
+ if (added > limits.newPerFrame) return { error: 'page-batch' };
120
+ // A page whose compaction failed keeps taking edits up to its byte limit, as a room
121
+ // without compaction does, and refuses past it as full instead of waiting for a fold.
122
+ if (state.failed) return { added, deleted, inserts, limit: limits.activeBytes + (inserts ? 0 : limits.deleteReserve), shape: null, failed: true };
123
+ const shape = pageShape(y, limits);
124
+ if (!shape.normal || shape.loose + added > limits.loose) return { error: 'checkpoint-required', retryAfter: 0 };
125
+ return { added, deleted, inserts, limit: limits.activeBytes + (inserts ? 0 : limits.deleteReserve), shape };
126
+ }
127
+
128
+ export function contained(roomEntries, fileValue, rangesFor) {
129
+ const index = unite(roomEntries.map(([k, v]) => rangesFor(k, v)));
130
+ return within(index, rangesOf(fromBase64(fileValue)));
131
+ }
132
+
133
+ const UNIQUE_KEY = (key) => CLIENT_KEY.test(key) || HOST_KEY.test(key);
134
+
135
+ export function createRangeCache(max = 256) {
136
+ const cache = new Map();
137
+ return (key, value) => {
138
+ const hit = cache.get(key);
139
+ if (hit && hit.value === value) return hit.ranges;
140
+ const ranges = rangesOf(fromBase64(value));
141
+ cache.delete(key);
142
+ cache.set(key, { value, ranges });
143
+ if (cache.size > max) cache.delete(cache.keys().next().value);
144
+ return ranges;
145
+ };
146
+ }
147
+
148
+ export function unionFileEntries({ y, fileY, retired, rangesFor, keyFor = (i, key) => key }) {
149
+ const roomEntries = entriesOf(y);
150
+ const have = new Map(roomEntries);
151
+ let index = null;
152
+ const adds = [];
153
+ for (const [key, value] of entriesOf(fileY ?? {})) {
154
+ if (have.get(key) === value) continue;
155
+ if (retired.has(key) && UNIQUE_KEY(key)) continue;
156
+ let ranges;
157
+ try { ranges = rangesOf(fromBase64(value)); } catch { continue; }
158
+ index ??= unite(roomEntries.map(([k, v]) => rangesFor(k, v)));
159
+ if (!within(index, ranges)) adds.push([key, value]);
160
+ }
161
+ return adds.map(([key, value], i) => [keyFor(i, key), value]);
162
+ }
@@ -46,6 +46,7 @@ export async function fetchLiveState(clientId, since = null, { signal } = {}) {
46
46
  const url = dataUrl();
47
47
  if (clientId) url.searchParams.set("client-id", clientId);
48
48
  if (since && typeof since.epoch === "string" && Number.isInteger(since.seq)) url.searchParams.set("since", `${since.epoch}:${since.seq}`);
49
+ url.searchParams.set("compaction", "1");
49
50
  try {
50
51
  const response = await fetch(url.href, {
51
52
  credentials: "same-origin",
package/src/live/store.js CHANGED
@@ -4,7 +4,7 @@
4
4
  // Local ops change state at once. When authoritative moves, state is rebuilt in
5
5
  // place, so an object or list page code holds a reference to stays live.
6
6
 
7
- import { validateOp, checkOp, applyOp, applyOps, applyTracked, undoTracked, cloneJson, assignDeep, coalesce, overlaps, serializeLive } from "./ops.js";
7
+ import { validateOp, checkOp, applyOp, applyOps, applyTracked, undoTracked, cloneJson, assignDeep, coalesce, overlaps, serializeLive, sameLive } from "./ops.js";
8
8
 
9
9
  const MUTATORS = new Set(["push", "pop", "shift", "unshift", "splice", "sort", "reverse", "fill", "copyWithin"]);
10
10
  const INDEX = /^(0|[1-9]\d*)$/;
@@ -112,6 +112,8 @@ export function createStore({ name, value, ephemeral = false }) {
112
112
  if (!isContainer(value)) throw new TypeError(`clay.live("${name}"): the tag must hold a JSON object or list`);
113
113
 
114
114
  let authoritative = cloneJson(value);
115
+ // The last room a rebase described, for a listener that subscribes after the lane joined.
116
+ let lastRoom = null;
115
117
  const state = cloneJson(value);
116
118
  // state is authoritative with pending replayed over it. Only a confirm whose
117
119
  // ops no longer apply breaks that, until the next rebuild.
@@ -171,7 +173,8 @@ export function createStore({ name, value, ephemeral = false }) {
171
173
  console.warn(`clay.live("${name}"): ignored an incoming value that is not ${kind(state)}`);
172
174
  return false;
173
175
  }
174
- const before = serializeLive(state);
176
+ if (room) lastRoom = room;
177
+ const before = cloneJson(state);
175
178
  const prev = authoritative;
176
179
  authoritative = cloneJson(next);
177
180
  if (drop) pending = pending.filter((entry) => entry.id < drop[0] || entry.id > drop[1]);
@@ -179,7 +182,7 @@ export function createStore({ name, value, ephemeral = false }) {
179
182
  else pending = through === undefined ? pending.slice(confirmed) : pending.filter((entry) => entry.id > through);
180
183
  rebuild();
181
184
  tell({ source, frame, room, cause, prev, before: (path) => readPath(prev, path), next: authoritative });
182
- if (serializeLive(state) !== before) emit(source, null);
185
+ if (!sameLive(before, state)) emit(source, null);
183
186
  return true;
184
187
  }
185
188
 
@@ -361,6 +364,9 @@ export function createStore({ name, value, ephemeral = false }) {
361
364
  get authoritative() {
362
365
  return authoritative;
363
366
  },
367
+ get room() {
368
+ return lastRoom;
369
+ },
364
370
  get pending() {
365
371
  return pending.map((entry) => entry.op);
366
372
  },
@@ -374,6 +380,7 @@ export function createStore({ name, value, ephemeral = false }) {
374
380
  },
375
381
  setMode(next) {
376
382
  mode = next;
383
+ if (next !== "fast") lastRoom = null;
377
384
  },
378
385
  // A transport hears every local op batch, in order, as it is committed.
379
386
  onLocal(fn) {
@@ -471,6 +478,7 @@ export function createStore({ name, value, ephemeral = false }) {
471
478
  }
472
479
  return readPath(authoritative, path);
473
480
  };
481
+ if (room) lastRoom = room;
474
482
  tell({ source, frame, room, cause, get prev() { return prev(); }, before, next: authoritative });
475
483
  if (changed) emit(source, null);
476
484
  return true;
@@ -214,7 +214,7 @@ const DRAFT_DROPS_CORE = ["attrs/refetch-on-save.js", "core/conflict-notice.js"]
214
214
  // Only that host's copies auto-add the update check; another source names it.
215
215
  const TEMPLATE_SOURCE_PREFIX = "https://hyperclay.com/templates/";
216
216
 
217
- export function resolveModules(params, isEditMode, { isDraftMode = false, hasSap = false, canEdit = false, templateSource = null } = {}) {
217
+ export function resolveModules(params, isEditMode, { isDraftMode = false, hasSap = false, hasVariants = false, canEdit = false, templateSource = null } = {}) {
218
218
  const core = [...CORE_WAVES.always];
219
219
  if (isEditMode) core.push(...CORE_WAVES.editOnly);
220
220
 
@@ -228,6 +228,8 @@ export function resolveModules(params, isEditMode, { isDraftMode = false, hasSap
228
228
  if (spec.default) enabled.add(name);
229
229
  }
230
230
  if (hasSap) enabled.add("derive");
231
+ // Two or more looks need the dock's variant pill, and only someone who can edit sees it.
232
+ if (hasVariants && (isEditMode || canEdit)) enabled.add("menu");
231
233
  // ai-edit cannot run without the wire, so excluding the wire excludes it too.
232
234
  if (excluded.has("wire")) enabled.delete("ai-edit");
233
235
  if (!isEditMode && !isDraftMode && enabled.has("page")) enabled.add("page-view");