@panphora/clayjs 1.16.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 (511) 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 +72519 -59839
  11. package/dist/m/attrs/refetch-on-save-ZUE7PSCI.js +1 -0
  12. package/dist/m/attrs/save-freeze-LWGF6XUN.js +1 -0
  13. package/dist/m/c-2GRUM77Q.js +30 -0
  14. package/dist/m/c-2KIEGNC4.js +1 -0
  15. package/dist/m/{c-5R4PTRG2.js → c-2KL3BD52.js} +1 -1
  16. package/dist/m/c-2YYDMLNZ.js +1 -0
  17. package/dist/m/c-2ZZBC45O.js +1 -0
  18. package/dist/m/c-372N6H3B.js +1 -0
  19. package/dist/m/{c-4WKFKEKF.js → c-3G2DGZUP.js} +1 -1
  20. package/dist/m/c-3LDGIBMN.js +1 -0
  21. package/dist/m/c-3O7OBDCM.js +1 -0
  22. package/dist/m/c-4ASQPRQL.js +1 -0
  23. package/dist/m/c-4TXNLX4K.js +6 -0
  24. package/dist/m/c-4YU3BR4D.js +6 -0
  25. package/dist/m/c-5734MF2P.js +1 -0
  26. package/dist/m/c-57XDY6HC.js +1 -0
  27. package/dist/m/{c-VFX45IZQ.js → c-5B4YBZXC.js} +1 -1
  28. package/dist/m/c-5ONTYIUI.js +1 -0
  29. package/dist/m/c-5OOVFCJM.js +1 -0
  30. package/dist/m/c-5T6NZHNM.js +1 -0
  31. package/dist/m/{c-YDH7Q3NY.js → c-5ZQV7KC4.js} +1 -1
  32. package/dist/m/{c-Z36J63PP.js → c-6DB2OLRJ.js} +1 -1
  33. package/dist/m/c-6DNRCPNM.js +1 -0
  34. package/dist/m/c-6UGWFMQC.js +2 -0
  35. package/dist/m/c-7K4SCQB7.js +1 -0
  36. package/dist/m/c-7MG5RU63.js +1 -0
  37. package/dist/m/c-7PRJCA3E.js +1 -0
  38. package/dist/m/c-A6L3FMUS.js +1 -0
  39. package/dist/m/c-AMU3BPPR.js +4 -0
  40. package/dist/m/c-AMYEFA2E.js +1 -0
  41. package/dist/m/c-AQYCG7QW.js +1 -0
  42. package/dist/m/c-ATKAOH23.js +1 -0
  43. package/dist/m/c-B3JM5ZXC.js +1 -0
  44. package/dist/m/c-B5CS55WR.js +1 -0
  45. package/dist/m/c-B5UJQT7W.js +1 -0
  46. package/dist/m/c-B7J4K2AE.js +3 -0
  47. package/dist/m/c-BHZYSHW4.js +1 -0
  48. package/dist/m/c-BIJALSRD.js +1 -0
  49. package/dist/m/c-BJ7YQ5QZ.js +1 -0
  50. package/dist/m/c-BPF2NSPV.js +1 -0
  51. package/dist/m/{c-KIPQJX4B.js → c-BRLCTNMC.js} +1 -1
  52. package/dist/m/c-BUAYFEV5.js +1 -0
  53. package/dist/m/c-C6IPRG34.js +1 -0
  54. package/dist/m/{c-2JL6E75N.js → c-CAS3NND4.js} +1 -1
  55. package/dist/m/{c-4DRMS4QG.js → c-CCQ25GFY.js} +1 -1
  56. package/dist/m/c-CJFDLD43.js +1 -0
  57. package/dist/m/c-COPTL5WB.js +1 -0
  58. package/dist/m/c-D3E4WJ4M.js +166 -0
  59. package/dist/m/c-D5IKEKXW.js +7 -0
  60. package/dist/m/c-DHBRA6DL.js +1 -0
  61. package/dist/m/c-DKLS74XB.js +72 -0
  62. package/dist/m/c-DVYGFTSK.js +3 -0
  63. package/dist/m/c-E3ZXARWA.js +1 -0
  64. package/dist/m/c-EK63G3CG.js +1 -0
  65. package/dist/m/c-ERQSNR6K.js +13 -0
  66. package/dist/m/c-EVYAISP3.js +1 -0
  67. package/dist/m/c-EXKHPOVC.js +3 -0
  68. package/dist/m/{c-7VGZ7RU5.js → c-FWKHM4E7.js} +1 -1
  69. package/dist/m/{c-4Q2XTQ2N.js → c-GBKB5WDL.js} +1 -1
  70. package/dist/m/c-GNDL3TKB.js +1 -0
  71. package/dist/m/c-GRWMD6O5.js +1 -0
  72. package/dist/m/c-GRXHHFPO.js +1 -0
  73. package/dist/m/{c-OYTFP5QN.js → c-GY422N3Q.js} +1 -1
  74. package/dist/m/c-HAWFNBYG.js +1 -0
  75. package/dist/m/c-HG2C77BM.js +4 -0
  76. package/dist/m/c-HHGJBU4D.js +1 -0
  77. package/dist/m/c-HMV6P4RX.js +1 -0
  78. package/dist/m/c-HMYWIHFG.js +1 -0
  79. package/dist/m/c-HT2DKBV2.js +1 -0
  80. package/dist/m/c-I2SCCUEW.js +1 -0
  81. package/dist/m/{c-HRDCD3WN.js → c-ICJYHABF.js} +2 -2
  82. package/dist/m/c-IMZCWYHR.js +7 -0
  83. package/dist/m/c-IVMD3FEM.js +1 -0
  84. package/dist/m/c-IVTRVENZ.js +1 -0
  85. package/dist/m/c-IW6GGOU2.js +1 -0
  86. package/dist/m/c-JKZJIUIP.js +1 -0
  87. package/dist/m/{c-NZMNXVF6.js → c-JNLX3RJB.js} +1 -1
  88. package/dist/m/c-JRKY44DK.js +1 -0
  89. package/dist/m/c-K3ADYNLU.js +1 -0
  90. package/dist/m/c-KBS3UV6B.js +1 -0
  91. package/dist/m/c-KGHVFRR3.js +1 -0
  92. package/dist/m/c-KJJPV77R.js +1 -0
  93. package/dist/m/{c-CZUSOB7V.js → c-KNPDOMDY.js} +1 -1
  94. package/dist/m/{c-5Y6TNX5W.js → c-KP64LT2A.js} +2 -2
  95. package/dist/m/c-KQ7E3PYY.js +1 -0
  96. package/dist/m/c-L3F5ZLIV.js +1 -0
  97. package/dist/m/{c-LR4CGEPT.js → c-L42OLXWM.js} +3 -2
  98. package/dist/m/c-L5VFATT6.js +1 -0
  99. package/dist/m/c-L7JL5TL3.js +3 -0
  100. package/dist/m/c-LKSL4AUJ.js +1 -0
  101. package/dist/m/c-M6FNQCTQ.js +1 -0
  102. package/dist/m/c-MCOOHCSG.js +1 -0
  103. package/dist/m/c-MHI3GYKF.js +1 -0
  104. package/dist/m/c-MWXFLW4J.js +1 -0
  105. package/dist/m/c-NEL7HQW3.js +1 -0
  106. package/dist/m/c-NQJRVJ56.js +1 -0
  107. package/dist/m/c-NWX3MFT4.js +1 -0
  108. package/dist/m/{c-HPURYVZS.js → c-OJYM642Q.js} +1 -1
  109. package/dist/m/c-OKXI52N7.js +1 -0
  110. package/dist/m/{c-MMUIUUZG.js → c-OPLUZR3A.js} +1 -1
  111. package/dist/m/{c-J7CA54IP.js → c-OTMKN2OO.js} +1 -1
  112. package/dist/m/{c-LTGMXXKO.js → c-OYRZ7XG2.js} +1 -1
  113. package/dist/m/c-P3OFYUMA.js +1 -0
  114. package/dist/m/c-PLN76GF3.js +6 -0
  115. package/dist/m/c-PLP4JKFC.js +1 -0
  116. package/dist/m/c-Q26MME6N.js +1 -0
  117. package/dist/m/{c-S7H2DRCB.js → c-Q3XOWZEV.js} +1 -1
  118. package/dist/m/{c-SWRJFCNN.js → c-QIKT6SKM.js} +1 -1
  119. package/dist/m/c-QIXT3Q7C.js +28 -0
  120. package/dist/m/c-QO4HNGCQ.js +1 -0
  121. package/dist/m/c-R27DWQKX.js +1 -0
  122. package/dist/m/{c-HVYXIS72.js → c-R2XUODVY.js} +1 -1
  123. package/dist/m/{c-QNKREOL6.js → c-R7LBFMF5.js} +1 -1
  124. package/dist/m/c-RND3G2S2.js +1 -0
  125. package/dist/m/c-S2U6NJU4.js +1 -0
  126. package/dist/m/c-SPZPGD3R.js +1 -0
  127. package/dist/m/c-SW4YEG2D.js +8 -0
  128. package/dist/m/c-SZGYW5HT.js +1 -0
  129. package/dist/m/c-T637T7SN.js +1 -0
  130. package/dist/m/c-TJAXGIRV.js +1 -0
  131. package/dist/m/{c-KKWLDOPM.js → c-TNEM3RNM.js} +1 -1
  132. package/dist/m/c-TTB5EK4S.js +32 -0
  133. package/dist/m/c-U7SHNWDH.js +1 -0
  134. package/dist/m/c-UDHIVLYH.js +20 -0
  135. package/dist/m/{c-BBVGTQG7.js → c-UGNV5MFB.js} +1 -1
  136. package/dist/m/{c-TQZ2JYHD.js → c-UORXLSD3.js} +1 -1
  137. package/dist/m/c-UPD33ZPE.js +1 -0
  138. package/dist/m/c-URSMSFCB.js +1 -0
  139. package/dist/m/c-UY4QX42B.js +1 -0
  140. package/dist/m/c-V44KWI2L.js +4 -0
  141. package/dist/m/c-VG5XIN4T.js +1 -0
  142. package/dist/m/c-VILENZTD.js +1 -0
  143. package/dist/m/{c-33HUYEDI.js → c-VJYEVRYK.js} +1 -1
  144. package/dist/m/c-VR2BJ4AN.js +1 -0
  145. package/dist/m/c-W4JCVLAB.js +1 -0
  146. package/dist/m/c-W6DQSHXI.js +135 -0
  147. package/dist/m/c-WBGWGOM7.js +1 -0
  148. package/dist/m/{c-4L3BTFRK.js → c-WCXLG3MV.js} +1 -1
  149. package/dist/m/c-WGW764WX.js +1 -0
  150. package/dist/m/c-WMRJKQ2V.js +5 -0
  151. package/dist/m/c-WNTYKCDG.js +1 -0
  152. package/dist/m/c-WQWIRWO3.js +72 -0
  153. package/dist/m/c-X5LNHET5.js +1 -0
  154. package/dist/m/c-X5TSXZYJ.js +2 -0
  155. package/dist/m/c-XGH4GS2T.js +1 -0
  156. package/dist/m/c-Y6F2FORL.js +1 -0
  157. package/dist/m/c-YBKHUAK7.js +10 -0
  158. package/dist/m/c-YBSS55KU.js +20 -0
  159. package/dist/m/c-YCLU4UNT.js +2 -0
  160. package/dist/m/c-YVZ2L7CE.js +1 -0
  161. package/dist/m/c-ZK4GIZH2.js +1 -0
  162. package/dist/m/c-ZRXJD4G6.js +1 -0
  163. package/dist/m/c-ZWBI4HYK.js +1 -0
  164. package/dist/m/{c-HT33EQRN.js → c-ZWIOJGMT.js} +1 -1
  165. package/dist/m/core/admin-attrs-YY7T5FGU.js +1 -0
  166. package/dist/m/core/autosave-S6OMNDCK.js +1 -0
  167. package/dist/m/core/conflict-notice-IWHMHH43.js +1 -0
  168. package/dist/m/core/host-permission-6EVJGNPL.js +1 -0
  169. package/dist/m/core/is-edit-mode-GCX6767E.js +1 -0
  170. package/dist/m/core/{persist-HKHA66IY.js → persist-UKXV5KLA.js} +1 -1
  171. package/dist/m/core/recovery-DRFZRJED.js +1 -0
  172. package/dist/m/core/save-LYY7K6U7.js +1 -0
  173. package/dist/m/core/save-core-MKRPSEEO.js +1 -0
  174. package/dist/m/core/snapshot-NU3YM6GR.js +1 -0
  175. package/dist/m/core/unsaved-warning-7QTKEN7Z.js +1 -0
  176. package/dist/m/events/index-RN2JNGBV.js +1 -0
  177. package/dist/m/internals/index-TGLQINN3.js +1 -0
  178. package/dist/m/lib/cache-bust-MUMFXZK6.js +1 -0
  179. package/dist/m/loader-3BBNZZGM.js +1 -0
  180. package/dist/m/manifest-F449FF98.js +1 -0
  181. package/dist/m/manifest-edit-4396A009.js +1 -0
  182. package/dist/m/manifest.json +2509 -1474
  183. package/dist/m/options/options-EJMS3K24.js +1 -0
  184. package/dist/m/page/editor-6LRB3GE4.js +304 -0
  185. package/dist/m/plugins/ai-edit-UTV7YO2I.js +6 -0
  186. package/dist/m/plugins/{cms-HP3LSTPN.js → cms-LRAOZHT7.js} +9 -9
  187. package/dist/m/plugins/demo-OYO5GCAO.js +1 -0
  188. package/dist/m/plugins/{derive-UAAA4V4S.js → derive-FRE5JJQE.js} +1 -1
  189. package/dist/m/plugins/indicator-4VDXWYTD.js +1 -0
  190. package/dist/m/plugins/live-AD4H6XSL.js +1 -0
  191. package/dist/m/plugins/menu-M5WW7DBF.js +1 -0
  192. package/dist/m/plugins/page-MD3T2ZX3.js +2 -0
  193. package/dist/m/plugins/page-view-PAWVZNFY.js +1 -0
  194. package/dist/m/plugins/people-JCHVPF7P.js +1 -0
  195. package/dist/m/plugins/quickcrop-BQ5YYFCH.js +26 -0
  196. package/dist/m/plugins/{richclay-RC3L2F6X.js → richclay-KLGETK77.js} +2 -2
  197. package/dist/m/plugins/sortable-4S6SQC3D.js +1 -0
  198. package/dist/m/plugins/source-TJXOIOYY.js +1 -0
  199. package/dist/m/plugins/undo-NLPGCJQY.js +1 -0
  200. package/dist/m/plugins/{unsent-HJFLQJ2O.js → unsent-OA4RGBUT.js} +1 -1
  201. package/dist/m/plugins/upgrade-HSUNDNJD.js +1 -0
  202. package/dist/m/plugins/upload-NHGJXZK2.js +1 -0
  203. package/dist/m/plugins/visitor-drafts-BW43DGY3.js +20 -0
  204. package/dist/m/plugins/visitor-drafts-owner-7ODQG7JX.js +1 -0
  205. package/dist/m/plugins/wire-VBWXEXU2.js +1 -0
  206. package/dist/m/reader-core-4W2KYEWQ.js +77 -0
  207. package/dist/m/route/route-62CHXL5B.js +1 -0
  208. package/dist/m/sync/live-sync-MO5DH33W.js +1 -0
  209. package/dist/m/ui/index-IVWN2QC4.js +1 -0
  210. package/dist/m/vendor/sap-core.vendor-RN4ZVY4X.js +6 -0
  211. package/dist/offline-manifest.json +1560 -889
  212. package/dist/page/themes/light-1.css +33 -8
  213. package/dist/server/core.cjs +1554 -311
  214. package/dist/server/offline-runtime.cjs +63 -63
  215. package/dist/server/offline-runtime.mjs +63 -63
  216. package/dist/server/page-compaction-worker.cjs +3710 -0
  217. package/dist/server/rooms.mjs +5 -0
  218. package/docs/guide.md +653 -101
  219. package/entries/sap.js +5 -5
  220. package/package.json +1 -1
  221. package/src/core/admin-contenteditable.js +5 -2
  222. package/src/core/admin-inputs.js +1 -0
  223. package/src/core/admin-onclick.js +2 -1
  224. package/src/core/boot-phase.js +2 -1
  225. package/src/core/conflict-download.js +3 -29
  226. package/src/core/conflict-notice-ui.js +4 -7
  227. package/src/core/download.js +32 -0
  228. package/src/core/host-meta.js +56 -0
  229. package/src/core/is-edit-mode.js +17 -1
  230. package/src/core/mode-key.js +125 -19
  231. package/src/core/mode-support.js +5 -0
  232. package/src/core/mode-transition.js +20 -6
  233. package/src/core/preserve-authored.js +4 -4
  234. package/src/core/recovery.js +121 -33
  235. package/src/core/save-accepted.js +33 -0
  236. package/src/core/save-core.js +111 -32
  237. package/src/core/save-job.js +108 -0
  238. package/src/core/save-prepare.js +53 -0
  239. package/src/core/save-size-notice.js +3 -1
  240. package/src/core/save-slices.js +15 -0
  241. package/src/core/save.js +557 -127
  242. package/src/core/serialize-steps.js +109 -0
  243. package/src/core/snapshot.js +209 -40
  244. package/src/core/source-map.js +83 -38
  245. package/src/core/stale-host-notice.js +1 -0
  246. package/src/core/upgrade-run.js +59 -8
  247. package/src/dom/active-element.js +5 -0
  248. package/src/lib/assets.js +1 -0
  249. package/src/lib/autosave-scheduler.js +3 -4
  250. package/src/lib/slice.js +65 -0
  251. package/src/lib/uimotion.js +774 -0
  252. package/src/lib/unsaved-state.js +2 -2
  253. package/src/live/fast-lane.js +10 -3
  254. package/src/live/local-lane.js +202 -11
  255. package/src/live/ops.js +16 -0
  256. package/src/live/page-compaction.js +162 -0
  257. package/src/live/server-lane.js +1 -0
  258. package/src/live/store.js +11 -3
  259. package/src/loader-logic.js +3 -1
  260. package/src/loader.js +14 -4
  261. package/src/page/adopt-boot.js +2 -1
  262. package/src/page/awareness.js +33 -2
  263. package/src/page/blocks.js +122 -27
  264. package/src/page/board-views.js +2 -16
  265. package/src/page/boot-pack.js +1 -1
  266. package/src/page/chart-tables.js +29 -0
  267. package/src/page/chart.js +411 -72
  268. package/src/page/checklist.js +10 -135
  269. package/src/page/checkpoint-proof.js +295 -0
  270. package/src/page/chrome.js +257 -40
  271. package/src/page/clipboard-shape.js +126 -0
  272. package/src/page/document-title-dom.js +12 -0
  273. package/src/page/document-title-runtime.js +15 -1
  274. package/src/page/document-title.js +4 -1
  275. package/src/page/drag.js +33 -3
  276. package/src/page/editor.js +207 -31
  277. package/src/page/emoji-data.js +162 -0
  278. package/src/page/export-assets.js +357 -0
  279. package/src/page/export-html.js +209 -0
  280. package/src/page/export-markdown.js +361 -0
  281. package/src/page/features/board.js +12 -3
  282. package/src/page/features/chart.js +11 -0
  283. package/src/page/features/checklist.js +428 -0
  284. package/src/page/features/dates.js +180 -0
  285. package/src/page/features/emoji.js +187 -0
  286. package/src/page/features/export-html.js +189 -0
  287. package/src/page/features/export-markdown.js +169 -0
  288. package/src/page/features/import-markdown.js +349 -0
  289. package/src/page/features/media.js +61 -5
  290. package/src/page/features/mentions.js +188 -0
  291. package/src/page/features/registry.js +24 -1
  292. package/src/page/features/table.js +375 -85
  293. package/src/page/features/title-cover.js +331 -0
  294. package/src/page/features/title-icon.js +213 -0
  295. package/src/page/features/toc.js +87 -0
  296. package/src/page/format-command.js +33 -0
  297. package/src/page/format-gate.js +68 -18
  298. package/src/page/format-host.js +71 -0
  299. package/src/page/format-native-commands.js +158 -0
  300. package/src/page/format-native.js +42 -0
  301. package/src/page/image-actions.js +64 -0
  302. package/src/page/import-markdown.js +262 -0
  303. package/src/page/inline.js +32 -8
  304. package/src/page/items.js +141 -11
  305. package/src/page/markdown.js +30 -2
  306. package/src/page/media.js +35 -119
  307. package/src/page/mention-picker.js +99 -0
  308. package/src/page/merge.js +1 -1
  309. package/src/page/placeholder.js +14 -1
  310. package/src/page/projection.js +156 -25
  311. package/src/page/recover.js +10 -12
  312. package/src/page/recovery-ui.js +464 -0
  313. package/src/page/rich/chart.js +63 -7
  314. package/src/page/save-projection.js +67 -0
  315. package/src/page/save-states.js +14 -4
  316. package/src/page/schema.js +52 -3
  317. package/src/page/table-paste.js +3 -1
  318. package/src/page/table-style.js +4 -4
  319. package/src/page/table.js +44 -2
  320. package/src/page/themes/content.css +25 -1
  321. package/src/page/themes/light-1.css +33 -8
  322. package/src/page/themes/light-1.source.css +8 -7
  323. package/src/page/toc-link.js +31 -0
  324. package/src/page/toc.js +5 -0
  325. package/src/page/view-hooks.js +58 -0
  326. package/src/page/view.js +13 -0
  327. package/src/page/ybind/binding.js +116 -6
  328. package/src/page/ybind/bridge.js +187 -20
  329. package/src/page/ybind/containment.js +162 -0
  330. package/src/page/ybind/doc.js +17 -9
  331. package/src/page/ybind/file-debt.js +63 -0
  332. package/src/page/ybind/layout.js +7 -2
  333. package/src/page/ybind/marks.js +2 -1
  334. package/src/page/ybind/moves.js +5 -4
  335. package/src/page/ybind/origins.js +106 -4
  336. package/src/page/ybind/parts.js +8 -1
  337. package/src/page/ybind/provider.js +125 -18
  338. package/src/page/ybind/recovery-proof.js +32 -9
  339. package/src/page/ybind/recovery-replay.js +905 -4
  340. package/src/page/ybind/recovery.js +174 -45
  341. package/src/page/ybind/table.js +67 -11
  342. package/src/page/ybind/walk-delta.js +258 -0
  343. package/src/page/ybind/walk.js +54 -29
  344. package/src/page/ybind/writer.js +1 -1
  345. package/src/plugins/ai-edit.js +13 -9
  346. package/src/plugins/indicator-chip.js +91 -0
  347. package/src/plugins/indicator.js +13 -80
  348. package/src/plugins/live.js +126 -15
  349. package/src/plugins/menu/actions.js +196 -0
  350. package/src/plugins/menu/people.js +279 -0
  351. package/src/plugins/menu/placement.js +121 -0
  352. package/src/plugins/menu/preferences.js +9 -0
  353. package/src/plugins/menu/seo.js +316 -397
  354. package/src/plugins/menu/style.js +171 -0
  355. package/src/plugins/menu/variants.js +296 -0
  356. package/src/plugins/menu.js +186 -183
  357. package/src/plugins/page-view.js +3 -2
  358. package/src/plugins/page.js +362 -43
  359. package/src/plugins/sortable.js +8 -1
  360. package/src/plugins/source.js +6 -0
  361. package/src/plugins/upgrade.js +51 -17
  362. package/src/plugins/upload-ui.js +12 -17
  363. package/src/plugins/visitor-drafts/bottom-ui.js +12 -4
  364. package/src/plugins/visitor-drafts/intro-style.js +7 -2
  365. package/src/plugins/visitor-drafts/intro.js +7 -4
  366. package/src/plugins/visitor-drafts/owner-chip.js +8 -12
  367. package/src/plugins/visitor-drafts/prompt.js +11 -9
  368. package/src/plugins/visitor-drafts/source.js +4 -3
  369. package/src/plugins/visitor-drafts/store.js +138 -9
  370. package/src/plugins/visitor-drafts/ui-memory.js +2 -2
  371. package/src/plugins/visitor-drafts-owner.js +4 -6
  372. package/src/plugins/visitor-drafts.js +5 -3
  373. package/src/recovery/store.js +1 -1
  374. package/src/route/route.js +4 -0
  375. package/src/server/checkpoint/index.js +2 -2
  376. package/src/server/rooms/index.js +5 -1
  377. package/src/server/rooms/live-data-rooms.js +302 -50
  378. package/src/server/rooms/page-compaction-worker.js +12 -0
  379. package/src/server/rooms/page-compactor.js +92 -0
  380. package/src/server/rooms/page-room-conformance.js +461 -0
  381. package/src/source/parse-steps.js +94 -0
  382. package/src/source/session.js +233 -57
  383. package/src/sync/editing-sender.js +12 -0
  384. package/src/sync/live-sync-edit.js +22 -2
  385. package/src/sync/live-sync.js +150 -23
  386. package/src/sync/page-relay.js +52 -0
  387. package/src/sync/presence-store.js +44 -4
  388. package/src/sync/presence.js +6 -3
  389. package/src/sync/saved-stream.js +28 -14
  390. package/src/ui/bevel-controls.js +1 -0
  391. package/src/ui/bevel-dialog.js +5 -2
  392. package/src/ui/editor-ui.js +77 -0
  393. package/src/ui/page-colour.js +281 -0
  394. package/src/ui/toast.js +16 -7
  395. package/src/vendor/hyper-html-api-upgrade.vendor.js +2 -2
  396. package/src/vendor/hyper-undo.vendor.js +1 -1
  397. package/src/vendor/hypercms.vendor.js +9 -9
  398. package/src/vendor/parse5.vendor.js +2 -2
  399. package/src/vendor/sap-core.vendor.js +5 -5
  400. package/dist/m/attrs/refetch-on-save-NWPPXOCZ.js +0 -1
  401. package/dist/m/attrs/save-freeze-FUP4Q4X3.js +0 -1
  402. package/dist/m/c-2M73252O.js +0 -1
  403. package/dist/m/c-2SPVQSHC.js +0 -1
  404. package/dist/m/c-35NBGMZB.js +0 -1
  405. package/dist/m/c-3IQRECYS.js +0 -1
  406. package/dist/m/c-4B6H4G5V.js +0 -1
  407. package/dist/m/c-4FFHZM32.js +0 -1
  408. package/dist/m/c-4YSCRICO.js +0 -1
  409. package/dist/m/c-54RCDCBU.js +0 -1
  410. package/dist/m/c-5M4YEKCX.js +0 -4
  411. package/dist/m/c-5V3MOVJT.js +0 -1
  412. package/dist/m/c-66O32MV3.js +0 -28
  413. package/dist/m/c-6HMB24EV.js +0 -1
  414. package/dist/m/c-7O6AFR65.js +0 -1
  415. package/dist/m/c-ANQTJ3B5.js +0 -2
  416. package/dist/m/c-AYVOREBE.js +0 -1
  417. package/dist/m/c-B2ZXPOS3.js +0 -3
  418. package/dist/m/c-BMIZKMPV.js +0 -1
  419. package/dist/m/c-BTDI4Y23.js +0 -1
  420. package/dist/m/c-CJZSLMVQ.js +0 -1
  421. package/dist/m/c-DBE6ORSI.js +0 -1
  422. package/dist/m/c-EDHUR4MX.js +0 -1
  423. package/dist/m/c-EY6UOM54.js +0 -1
  424. package/dist/m/c-F3CKJTJS.js +0 -1
  425. package/dist/m/c-F7Y6FLPY.js +0 -1
  426. package/dist/m/c-FA4DZVTC.js +0 -1
  427. package/dist/m/c-G2BUZITR.js +0 -1
  428. package/dist/m/c-GB3JTV6E.js +0 -1
  429. package/dist/m/c-GBGWXTX4.js +0 -1
  430. package/dist/m/c-GHBCCVG4.js +0 -1
  431. package/dist/m/c-GJAEGGXY.js +0 -1
  432. package/dist/m/c-GOWBDM6J.js +0 -1
  433. package/dist/m/c-IB52R237.js +0 -1
  434. package/dist/m/c-IKWNE4R4.js +0 -1
  435. package/dist/m/c-IU3IYEU6.js +0 -1
  436. package/dist/m/c-IZ2EBRAJ.js +0 -1
  437. package/dist/m/c-KR4UW4KW.js +0 -1
  438. package/dist/m/c-KY3VJGUA.js +0 -8
  439. package/dist/m/c-LFPHCXY3.js +0 -1
  440. package/dist/m/c-LRPUO2MX.js +0 -1
  441. package/dist/m/c-LXENEBV7.js +0 -1
  442. package/dist/m/c-NMJRXKPC.js +0 -8
  443. package/dist/m/c-OIBUTDXI.js +0 -1
  444. package/dist/m/c-PPOSCAQD.js +0 -1
  445. package/dist/m/c-QIX5BJYN.js +0 -1
  446. package/dist/m/c-QQHEX6HA.js +0 -1
  447. package/dist/m/c-R7RMANJI.js +0 -9
  448. package/dist/m/c-RI4NT75Y.js +0 -1
  449. package/dist/m/c-RM7J7BV2.js +0 -1
  450. package/dist/m/c-RUJKSUQW.js +0 -1
  451. package/dist/m/c-SGCCC4FW.js +0 -25
  452. package/dist/m/c-T4VO4OCB.js +0 -1
  453. package/dist/m/c-TQEMVJCS.js +0 -1
  454. package/dist/m/c-TR6RMDQ7.js +0 -3
  455. package/dist/m/c-UKHTLWUW.js +0 -1
  456. package/dist/m/c-UNN7LWCT.js +0 -1
  457. package/dist/m/c-UXBMRDZB.js +0 -235
  458. package/dist/m/c-V7PZZRG7.js +0 -1
  459. package/dist/m/c-VL52ADDR.js +0 -1
  460. package/dist/m/c-WNYBLIOZ.js +0 -1
  461. package/dist/m/c-WWDBYXLK.js +0 -1
  462. package/dist/m/c-XNNW2CWK.js +0 -1
  463. package/dist/m/c-Y6USGTS5.js +0 -5
  464. package/dist/m/c-YG2NJYS6.js +0 -135
  465. package/dist/m/c-Z4VRC7GU.js +0 -7
  466. package/dist/m/c-Z6K52OLP.js +0 -19
  467. package/dist/m/c-ZDY2MDAA.js +0 -21
  468. package/dist/m/c-ZUCOKOHT.js +0 -1
  469. package/dist/m/core/admin-attrs-VGNLAUDK.js +0 -1
  470. package/dist/m/core/autosave-E4PEFPZ6.js +0 -1
  471. package/dist/m/core/conflict-notice-VQ6Y64PH.js +0 -1
  472. package/dist/m/core/host-permission-XN3K2IW3.js +0 -1
  473. package/dist/m/core/is-edit-mode-JXHDVGUL.js +0 -1
  474. package/dist/m/core/recovery-TBBSICE2.js +0 -1
  475. package/dist/m/core/save-WUCHTQBE.js +0 -1
  476. package/dist/m/core/save-core-FJVHT5M3.js +0 -1
  477. package/dist/m/core/snapshot-F3SWDZMM.js +0 -1
  478. package/dist/m/core/unsaved-warning-STKBHE6G.js +0 -1
  479. package/dist/m/events/index-23DPTUT4.js +0 -1
  480. package/dist/m/internals/index-WOVAP3RU.js +0 -1
  481. package/dist/m/lib/cache-bust-CGWT6ZJI.js +0 -1
  482. package/dist/m/loader-HXX6YNH5.js +0 -1
  483. package/dist/m/manifest-F22DEDAC.js +0 -1
  484. package/dist/m/manifest-edit-C3C7376B.js +0 -1
  485. package/dist/m/options/options-2LH76JQ3.js +0 -1
  486. package/dist/m/page/editor-XUTB2HB2.js +0 -189
  487. package/dist/m/plugins/ai-edit-ZQ4CGWIP.js +0 -6
  488. package/dist/m/plugins/demo-ZIBFAA3R.js +0 -1
  489. package/dist/m/plugins/indicator-PV4YW2ID.js +0 -1
  490. package/dist/m/plugins/live-NUTTXSBS.js +0 -1
  491. package/dist/m/plugins/menu-HCKO3DSP.js +0 -138
  492. package/dist/m/plugins/page-KSUH7G6J.js +0 -2
  493. package/dist/m/plugins/page-view-W73OW7WI.js +0 -1
  494. package/dist/m/plugins/people-AJJ32YCG.js +0 -1
  495. package/dist/m/plugins/quickcrop-QNY3PKLD.js +0 -26
  496. package/dist/m/plugins/sortable-YRMQX3ZU.js +0 -1
  497. package/dist/m/plugins/source-KHK5UUSK.js +0 -1
  498. package/dist/m/plugins/undo-OF2L3CXU.js +0 -1
  499. package/dist/m/plugins/upgrade-ZNYODP5Y.js +0 -1
  500. package/dist/m/plugins/upload-XSGNILUY.js +0 -1
  501. package/dist/m/plugins/visitor-drafts-XK5GW55Q.js +0 -21
  502. package/dist/m/plugins/visitor-drafts-owner-KSSH75EV.js +0 -1
  503. package/dist/m/plugins/wire-ZWVWKF76.js +0 -1
  504. package/dist/m/reader-core-ILWLXVMP.js +0 -68
  505. package/dist/m/route/route-KOCZSEGN.js +0 -1
  506. package/dist/m/sync/live-sync-4AUVXVJU.js +0 -1
  507. package/dist/m/ui/index-EVHKMKBL.js +0 -1
  508. package/dist/m/vendor/sap-core.vendor-A7BMUYZT.js +0 -6
  509. /package/dist/m/{c-FILLTJA7.js → c-QTTJYJMA.js} +0 -0
  510. /package/dist/m/plugins/{quickcrop-QNY3PKLD.js.LEGAL.txt → quickcrop-BQ5YYFCH.js.LEGAL.txt} +0 -0
  511. /package/dist/m/plugins/{richclay-RC3L2F6X.js.LEGAL.txt → richclay-KLGETK77.js.LEGAL.txt} +0 -0
@@ -4,6 +4,7 @@ import { state as pmState } from "../../vendor/prosemirror.vendor.js";
4
4
  import { walk } from "./walk.js";
5
5
  import { docOf } from "./doc.js";
6
6
  import { createProjectionMap } from "./projection-map.js";
7
+ import { beforeFence } from "./writer.js";
7
8
  import { unitIds } from "./origins.js";
8
9
  import { attrKey, copyActiveKey, parseGoneIntents, physicalKeyOf, recoveredRoot, removeKey, restoreKey } from "./recovery.js";
9
10
 
@@ -113,12 +114,12 @@ const liveRemovals = (pd, current, selected) => {
113
114
  return out;
114
115
  };
115
116
 
116
- export function moveBlocks({ pd, view, um, origins, render }, args = {}) {
117
+ export function moveBlocks({ pd, view, um, origins, render, walked = null }, args = {}) {
117
118
  const ids = Array.isArray(args?.ids) ? [...new Set(args.ids)] : [];
118
119
  const afterId = args?.afterId === undefined ? null : args.afterId;
119
120
  const hasParent = args?.parent !== undefined;
120
121
 
121
- const current = walk(pd, origins);
122
+ const current = walked ?? walk(pd, origins);
122
123
  // An opaque block is an operation this runtime must refuse before writing: it
123
124
  // has no turn-into, it cannot be carried, and its record is never rewritten.
124
125
  const protectedIds = new Set(current.visible.filter((seg) => seg.opaque).map((seg) => seg.id));
@@ -264,7 +265,7 @@ export function moveBlocks({ pd, view, um, origins, render }, args = {}) {
264
265
  let text = null, textAttrs = null;
265
266
  const flush = () => {
266
267
  if (text === null) return;
267
- pd.flow.insert(cursor, text, textAttrs ?? undefined);
268
+ beforeFence(pd, cursor, after.fences, () => pd.flow.insert(cursor, text, textAttrs ?? undefined));
268
269
  cursor += text.length;
269
270
  text = null;
270
271
  };
@@ -275,7 +276,7 @@ export function moveBlocks({ pd, view, um, origins, render }, args = {}) {
275
276
  text += unit.ch;
276
277
  } else {
277
278
  flush();
278
- pd.flow.insertEmbed(cursor, unit.embed, unit.attrs);
279
+ beforeFence(pd, cursor, after.fences, () => pd.flow.insertEmbed(cursor, unit.embed, unit.attrs));
279
280
  cursor += 1;
280
281
  }
281
282
  }
@@ -34,6 +34,15 @@ const boundaryRecord = (value) => {
34
34
 
35
35
  const sameBoundary = (a, b) => a.blockId === b.blockId && a.kind === b.kind && a.recoveryOf === b.recoveryOf;
36
36
 
37
+ // The keys walkFlow reads boundary and alias positions from: any key that parses to
38
+ // ["boundary" | "alias", client, clock], whatever its value.
39
+ const markerKey = (key) => {
40
+ let parts;
41
+ try { parts = JSON.parse(key); } catch { return null; }
42
+ if (!Array.isArray(parts) || parts.length !== 3 || (parts[0] !== "boundary" && parts[0] !== "alias")) return null;
43
+ return isClock(parts[1]) && isClock(parts[2]) ? parts : null;
44
+ };
45
+
37
46
  const recordKey = (key) => {
38
47
  if (typeof key !== "string" || key.charCodeAt(0) !== 91) return null;
39
48
  let parts;
@@ -46,6 +55,7 @@ const recordKey = (key) => {
46
55
  export function createOriginIndex(origins) {
47
56
  const spans = new Map();
48
57
  const aliases = new Map();
58
+ const aliasClocks = new Map();
49
59
  const boundaries = new Map();
50
60
 
51
61
  const lower = (list, clock) => {
@@ -53,13 +63,56 @@ export function createOriginIndex(origins) {
53
63
  while (lo < hi) { const mid = (lo + hi) >> 1; if (list[mid].clock < clock) lo = mid + 1; else hi = mid; }
54
64
  return lo;
55
65
  };
66
+ const lowerClock = (list, clock) => {
67
+ let lo = 0, hi = list.length;
68
+ while (lo < hi) { const mid = (lo + hi) >> 1; if (list[mid] < clock) lo = mid + 1; else hi = mid; }
69
+ return lo;
70
+ };
71
+ const markAlias = (client, clock, present) => {
72
+ const list = aliasClocks.get(client) ?? [];
73
+ const at = lowerClock(list, clock);
74
+ const there = list[at] === clock;
75
+ if (present && !there) list.splice(at, 0, clock);
76
+ else if (!present && there) list.splice(at, 1);
77
+ if (list.length) aliasClocks.set(client, list);
78
+ else aliasClocks.delete(client);
79
+ };
80
+ // Boundary and alias keys in the map's key order (Yjs keeps a key where it was first
81
+ // written), and each client's clocks that have one, sorted, for walkFlow.
82
+ const markerRecords = [];
83
+ const markerEntries = new Map();
84
+ const markerCounts = new Map();
85
+ const markerClocks = new Map();
86
+ const trackMarker = (key, live) => {
87
+ let entry = markerEntries.get(key);
88
+ if (entry === undefined) {
89
+ const parts = markerKey(key);
90
+ entry = parts ? { kind: parts[0], client: parts[1], clock: parts[2], live: false } : null;
91
+ markerEntries.set(key, entry);
92
+ if (entry) markerRecords.push(entry);
93
+ }
94
+ if (!entry || entry.live === live) return;
95
+ entry.live = live;
96
+ const counts = markerCounts.get(entry.client) ?? markerCounts.set(entry.client, new Map()).get(entry.client);
97
+ const list = markerClocks.get(entry.client) ?? markerClocks.set(entry.client, []).get(entry.client);
98
+ const n = (counts.get(entry.clock) ?? 0) + (live ? 1 : -1);
99
+ if (n) counts.set(entry.clock, n);
100
+ else counts.delete(entry.clock);
101
+ const at = lowerClock(list, entry.clock);
102
+ if (live && n === 1) list.splice(at, 0, entry.clock);
103
+ else if (!live && n === 0) list.splice(at, 1);
104
+ if (!list.length) { markerClocks.delete(entry.client); markerCounts.delete(entry.client); }
105
+ };
56
106
  const apply = (key, value) => {
107
+ trackMarker(key, value !== undefined);
57
108
  const parts = recordKey(key);
58
109
  if (!parts) return;
59
110
  const [kind, client, clock] = parts;
60
111
  if (kind === "alias") {
61
- if (value && isClock(value.sourceClient) && isClock(value.sourceClock)) aliases.set(idKey(client, clock), { client: value.sourceClient, clock: value.sourceClock });
112
+ const valid = Boolean(value && isClock(value.sourceClient) && isClock(value.sourceClock));
113
+ if (valid) aliases.set(idKey(client, clock), { client: value.sourceClient, clock: value.sourceClock });
62
114
  else aliases.delete(idKey(client, clock));
115
+ markAlias(client, clock, valid);
63
116
  return;
64
117
  }
65
118
  if (kind === "boundary") {
@@ -77,12 +130,17 @@ export function createOriginIndex(origins) {
77
130
  if (list.length) spans.set(client, list);
78
131
  else spans.delete(client);
79
132
  };
80
- for (const [key, value] of origins) apply(key, value);
133
+ for (const key of origins._map.keys()) apply(key, origins.get(key));
81
134
  const observer = (event) => { for (const key of event.keysChanged) apply(key, origins.get(key)); };
82
135
  origins.observe(observer);
83
136
 
137
+ const hasAlias = (client, clock) => {
138
+ const list = aliasClocks.get(client);
139
+ return list !== undefined && list[lowerClock(list, clock)] === clock;
140
+ };
141
+
84
142
  const resolve = (id) => {
85
- const alias = aliases.get(idKey(id.client, id.clock));
143
+ const alias = hasAlias(id.client, id.clock) ? aliases.get(idKey(id.client, id.clock)) : undefined;
86
144
  if (alias) return { client: alias.client, clock: alias.clock };
87
145
  const list = spans.get(id.client);
88
146
  if (!list) return { client: id.client, clock: id.clock };
@@ -95,8 +153,45 @@ export function createOriginIndex(origins) {
95
153
  return { client: record.sourceClient, clock: record.sourceClock + offset };
96
154
  };
97
155
 
156
+ const resolveRange = (client, clock, length) => {
157
+ const out = [];
158
+ const push = (sourceClient, sourceClock, size) => {
159
+ const last = out[out.length - 1];
160
+ if (last && last.client === sourceClient && last.clock + last.length === sourceClock) last.length += size;
161
+ else out.push({ client: sourceClient, clock: sourceClock, length: size });
162
+ };
163
+ const list = spans.get(client) ?? [];
164
+ const marks = aliasClocks.get(client) ?? [];
165
+ const end = clock + length;
166
+ let s = lower(list, clock + 1) - 1, a = lowerClock(marks, clock), at = clock;
167
+ while (at < end) {
168
+ if (a < marks.length && marks[a] === at) {
169
+ const target = aliases.get(idKey(client, at));
170
+ push(target.client, target.clock, 1);
171
+ at += 1;
172
+ a += 1;
173
+ } else {
174
+ let stop = end;
175
+ if (a < marks.length && marks[a] < stop) stop = marks[a];
176
+ if (s + 1 < list.length && list[s + 1].clock < stop) stop = list[s + 1].clock;
177
+ const span = s >= 0 ? list[s] : null;
178
+ if (span && at < span.clock + span.length) {
179
+ stop = Math.min(stop, span.clock + span.length);
180
+ push(span.sourceClient, span.sourceClock + at - span.clock, stop - at);
181
+ } else push(client, at, stop - at);
182
+ at = stop;
183
+ }
184
+ while (s + 1 < list.length && list[s + 1].clock <= at) s += 1;
185
+ }
186
+ return out;
187
+ };
188
+
98
189
  const boundaryOf = (id) => {
99
190
  if (!id || !isClock(id.client) || !isClock(id.clock)) return null;
191
+ if (!hasAlias(id.client, id.clock)) {
192
+ const record = boundaries.get(idKey(id.client, id.clock));
193
+ return record ? { ...record } : null;
194
+ }
100
195
  let key = idKey(id.client, id.clock);
101
196
  const seen = new Set();
102
197
  while (!seen.has(key)) {
@@ -164,8 +259,15 @@ export function createOriginIndex(origins) {
164
259
  if (!previous || !sameBoundary(previous, record)) throw conflicting(key);
165
260
  };
166
261
 
262
+ // null while a transaction that wrote origin records has not settled: the observer has
263
+ // not seen those keys yet, and walkFlow then reads the map's keys itself.
264
+ const markers = () => {
265
+ for (const t of origins.doc?._transactionCleanups ?? []) if (t.changed.has(origins)) return null;
266
+ return { records: markerRecords, clocks: markerClocks };
267
+ };
268
+
167
269
  return {
168
- origins, resolve, recordSpans, recordAlias, recordBoundary, boundaryOf,
270
+ origins, resolve, resolveRange, recordSpans, recordAlias, recordBoundary, boundaryOf, markers,
169
271
  get size() { return origins.size; },
170
272
  dispose() { origins.unobserve(observer); },
171
273
  };
@@ -1,5 +1,12 @@
1
1
  export const PART_KEY = /^(.+)\.(\d+)\.(\d+)$/;
2
- export const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}(?:==)?|[A-Za-z0-9+/]{3}=?)?$/;
2
+ // A regexp group per quad keeps backtrack state per quad and overflows on a page of a few MB.
3
+ const NOT_BASE64 = /[^A-Za-z0-9+/]/;
4
+ export function isBase64(text) {
5
+ const pad = text.endsWith("==") ? 2 : text.endsWith("=") ? 1 : 0;
6
+ const body = text.length - pad;
7
+ if (NOT_BASE64.test(pad ? text.slice(0, body) : text)) return false;
8
+ return pad ? body % 4 + pad === 4 : body % 4 !== 1;
9
+ }
3
10
  const PART_CHARS = /^[A-Za-z0-9+/=\t\n\f\r ]*$/;
4
11
 
5
12
  export function entriesOf(y) {
@@ -2,8 +2,9 @@ import * as Y from "../../vendor/yjs.vendor.js";
2
2
  import { toBase64, fromBase64 } from "../../vendor/yjs.vendor.js";
3
3
  import { NET } from "./layout.js";
4
4
  import { ORIGINS_ORIGIN } from "./origins.js";
5
- import { PART_KEY, BASE64, entriesOf, captureYValue } from "./parts.js";
5
+ import { PART_KEY, isBase64, entriesOf, captureYValue } from "./parts.js";
6
6
  import { containsUpdate } from "./recovery-proof.js";
7
+ import { rangesOf, unite, within, docRanges } from "./containment.js";
7
8
 
8
9
  export { entriesOf } from "./parts.js";
9
10
 
@@ -11,6 +12,10 @@ export const enc = (u) => toBase64(u);
11
12
  export const dec = (s) => fromBase64(s);
12
13
 
13
14
  export const PART = 12 * 1024;
15
+
16
+ // The origin a replayed journal is applied through. It is offered to the room like any
17
+ // other local work and is never journaled again: a recovery is not this tab's typing.
18
+ export const RECOVERY_ORIGIN = "page-recovery";
14
19
  const baseOf = (k) => PART_KEY.exec(k)?.[1] ?? k;
15
20
 
16
21
  export function yValueOf(ydoc) {
@@ -59,7 +64,7 @@ export function docFromValue(value, { strict = false } = {}) {
59
64
  try {
60
65
  for (const [key, value] of entries) {
61
66
  const text = strict ? value.replace(/[\t\n\f\r ]/g, "") : value;
62
- if (strict && !BASE64.test(text)) throw new YDecodeError("invalid-update", `Invalid base64 in Yjs update: ${key}`);
67
+ if (strict && !isBase64(text)) throw new YDecodeError("invalid-update", `Invalid base64 in Yjs update: ${key}`);
63
68
  Y.applyUpdateV2(d, dec(text), NET);
64
69
  }
65
70
  if (strict && (d.store.pendingStructs || d.store.pendingDs)) {
@@ -80,7 +85,7 @@ const utf8Bytes = (text) => (encoder ? encoder.encode(text).length : text.length
80
85
  const sameDeletes = (a, b) => Y.equalDeleteSets(Y.createDeleteSetFromStructStore(a.store), Y.createDeleteSetFromStructStore(b.store));
81
86
  const svEqual = (a, b) => { if (a.size !== b.size) return false; for (const [c, n] of a) if (b.get(c) !== n) return false; return true; };
82
87
 
83
- export function createYProvider({ store, ydoc, compact: limits = {}, setTimer = setTimeout, clearTimer = clearTimeout, onStopped = () => {}, stats = { applied: 0, offered: 0, compactions: 0 } }) {
88
+ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer = setTimeout, clearTimer = clearTimeout, onStopped = () => {}, stats = { applied: 0, offered: 0, compactions: 0 }, journal = null, onLocalUpdate = () => {} }) {
84
89
  const initial = entriesOf(store.value.y);
85
90
  const known = new Set();
86
91
  // Set when this tab's page can no longer reach the room: "lineage" when the room holds
@@ -95,8 +100,14 @@ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer =
95
100
  let canCompact = !initial.some(([k]) => k[0] === "m");
96
101
  let counter = 0;
97
102
  const me = ydoc.clientID.toString(36);
98
- const nextKey = (p) => `${p}${me}-${(counter++).toString(36)}`;
103
+ const inst = Math.floor(Math.random() * 36 ** 6).toString(36);
104
+ let mode = store.room?.compaction?.v === 1 ? "host" : "legacy";
105
+ const nextKey = (p) => (mode === "host" ? `${p}${me}-${inst}-${(counter++).toString(36)}` : `${p}${me}-${(counter++).toString(36)}`);
99
106
  let timer = null;
107
+ let outbox = [];
108
+ // A refusal took back writes of this tab's that later updates build on: until a reconcile
109
+ // puts them back, nothing newer goes out.
110
+ let refusedGap = false;
100
111
  const generation = store.value.g;
101
112
  // Keys this tab wrote itself: the updates and offers the room has not confirmed yet.
102
113
  const own = new Set();
@@ -113,12 +124,50 @@ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer =
113
124
  const parts = partsOf(key, enc(bytes));
114
125
  store.handle.update((data) => { for (const [k, v] of parts) data.y[k] = v; });
115
126
  };
127
+ const putWhole = (key, bytes) => {
128
+ known.add(key);
129
+ own.add(key);
130
+ const text = enc(bytes);
131
+ store.handle.update((data) => { data.y[key] = text; });
132
+ };
133
+ const isOwnYOp = (op) => {
134
+ const path = op.path;
135
+ if (!Array.isArray(path) || path.length !== 2 || path[0] !== "y") return true;
136
+ return own.has(path[1]);
137
+ };
138
+ const mineInFlight = () => store.pending.some((op) => isOwnYOp(op));
139
+ // Host mode orders this tab's writes only while the store is in the room: at save
140
+ // speed nothing confirms a write until the save lands, so updates go out at once.
141
+ const hosted = () => mode === "host" && store.handle.status?.mode !== "file";
142
+ const flushOutbox = () => {
143
+ if (!outbox.length) return;
144
+ const update = outbox.length === 1 ? outbox[0] : Y.mergeUpdatesV2(outbox);
145
+ outbox = [];
146
+ putWhole(nextKey("u"), update);
147
+ };
148
+ const offer = () => {
149
+ if (!hosted() || stopped || mineInFlight()) return;
150
+ if (refusedGap) {
151
+ refusedGap = false;
152
+ if (reconcile()) return;
153
+ }
154
+ if (!outbox.length) return;
155
+ const update = outbox.length === 1 ? outbox[0] : Y.mergeUpdatesV2(outbox);
156
+ outbox = [];
157
+ putWhole(nextKey("u"), update);
158
+ };
116
159
 
117
160
  for (const [k] of initial) known.add(k);
118
161
 
162
+ // Ahead of the stop guards, and of the room's own delivery: work this tab can no longer
163
+ // offer, and work a refused offer took back, is still work a crash must not lose.
119
164
  const onUpdate = (u, origin) => {
165
+ if (origin !== NET && origin !== ORIGINS_ORIGIN) onLocalUpdate(u);
166
+ if (origin !== NET && origin !== ORIGINS_ORIGIN && origin !== RECOVERY_ORIGIN) journal?.local(u);
120
167
  if (stopped || origin === NET || origin === ORIGINS_ORIGIN) return;
121
- put(nextKey("u"), u);
168
+ if (hosted()) { outbox.push(u); offer(); }
169
+ else if (mode === "host") { outbox.push(u); flushOutbox(); }
170
+ else put(nextKey("u"), u);
122
171
  };
123
172
  ydoc.on("updateV2", onUpdate);
124
173
 
@@ -127,7 +176,11 @@ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer =
127
176
  canCompact = true;
128
177
  known.add(k);
129
178
  stats.applied++;
130
- Y.applyUpdateV2(ydoc, dec(v), NET);
179
+ const update = dec(v);
180
+ Y.applyUpdateV2(ydoc, update, NET);
181
+ // A received update can be the prerequisite of a later local insert, so the journal
182
+ // keeps it beside the work it owns, and never counts it as this tab's own.
183
+ journal?.dependency(update);
131
184
  };
132
185
 
133
186
  let wantReconcile = false;
@@ -140,15 +193,24 @@ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer =
140
193
  timer = null;
141
194
  const full = wantReconcile;
142
195
  if (full) reconcile();
143
- maybeCompact();
196
+ if (mode === "legacy") maybeCompact();
144
197
  }, 0);
145
198
  };
146
199
 
147
- const settle = () => { reconcile(); maybeCompact(); };
200
+ const settle = () => { reconcile(); if (mode === "legacy") maybeCompact(); };
201
+
202
+ // The room's entries already hold every item and deletion of this tab's document, read
203
+ // from each entry's cached ranges: nothing to offer, and no copy of the room is built.
204
+ const roomHolds = (y) => {
205
+ const mine = docRanges(ydoc);
206
+ return mine.complete && within(unite(entriesOf(y ?? {}).map(([k, v]) => rangesFor(k, v))), mine);
207
+ };
148
208
 
149
209
  function reconcile() {
150
210
  if (stopped) return false;
211
+ if (hosted() && mineInFlight()) return false;
151
212
  const y = store.value.y;
213
+ if (roomHolds(y)) { wantReconcile = false; reconcilePending = false; return false; }
152
214
  const scratch = new Y.Doc();
153
215
  for (const [, v] of entriesOf(y)) Y.applyUpdateV2(scratch, dec(v), NET);
154
216
  const sv = Y.encodeStateVector(scratch);
@@ -158,7 +220,8 @@ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer =
158
220
  const diff = Y.encodeStateAsUpdateV2(ydoc, sv);
159
221
  scratch.destroy();
160
222
  stats.offered++;
161
- put(nextKey("r"), diff);
223
+ if (mode === "host") { outbox = []; putWhole(nextKey("r"), diff); }
224
+ else put(nextKey("r"), diff);
162
225
  wantReconcile = false;
163
226
  reconcilePending = false;
164
227
  return true;
@@ -171,24 +234,38 @@ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer =
171
234
  let quietUntil = 0;
172
235
  const COVER_MAX = 256;
173
236
  const coverCache = new Map();
237
+ const META_MAX = 256;
238
+ const metas = new Map();
174
239
  const drop = (keys) => store.handle.update((data) => { for (const k of keys) delete data.y[k]; });
175
240
  const keysOf = (auth, bases) => Object.keys(auth).filter((k) => bases.has(baseOf(k)));
176
241
  // A key is retired only while what this tab shows under it is still what the proof was
177
242
  // made over: a late write under the same key keeps its own entry.
178
243
  const retire = (auth, keys) => { const visible = store.value.y ?? {}; drop(keys.filter((k) => k in visible && visible[k] === auth[k])); };
179
244
 
180
- // `known` and the coverage memo keep live references only: the authoritative entries,
181
- // this tab's own unconfirmed writes and the fold in progress. Pruned after a rebase, a
182
- // fold and a refusal. A key that is forgotten can be decoded again; Yjs is idempotent.
245
+ // The items and deletions one entry holds, read once per value.
246
+ const rangesFor = (key, value) => {
247
+ const meta = metas.get(key);
248
+ if (meta && meta.value === value) return meta.ranges;
249
+ if (metas.size >= META_MAX) metas.delete(metas.keys().next().value);
250
+ const ranges = rangesOf(dec(value));
251
+ metas.set(key, { value, ranges });
252
+ return ranges;
253
+ };
254
+
255
+ // `known` and the entry memo keep live references only: the authoritative entries, by
256
+ // their whole keys, this tab's own unconfirmed writes and the fold in progress. Pruned
257
+ // after a rebase, a fold and a refusal. A key that is forgotten can be decoded again;
258
+ // Yjs is idempotent.
183
259
  function prune() {
184
260
  const auth = store.authoritative.y ?? {};
185
261
  const visible = store.value.y ?? {};
186
262
  for (const k of own) if (!(k in visible)) own.delete(k);
187
- const held = fold ? [...fold.parts.map(([k]) => k), ...fold.inputs.keys()] : [];
188
- const retained = new Set([...Object.keys(auth), ...own, ...held]);
189
- for (const key of known) if (!retained.has(key)) known.delete(key);
190
263
  const values = new Map(entriesOf(auth));
264
+ const held = fold ? [fold.key, ...fold.parts.map(([k]) => k), ...fold.inputs.keys()] : [];
265
+ const retained = new Set([...Object.keys(auth), ...values.keys(), ...own, ...held]);
266
+ for (const key of known) if (!retained.has(key)) known.delete(key);
191
267
  for (const [key, proof] of coverCache) if (!retained.has(key) || values.get(key) !== proof.value) coverCache.delete(key);
268
+ for (const [key, meta] of metas) if (!retained.has(key) || (values.has(key) && values.get(key) !== meta.value)) metas.delete(key);
192
269
  }
193
270
 
194
271
  // Whether a fold's merged bytes already carry this input: merging it in again changes
@@ -229,7 +306,15 @@ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer =
229
306
  prune();
230
307
  return true;
231
308
  }
232
- if (store.pending.length || !stillHeld(auth, active) || !coversInputs(active)) return false;
309
+ if (store.pending.length) return false;
310
+ if (!stillHeld(auth, active) || !coversInputs(active)) {
311
+ // A fold that can never speak for its inputs is re-planned, never held.
312
+ fold = null;
313
+ if (arrived.length) retire(auth, arrived.map(([k]) => k));
314
+ quietUntil = entriesOf(auth).length + compact.keys;
315
+ prune();
316
+ return true;
317
+ }
233
318
  fold = null;
234
319
  retire(auth, keysOf(auth, active.inputs));
235
320
  prune();
@@ -295,7 +380,7 @@ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer =
295
380
  // with this tab's document is a page rebuilt from another article while the tab was away.
296
381
  const clientsOf = (y) => {
297
382
  const clients = new Set();
298
- for (const [, v] of entriesOf(y ?? {})) for (const c of Y.decodeStateVector(Y.encodeStateVectorFromUpdateV2(dec(v))).keys()) clients.add(c);
383
+ for (const [k, v] of entriesOf(y ?? {})) for (const [c, spans] of rangesFor(k, v).items) if (spans[0][0] === 0) clients.add(c);
299
384
  return clients;
300
385
  };
301
386
  // A page whose article has no blocks seeds with no client at all; it still shares its
@@ -310,6 +395,18 @@ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer =
310
395
 
311
396
  const off = store.onRebase((ev) => {
312
397
  if (stopped) return;
398
+ if (ev.room) {
399
+ const was = mode;
400
+ mode = ev.room.compaction?.v === 1 ? "host" : "legacy";
401
+ // A room that stopped compacting confirms nothing in host order: what host mode
402
+ // was holding back goes out as ordinary updates.
403
+ if (was === "host" && mode === "legacy" && outbox.length) setTimer(() => {
404
+ if (stopped || mode !== "legacy" || !outbox.length) return;
405
+ const update = outbox.length === 1 ? outbox[0] : Y.mergeUpdatesV2(outbox);
406
+ outbox = [];
407
+ put(nextKey("u"), update);
408
+ }, 0);
409
+ }
313
410
  const ops = ev.source === "remote" && ev.frame ? ev.frame.ops : null;
314
411
  const plain = ops && ops.every((op) => op.path[0] === "y" && op.path.length === 2 && !PART_KEY.test(op.path[1]));
315
412
  if (!plain && ev.cause !== "refused" && !sharesLineage(ev.next.y)) {
@@ -323,6 +420,7 @@ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer =
323
420
  }
324
421
  prune();
325
422
  schedule(ev.source !== "remote" || !ops, ev.source !== "remote");
423
+ offer();
326
424
  });
327
425
 
328
426
  // A page update the room refused as too large is never offered again: the difference would
@@ -331,6 +429,12 @@ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer =
331
429
  // hold whole, and the room's page stays one a checkpoint can render.
332
430
  const offDelivery = store.onDelivery((event) => {
333
431
  if (event.phase !== "refused") return;
432
+ if (mode === "host") {
433
+ if (event.code === "page-full" || event.code === "compaction-failed") { stop(event.code); return; }
434
+ refusedGap = true;
435
+ schedule(true, false);
436
+ return;
437
+ }
334
438
  if (event.code !== "too-large" || stopped) { prune(); return; }
335
439
  stop("too-large");
336
440
  const auth = store.authoritative.y ?? {};
@@ -341,7 +445,10 @@ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer =
341
445
  prune();
342
446
  });
343
447
 
448
+ const offConfirm = store.onConfirm(() => { offer(); });
449
+
344
450
  return { known, stats, settle, reconcile, get reconcilePending() { return reconcilePending; }, get stopped() { return stopped; },
451
+ get pending() { return outbox.length > 0; },
345
452
  // Test-only snapshot of the counters and cache sizes; no mutable set leaves the provider.
346
- _diagnostics: () => ({ known: known.size, cover: coverCache.size, fold: fold ? fold.parts.length : 0, generation }), compact: maybeCompact, dispose() { off(); offDelivery(); ydoc.off("updateV2", onUpdate); if (timer) clearTimer(timer); } };
453
+ _diagnostics: () => ({ known: known.size, cover: coverCache.size, fold: fold ? fold.parts.length : 0, generation }), compact: maybeCompact, dispose() { off(); offDelivery(); offConfirm(); ydoc.off("updateV2", onUpdate); if (timer) clearTimer(timer); } };
347
454
  }
@@ -14,8 +14,9 @@
14
14
  * state and the receipt arrive in the proof itself.
15
15
  */
16
16
 
17
- import { encodeStateAsUpdateV2, mergeUpdatesV2 } from "../../vendor/yjs.vendor.js";
17
+ import { encodeStateAsUpdateV2 } from "../../vendor/yjs.vendor.js";
18
18
  import { docFromValue } from "./provider.js";
19
+ import { indexOf, indexContains } from "./containment.js";
19
20
 
20
21
  export class PageProofError extends Error {
21
22
  constructor(code, message) {
@@ -40,17 +41,19 @@ const copyState = (state) => ({ v: state.v, g: state.g, y: { ...(state.y ?? {})
40
41
  export const equalBytes = (a, b) => a.length === b.length && a.every((value, index) => value === b[index]);
41
42
 
42
43
  /**
43
- * Whether merging `update` into `checkpoint` adds nothing: no insertion, no deletion.
44
+ * Whether applying `update` on top of `checkpoint` adds nothing: no insertion, no
45
+ * deletion. Compared by structure (the items and deletions the checkpoint holds), since
46
+ * merged bytes are not stable across merge orders. An update whose dependencies the
47
+ * checkpoint lacks is not contained: Yjs would hold it pending rather than refuse it. The
48
+ * checkpoint is integrated once for as long as its bytes are held, and each update after
49
+ * it is read, not applied.
44
50
  *
45
51
  * @param {Uint8Array} checkpoint - canonical or merged Yjs V2 update bytes
46
52
  * @param {Uint8Array} update - the bytes to look for
47
- * @param {Object} [Yjs] - the vendored Yjs module
48
53
  * @returns {boolean}
49
54
  */
50
- export function containsUpdate(checkpoint, update, Yjs = null) {
51
- const merge = Yjs?.mergeUpdatesV2 ?? mergeUpdatesV2;
52
- const canonical = merge([checkpoint]);
53
- return equalBytes(canonical, merge([canonical, update]));
55
+ export function containsUpdate(checkpoint, update) {
56
+ return indexContains(indexOf(checkpoint), update);
54
57
  }
55
58
 
56
59
  export async function digestHex(bytes) {
@@ -58,27 +61,47 @@ export async function digestHex(bytes) {
58
61
  return [...new Uint8Array(digest)].map((value) => value.toString(16).padStart(2, "0")).join("");
59
62
  }
60
63
 
64
+ // The last few states decoded, by their entries: a proof, its validation, the file debt
65
+ // and the journal's rebase all ask for the same state's bytes after one accepted save.
66
+ const CANONICAL_MAX = 4;
67
+ const canonical = [];
68
+ const sameState = (memo, state) => {
69
+ if (memo.v !== state.v || memo.g !== state.g) return false;
70
+ const keys = Object.keys(state.y ?? {});
71
+ if (keys.length !== memo.y.size) return false;
72
+ for (const key of keys) if (memo.y.get(key) !== state.y[key]) return false;
73
+ return true;
74
+ };
75
+
61
76
  /**
62
77
  * The accepted page state as one canonical update, decoded strictly.
63
78
  *
64
79
  * Pending structs, missing part and a delete set naming an unknown item all refuse:
65
- * a checkpoint nobody can decode proves nothing about a journaled record.
80
+ * a checkpoint nobody can decode proves nothing about a journaled record. The same state
81
+ * answers with the same bytes, which no caller may change.
66
82
  *
67
83
  * @param {Object} state - a version 1 page value
68
84
  * @returns {Uint8Array}
69
85
  */
70
86
  export function canonicalPageBytes(state) {
87
+ const hit = state && typeof state === "object" && state.y && typeof state.y === "object" ? canonical.find((memo) => sameState(memo, state)) : null;
88
+ if (hit) return hit.bytes;
71
89
  let doc;
72
90
  try {
73
91
  doc = docFromValue(state, { strict: true });
74
92
  } catch (error) {
75
93
  fail("invalid-state", `The accepted page state does not decode: ${error.message}`);
76
94
  }
95
+ let bytes;
77
96
  try {
78
- return encodeStateAsUpdateV2(doc);
97
+ bytes = encodeStateAsUpdateV2(doc);
98
+ indexOf(bytes, doc);
79
99
  } finally {
80
100
  doc.destroy();
81
101
  }
102
+ if (canonical.length >= CANONICAL_MAX) canonical.shift();
103
+ canonical.push({ v: state.v, g: state.g, y: new Map(Object.entries(state.y)), bytes });
104
+ return bytes;
82
105
  }
83
106
 
84
107
  // The same answer `sameAuthority` gives in recovery/identity.js, written here because the