@panphora/clayjs 1.17.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (467) hide show
  1. package/dist/all.js +1 -1
  2. package/dist/clay-dom.js +1 -1
  3. package/dist/clay-events.js +1 -1
  4. package/dist/clay-internals.js +1 -1
  5. package/dist/clay-options.js +1 -1
  6. package/dist/clay-router.js +1 -1
  7. package/dist/clay-ui.js +1 -1
  8. package/dist/clay-utils.js +1 -1
  9. package/dist/clay.js +1 -1
  10. package/dist/clay.standalone.js +72474 -62090
  11. package/dist/m/attrs/{save-freeze-MYNVXREN.js → save-freeze-LWGF6XUN.js} +1 -1
  12. package/dist/m/c-2GRUM77Q.js +30 -0
  13. package/dist/m/c-2KIEGNC4.js +1 -0
  14. package/dist/m/{c-AMSNPOCX.js → c-2KL3BD52.js} +1 -1
  15. package/dist/m/c-2ZZBC45O.js +1 -0
  16. package/dist/m/c-372N6H3B.js +1 -0
  17. package/dist/m/{c-4WKFKEKF.js → c-3G2DGZUP.js} +1 -1
  18. package/dist/m/c-3LDGIBMN.js +1 -0
  19. package/dist/m/c-3O7OBDCM.js +1 -0
  20. package/dist/m/c-4ASQPRQL.js +1 -0
  21. package/dist/m/c-4TXNLX4K.js +6 -0
  22. package/dist/m/c-4YU3BR4D.js +6 -0
  23. package/dist/m/c-5734MF2P.js +1 -0
  24. package/dist/m/c-57XDY6HC.js +1 -0
  25. package/dist/m/{c-KAU6E43D.js → c-5B4YBZXC.js} +1 -1
  26. package/dist/m/c-5ONTYIUI.js +1 -0
  27. package/dist/m/c-5OOVFCJM.js +1 -0
  28. package/dist/m/{c-3XIRD4NU.js → c-5T6NZHNM.js} +1 -1
  29. package/dist/m/{c-74MGYWZF.js → c-5ZQV7KC4.js} +1 -1
  30. package/dist/m/{c-7OLLJPWP.js → c-6DB2OLRJ.js} +1 -1
  31. package/dist/m/c-6DNRCPNM.js +1 -0
  32. package/dist/m/c-6UGWFMQC.js +2 -0
  33. package/dist/m/c-7K4SCQB7.js +1 -0
  34. package/dist/m/c-7MG5RU63.js +1 -0
  35. package/dist/m/c-7PRJCA3E.js +1 -0
  36. package/dist/m/c-A6L3FMUS.js +1 -0
  37. package/dist/m/c-AMU3BPPR.js +4 -0
  38. package/dist/m/{c-Y5HBBIQ4.js → c-AMYEFA2E.js} +1 -1
  39. package/dist/m/c-AQYCG7QW.js +1 -0
  40. package/dist/m/c-ATKAOH23.js +1 -0
  41. package/dist/m/c-B5CS55WR.js +1 -0
  42. package/dist/m/c-B5UJQT7W.js +1 -0
  43. package/dist/m/c-B7J4K2AE.js +3 -0
  44. package/dist/m/c-BHZYSHW4.js +1 -0
  45. package/dist/m/{c-VHM5XQHK.js → c-BIJALSRD.js} +1 -1
  46. package/dist/m/c-BJ7YQ5QZ.js +1 -0
  47. package/dist/m/{c-KIPQJX4B.js → c-BRLCTNMC.js} +1 -1
  48. package/dist/m/{c-VE6ZMYMC.js → c-BUAYFEV5.js} +1 -1
  49. package/dist/m/c-C6IPRG34.js +1 -0
  50. package/dist/m/{c-KU3ZFADW.js → c-CAS3NND4.js} +1 -1
  51. package/dist/m/{c-PS5ARVWR.js → c-CCQ25GFY.js} +1 -1
  52. package/dist/m/c-CJFDLD43.js +1 -0
  53. package/dist/m/c-COPTL5WB.js +1 -0
  54. package/dist/m/c-D3E4WJ4M.js +166 -0
  55. package/dist/m/c-D5IKEKXW.js +7 -0
  56. package/dist/m/c-DHBRA6DL.js +1 -0
  57. package/dist/m/c-DKLS74XB.js +72 -0
  58. package/dist/m/c-DVYGFTSK.js +3 -0
  59. package/dist/m/c-E3ZXARWA.js +1 -0
  60. package/dist/m/c-EK63G3CG.js +1 -0
  61. package/dist/m/c-ERQSNR6K.js +13 -0
  62. package/dist/m/c-EVYAISP3.js +1 -0
  63. package/dist/m/c-EXKHPOVC.js +3 -0
  64. package/dist/m/{c-QJ4OM3G6.js → c-FWKHM4E7.js} +1 -1
  65. package/dist/m/{c-4Q2XTQ2N.js → c-GBKB5WDL.js} +1 -1
  66. package/dist/m/c-GNDL3TKB.js +1 -0
  67. package/dist/m/c-GRWMD6O5.js +1 -0
  68. package/dist/m/c-GRXHHFPO.js +1 -0
  69. package/dist/m/c-HG2C77BM.js +4 -0
  70. package/dist/m/c-HHGJBU4D.js +1 -0
  71. package/dist/m/c-HMV6P4RX.js +1 -0
  72. package/dist/m/c-HMYWIHFG.js +1 -0
  73. package/dist/m/c-HT2DKBV2.js +1 -0
  74. package/dist/m/c-I2SCCUEW.js +1 -0
  75. package/dist/m/{c-TJ6WOJL3.js → c-ICJYHABF.js} +2 -2
  76. package/dist/m/c-IMZCWYHR.js +7 -0
  77. package/dist/m/c-IVMD3FEM.js +1 -0
  78. package/dist/m/{c-WDC5GVKU.js → c-IVTRVENZ.js} +1 -1
  79. package/dist/m/c-JKZJIUIP.js +1 -0
  80. package/dist/m/{c-UDUUJPFZ.js → c-JNLX3RJB.js} +1 -1
  81. package/dist/m/c-K3ADYNLU.js +1 -0
  82. package/dist/m/c-KGHVFRR3.js +1 -0
  83. package/dist/m/{c-5QWQ7A5L.js → c-KJJPV77R.js} +1 -1
  84. package/dist/m/{c-CZUSOB7V.js → c-KNPDOMDY.js} +1 -1
  85. package/dist/m/{c-EZU56NQV.js → c-KP64LT2A.js} +2 -2
  86. package/dist/m/c-KQ7E3PYY.js +1 -0
  87. package/dist/m/c-L3F5ZLIV.js +1 -0
  88. package/dist/m/{c-3RUU4TPX.js → c-L42OLXWM.js} +3 -2
  89. package/dist/m/c-L5VFATT6.js +1 -0
  90. package/dist/m/c-L7JL5TL3.js +3 -0
  91. package/dist/m/c-LKSL4AUJ.js +1 -0
  92. package/dist/m/c-M6FNQCTQ.js +1 -0
  93. package/dist/m/c-MCOOHCSG.js +1 -0
  94. package/dist/m/c-MHI3GYKF.js +1 -0
  95. package/dist/m/c-MWXFLW4J.js +1 -0
  96. package/dist/m/{c-5RJFTLSF.js → c-NEL7HQW3.js} +1 -1
  97. package/dist/m/{c-DCXOXBF3.js → c-NQJRVJ56.js} +1 -1
  98. package/dist/m/c-NWX3MFT4.js +1 -0
  99. package/dist/m/{c-HPURYVZS.js → c-OJYM642Q.js} +1 -1
  100. package/dist/m/c-OKXI52N7.js +1 -0
  101. package/dist/m/{c-R3L7ZJGB.js → c-OPLUZR3A.js} +1 -1
  102. package/dist/m/{c-GSFIGKOP.js → c-OTMKN2OO.js} +1 -1
  103. package/dist/m/{c-6NC5I2TV.js → c-OYRZ7XG2.js} +1 -1
  104. package/dist/m/{c-EVCQ3BKA.js → c-P3OFYUMA.js} +1 -1
  105. package/dist/m/c-PLN76GF3.js +6 -0
  106. package/dist/m/c-PLP4JKFC.js +1 -0
  107. package/dist/m/c-Q26MME6N.js +1 -0
  108. package/dist/m/{c-S7H2DRCB.js → c-Q3XOWZEV.js} +1 -1
  109. package/dist/m/{c-FRLHSY7E.js → c-QIKT6SKM.js} +1 -1
  110. package/dist/m/c-QIXT3Q7C.js +28 -0
  111. package/dist/m/c-QO4HNGCQ.js +1 -0
  112. package/dist/m/{c-JU3NK25H.js → c-R27DWQKX.js} +1 -1
  113. package/dist/m/{c-RSMOODDU.js → c-R2XUODVY.js} +1 -1
  114. package/dist/m/{c-QNKREOL6.js → c-R7LBFMF5.js} +1 -1
  115. package/dist/m/c-RND3G2S2.js +1 -0
  116. package/dist/m/{c-Q622NS2Y.js → c-S2U6NJU4.js} +1 -1
  117. package/dist/m/c-SPZPGD3R.js +1 -0
  118. package/dist/m/c-SW4YEG2D.js +8 -0
  119. package/dist/m/c-T637T7SN.js +1 -0
  120. package/dist/m/c-TJAXGIRV.js +1 -0
  121. package/dist/m/{c-RYUXUUBX.js → c-TNEM3RNM.js} +1 -1
  122. package/dist/m/c-TTB5EK4S.js +32 -0
  123. package/dist/m/c-U7SHNWDH.js +1 -0
  124. package/dist/m/c-UDHIVLYH.js +20 -0
  125. package/dist/m/{c-BBVGTQG7.js → c-UGNV5MFB.js} +1 -1
  126. package/dist/m/{c-QICP2YR5.js → c-UORXLSD3.js} +1 -1
  127. package/dist/m/c-UPD33ZPE.js +1 -0
  128. package/dist/m/c-URSMSFCB.js +1 -0
  129. package/dist/m/c-UY4QX42B.js +1 -0
  130. package/dist/m/c-V44KWI2L.js +4 -0
  131. package/dist/m/c-VILENZTD.js +1 -0
  132. package/dist/m/{c-HDDEEL77.js → c-VJYEVRYK.js} +1 -1
  133. package/dist/m/{c-SMBZG5X4.js → c-VR2BJ4AN.js} +1 -1
  134. package/dist/m/c-W4JCVLAB.js +1 -0
  135. package/dist/m/c-W6DQSHXI.js +135 -0
  136. package/dist/m/c-WBGWGOM7.js +1 -0
  137. package/dist/m/{c-4L3BTFRK.js → c-WCXLG3MV.js} +1 -1
  138. package/dist/m/c-WGW764WX.js +1 -0
  139. package/dist/m/c-WMRJKQ2V.js +5 -0
  140. package/dist/m/c-WNTYKCDG.js +1 -0
  141. package/dist/m/c-WQWIRWO3.js +72 -0
  142. package/dist/m/c-X5LNHET5.js +1 -0
  143. package/dist/m/{c-I2ZATQCD.js → c-X5TSXZYJ.js} +2 -2
  144. package/dist/m/c-XGH4GS2T.js +1 -0
  145. package/dist/m/c-Y6F2FORL.js +1 -0
  146. package/dist/m/c-YBKHUAK7.js +10 -0
  147. package/dist/m/c-YBSS55KU.js +20 -0
  148. package/dist/m/c-YCLU4UNT.js +2 -0
  149. package/dist/m/c-YVZ2L7CE.js +1 -0
  150. package/dist/m/c-ZK4GIZH2.js +1 -0
  151. package/dist/m/c-ZRXJD4G6.js +1 -0
  152. package/dist/m/c-ZWBI4HYK.js +1 -0
  153. package/dist/m/core/{admin-attrs-QHOKPJ7W.js → admin-attrs-YY7T5FGU.js} +1 -1
  154. package/dist/m/core/autosave-S6OMNDCK.js +1 -0
  155. package/dist/m/core/conflict-notice-IWHMHH43.js +1 -0
  156. package/dist/m/core/{host-permission-FNXVEFNE.js → host-permission-6EVJGNPL.js} +1 -1
  157. package/dist/m/core/is-edit-mode-GCX6767E.js +1 -0
  158. package/dist/m/core/{persist-JFYRHMRJ.js → persist-UKXV5KLA.js} +1 -1
  159. package/dist/m/core/recovery-DRFZRJED.js +1 -0
  160. package/dist/m/core/save-LYY7K6U7.js +1 -0
  161. package/dist/m/core/save-core-MKRPSEEO.js +1 -0
  162. package/dist/m/core/snapshot-NU3YM6GR.js +1 -0
  163. package/dist/m/core/unsaved-warning-7QTKEN7Z.js +1 -0
  164. package/dist/m/internals/{index-PVXGPUDA.js → index-TGLQINN3.js} +1 -1
  165. package/dist/m/loader-3BBNZZGM.js +1 -0
  166. package/dist/m/manifest-F449FF98.js +1 -0
  167. package/dist/m/manifest-edit-4396A009.js +1 -0
  168. package/dist/m/manifest.json +2111 -1573
  169. package/dist/m/page/{editor-NNJA4OKD.js → editor-6LRB3GE4.js} +123 -21
  170. package/dist/m/plugins/ai-edit-UTV7YO2I.js +6 -0
  171. package/dist/m/plugins/{cms-HP3LSTPN.js → cms-LRAOZHT7.js} +9 -9
  172. package/dist/m/plugins/{demo-RM5IR6AO.js → demo-OYO5GCAO.js} +1 -1
  173. package/dist/m/plugins/{derive-UAAA4V4S.js → derive-FRE5JJQE.js} +1 -1
  174. package/dist/m/plugins/indicator-4VDXWYTD.js +1 -0
  175. package/dist/m/plugins/live-AD4H6XSL.js +1 -0
  176. package/dist/m/plugins/menu-M5WW7DBF.js +1 -0
  177. package/dist/m/plugins/page-MD3T2ZX3.js +2 -0
  178. package/dist/m/plugins/page-view-PAWVZNFY.js +1 -0
  179. package/dist/m/plugins/{people-SGPJKANK.js → people-JCHVPF7P.js} +1 -1
  180. package/dist/m/plugins/quickcrop-BQ5YYFCH.js +26 -0
  181. package/dist/m/plugins/{richclay-NRQDSWX6.js → richclay-KLGETK77.js} +2 -2
  182. package/dist/m/plugins/sortable-4S6SQC3D.js +1 -0
  183. package/dist/m/plugins/source-TJXOIOYY.js +1 -0
  184. package/dist/m/plugins/undo-NLPGCJQY.js +1 -0
  185. package/dist/m/plugins/{unsent-HJFLQJ2O.js → unsent-OA4RGBUT.js} +1 -1
  186. package/dist/m/plugins/upgrade-HSUNDNJD.js +1 -0
  187. package/dist/m/plugins/{upload-WLYKK6TL.js → upload-NHGJXZK2.js} +1 -1
  188. package/dist/m/plugins/visitor-drafts-BW43DGY3.js +20 -0
  189. package/dist/m/plugins/visitor-drafts-owner-7ODQG7JX.js +1 -0
  190. package/dist/m/plugins/{wire-4N6GYAF6.js → wire-VBWXEXU2.js} +1 -1
  191. package/dist/m/reader-core-4W2KYEWQ.js +77 -0
  192. package/dist/m/route/route-62CHXL5B.js +1 -0
  193. package/dist/m/sync/live-sync-MO5DH33W.js +1 -0
  194. package/dist/m/ui/index-IVWN2QC4.js +1 -0
  195. package/dist/m/vendor/sap-core.vendor-RN4ZVY4X.js +6 -0
  196. package/dist/offline-manifest.json +1367 -930
  197. package/dist/page/themes/light-1.css +8 -7
  198. package/dist/server/core.cjs +1440 -288
  199. package/dist/server/offline-runtime.cjs +63 -63
  200. package/dist/server/offline-runtime.mjs +63 -63
  201. package/dist/server/page-compaction-worker.cjs +3710 -0
  202. package/dist/server/rooms.mjs +5 -0
  203. package/docs/guide.md +653 -101
  204. package/entries/sap.js +5 -5
  205. package/package.json +1 -1
  206. package/src/core/admin-contenteditable.js +5 -2
  207. package/src/core/admin-inputs.js +1 -0
  208. package/src/core/admin-onclick.js +2 -1
  209. package/src/core/conflict-notice-ui.js +4 -7
  210. package/src/core/download.js +2 -2
  211. package/src/core/host-meta.js +38 -0
  212. package/src/core/is-edit-mode.js +17 -1
  213. package/src/core/mode-key.js +125 -19
  214. package/src/core/mode-support.js +5 -0
  215. package/src/core/mode-transition.js +20 -6
  216. package/src/core/preserve-authored.js +4 -4
  217. package/src/core/recovery.js +121 -33
  218. package/src/core/save-accepted.js +33 -0
  219. package/src/core/save-core.js +106 -31
  220. package/src/core/save-job.js +108 -0
  221. package/src/core/save-prepare.js +53 -0
  222. package/src/core/save-size-notice.js +3 -1
  223. package/src/core/save-slices.js +15 -0
  224. package/src/core/save.js +557 -127
  225. package/src/core/serialize-steps.js +109 -0
  226. package/src/core/snapshot.js +170 -39
  227. package/src/core/source-map.js +83 -38
  228. package/src/core/stale-host-notice.js +1 -0
  229. package/src/core/upgrade-run.js +59 -8
  230. package/src/dom/active-element.js +5 -0
  231. package/src/lib/assets.js +1 -0
  232. package/src/lib/slice.js +65 -0
  233. package/src/lib/uimotion.js +774 -0
  234. package/src/lib/unsaved-state.js +2 -2
  235. package/src/live/fast-lane.js +10 -3
  236. package/src/live/local-lane.js +202 -11
  237. package/src/live/ops.js +16 -0
  238. package/src/live/page-compaction.js +162 -0
  239. package/src/live/server-lane.js +1 -0
  240. package/src/live/store.js +11 -3
  241. package/src/loader-logic.js +3 -1
  242. package/src/loader.js +14 -4
  243. package/src/page/adopt-boot.js +2 -1
  244. package/src/page/awareness.js +33 -2
  245. package/src/page/blocks.js +89 -27
  246. package/src/page/boot-pack.js +1 -1
  247. package/src/page/chart-tables.js +21 -3
  248. package/src/page/chart.js +406 -63
  249. package/src/page/checklist.js +10 -135
  250. package/src/page/checkpoint-proof.js +7 -1
  251. package/src/page/chrome.js +250 -51
  252. package/src/page/clipboard-shape.js +126 -0
  253. package/src/page/drag.js +33 -3
  254. package/src/page/editor.js +136 -25
  255. package/src/page/export-assets.js +357 -0
  256. package/src/page/export-html.js +209 -0
  257. package/src/page/export-markdown.js +361 -0
  258. package/src/page/features/checklist.js +428 -0
  259. package/src/page/features/export-html.js +189 -0
  260. package/src/page/features/export-markdown.js +169 -0
  261. package/src/page/features/import-markdown.js +349 -0
  262. package/src/page/features/media.js +61 -5
  263. package/src/page/features/registry.js +18 -0
  264. package/src/page/features/table.js +373 -83
  265. package/src/page/features/title-cover.js +331 -0
  266. package/src/page/features/title-icon.js +213 -0
  267. package/src/page/features/toc.js +87 -0
  268. package/src/page/format-command.js +25 -1
  269. package/src/page/format-gate.js +68 -18
  270. package/src/page/format-host.js +17 -0
  271. package/src/page/format-native-commands.js +21 -5
  272. package/src/page/image-actions.js +64 -0
  273. package/src/page/import-markdown.js +262 -0
  274. package/src/page/items.js +135 -17
  275. package/src/page/markdown.js +6 -2
  276. package/src/page/media.js +35 -119
  277. package/src/page/placeholder.js +14 -1
  278. package/src/page/projection.js +98 -10
  279. package/src/page/recover.js +10 -12
  280. package/src/page/recovery-ui.js +464 -0
  281. package/src/page/rich/chart.js +63 -7
  282. package/src/page/save-projection.js +28 -0
  283. package/src/page/save-states.js +13 -3
  284. package/src/page/schema.js +19 -3
  285. package/src/page/table-paste.js +3 -1
  286. package/src/page/table-style.js +4 -4
  287. package/src/page/table.js +44 -2
  288. package/src/page/themes/light-1.css +8 -7
  289. package/src/page/themes/light-1.source.css +8 -7
  290. package/src/page/toc-link.js +31 -0
  291. package/src/page/toc.js +5 -0
  292. package/src/page/view-hooks.js +38 -12
  293. package/src/page/view.js +13 -0
  294. package/src/page/ybind/binding.js +88 -5
  295. package/src/page/ybind/bridge.js +187 -20
  296. package/src/page/ybind/containment.js +162 -0
  297. package/src/page/ybind/doc.js +17 -9
  298. package/src/page/ybind/file-debt.js +63 -0
  299. package/src/page/ybind/layout.js +7 -2
  300. package/src/page/ybind/marks.js +1 -1
  301. package/src/page/ybind/moves.js +5 -4
  302. package/src/page/ybind/origins.js +106 -4
  303. package/src/page/ybind/parts.js +8 -1
  304. package/src/page/ybind/provider.js +113 -17
  305. package/src/page/ybind/recovery-proof.js +32 -9
  306. package/src/page/ybind/recovery-replay.js +558 -15
  307. package/src/page/ybind/recovery.js +174 -45
  308. package/src/page/ybind/table.js +67 -11
  309. package/src/page/ybind/walk-delta.js +258 -0
  310. package/src/page/ybind/walk.js +54 -29
  311. package/src/page/ybind/writer.js +1 -1
  312. package/src/plugins/ai-edit.js +13 -9
  313. package/src/plugins/indicator-chip.js +91 -0
  314. package/src/plugins/indicator.js +13 -80
  315. package/src/plugins/live.js +89 -14
  316. package/src/plugins/menu/actions.js +196 -0
  317. package/src/plugins/menu/people.js +279 -0
  318. package/src/plugins/menu/placement.js +121 -0
  319. package/src/plugins/menu/preferences.js +9 -0
  320. package/src/plugins/menu/seo.js +316 -397
  321. package/src/plugins/menu/style.js +171 -0
  322. package/src/plugins/menu/variants.js +296 -0
  323. package/src/plugins/menu.js +186 -183
  324. package/src/plugins/page-view.js +3 -2
  325. package/src/plugins/page.js +249 -44
  326. package/src/plugins/sortable.js +8 -1
  327. package/src/plugins/source.js +6 -0
  328. package/src/plugins/upgrade.js +51 -17
  329. package/src/plugins/upload-ui.js +12 -17
  330. package/src/plugins/visitor-drafts/bottom-ui.js +12 -4
  331. package/src/plugins/visitor-drafts/intro-style.js +7 -2
  332. package/src/plugins/visitor-drafts/intro.js +5 -3
  333. package/src/plugins/visitor-drafts/owner-chip.js +8 -12
  334. package/src/plugins/visitor-drafts/prompt.js +11 -9
  335. package/src/plugins/visitor-drafts/source.js +4 -3
  336. package/src/plugins/visitor-drafts/store.js +138 -9
  337. package/src/plugins/visitor-drafts/ui-memory.js +2 -2
  338. package/src/plugins/visitor-drafts-owner.js +4 -6
  339. package/src/plugins/visitor-drafts.js +5 -3
  340. package/src/recovery/store.js +1 -1
  341. package/src/route/route.js +4 -0
  342. package/src/server/rooms/index.js +5 -1
  343. package/src/server/rooms/live-data-rooms.js +302 -50
  344. package/src/server/rooms/page-compaction-worker.js +12 -0
  345. package/src/server/rooms/page-compactor.js +92 -0
  346. package/src/server/rooms/page-room-conformance.js +461 -0
  347. package/src/source/parse-steps.js +94 -0
  348. package/src/source/session.js +233 -57
  349. package/src/sync/editing-sender.js +12 -0
  350. package/src/sync/live-sync-edit.js +22 -2
  351. package/src/sync/live-sync.js +150 -23
  352. package/src/sync/page-relay.js +52 -0
  353. package/src/sync/presence-store.js +44 -4
  354. package/src/sync/presence.js +6 -3
  355. package/src/sync/saved-stream.js +11 -6
  356. package/src/ui/bevel-controls.js +1 -0
  357. package/src/ui/bevel-dialog.js +5 -2
  358. package/src/ui/editor-ui.js +77 -0
  359. package/src/ui/page-colour.js +281 -0
  360. package/src/ui/toast.js +16 -7
  361. package/src/vendor/hyper-html-api-upgrade.vendor.js +2 -2
  362. package/src/vendor/hyper-undo.vendor.js +1 -1
  363. package/src/vendor/hypercms.vendor.js +9 -9
  364. package/src/vendor/parse5.vendor.js +2 -2
  365. package/src/vendor/sap-core.vendor.js +5 -5
  366. package/dist/m/c-2SPVQSHC.js +0 -1
  367. package/dist/m/c-336SGU5W.js +0 -1
  368. package/dist/m/c-3H6BZDNJ.js +0 -6
  369. package/dist/m/c-4DFEJ2PO.js +0 -3
  370. package/dist/m/c-56QRZTZH.js +0 -1
  371. package/dist/m/c-5J4E7O4Y.js +0 -4
  372. package/dist/m/c-5LKDWQTL.js +0 -19
  373. package/dist/m/c-5V3MOVJT.js +0 -1
  374. package/dist/m/c-6AZOGAPE.js +0 -1
  375. package/dist/m/c-6CRUUXOX.js +0 -5
  376. package/dist/m/c-6HMB24EV.js +0 -1
  377. package/dist/m/c-6KEVEQ7L.js +0 -1
  378. package/dist/m/c-A3E3NIMC.js +0 -1
  379. package/dist/m/c-BQ6LOBEE.js +0 -1
  380. package/dist/m/c-BTDI4Y23.js +0 -1
  381. package/dist/m/c-CJ3A7ZKE.js +0 -1
  382. package/dist/m/c-CQHPGKX2.js +0 -1
  383. package/dist/m/c-DFS4XZJV.js +0 -2
  384. package/dist/m/c-DNENWY4O.js +0 -1
  385. package/dist/m/c-E4LIR5X5.js +0 -1
  386. package/dist/m/c-FDNHHZY5.js +0 -1
  387. package/dist/m/c-FROJUZMV.js +0 -1
  388. package/dist/m/c-G2JPKCTP.js +0 -1
  389. package/dist/m/c-GB6TU7WV.js +0 -1
  390. package/dist/m/c-GFJJEBTU.js +0 -1
  391. package/dist/m/c-GJAEGGXY.js +0 -1
  392. package/dist/m/c-GOWBDM6J.js +0 -1
  393. package/dist/m/c-HFKUVACH.js +0 -1
  394. package/dist/m/c-HHDFCNHV.js +0 -1
  395. package/dist/m/c-IEM4NTCS.js +0 -1
  396. package/dist/m/c-JDCX26UP.js +0 -25
  397. package/dist/m/c-KGVGROZT.js +0 -1
  398. package/dist/m/c-KQ6TYJAP.js +0 -135
  399. package/dist/m/c-LFPHCXY3.js +0 -1
  400. package/dist/m/c-LRPUO2MX.js +0 -1
  401. package/dist/m/c-LWLBHLVC.js +0 -3
  402. package/dist/m/c-M2L47S2X.js +0 -7
  403. package/dist/m/c-MFP2ZWD4.js +0 -1
  404. package/dist/m/c-MKSQ4Y57.js +0 -1
  405. package/dist/m/c-NUFFJVVJ.js +0 -1
  406. package/dist/m/c-NUHSPT5P.js +0 -1
  407. package/dist/m/c-O72HVYSA.js +0 -1
  408. package/dist/m/c-OIBUTDXI.js +0 -1
  409. package/dist/m/c-OW5QIGG3.js +0 -1
  410. package/dist/m/c-PGRCK73F.js +0 -1
  411. package/dist/m/c-Q7NQD65V.js +0 -1
  412. package/dist/m/c-QH63LQOS.js +0 -1
  413. package/dist/m/c-R7IWVINQ.js +0 -1
  414. package/dist/m/c-RM7J7BV2.js +0 -1
  415. package/dist/m/c-S4ZEH46B.js +0 -9
  416. package/dist/m/c-SDAXDM2D.js +0 -1
  417. package/dist/m/c-SMGU7RT3.js +0 -235
  418. package/dist/m/c-SPIORRYS.js +0 -1
  419. package/dist/m/c-SWLRT66R.js +0 -1
  420. package/dist/m/c-TCJTVAFN.js +0 -1
  421. package/dist/m/c-TQEMVJCS.js +0 -1
  422. package/dist/m/c-TYLXX4NF.js +0 -21
  423. package/dist/m/c-U62OB4BT.js +0 -1
  424. package/dist/m/c-U6YWUAR3.js +0 -8
  425. package/dist/m/c-UDCSU5G5.js +0 -2
  426. package/dist/m/c-VI5RMSVY.js +0 -1
  427. package/dist/m/c-VRZN56XY.js +0 -1
  428. package/dist/m/c-W5MCHEGA.js +0 -1
  429. package/dist/m/c-WNYBLIOZ.js +0 -1
  430. package/dist/m/c-WWXIDH4O.js +0 -28
  431. package/dist/m/c-XCNTK76U.js +0 -1
  432. package/dist/m/c-XMS6DPBR.js +0 -1
  433. package/dist/m/c-ZUCOKOHT.js +0 -1
  434. package/dist/m/c-ZVPVXTJF.js +0 -4
  435. package/dist/m/c-ZVRKXKM7.js +0 -1
  436. package/dist/m/core/autosave-HRYGUWV6.js +0 -1
  437. package/dist/m/core/conflict-notice-6MROJN2L.js +0 -1
  438. package/dist/m/core/is-edit-mode-KTWA3O5N.js +0 -1
  439. package/dist/m/core/recovery-FLTCC2OC.js +0 -1
  440. package/dist/m/core/save-HQZ4S667.js +0 -1
  441. package/dist/m/core/save-core-KGQWFUNH.js +0 -1
  442. package/dist/m/core/snapshot-EGQZNT45.js +0 -1
  443. package/dist/m/core/unsaved-warning-IDJLD66H.js +0 -1
  444. package/dist/m/loader-S4I7GKXR.js +0 -1
  445. package/dist/m/manifest-4C419D67.js +0 -1
  446. package/dist/m/manifest-edit-B077F288.js +0 -1
  447. package/dist/m/plugins/ai-edit-QN4PMMNL.js +0 -6
  448. package/dist/m/plugins/indicator-URDBBOQO.js +0 -1
  449. package/dist/m/plugins/live-HFWD4OIH.js +0 -1
  450. package/dist/m/plugins/menu-3HIEQSI6.js +0 -138
  451. package/dist/m/plugins/page-JOKB6XPR.js +0 -2
  452. package/dist/m/plugins/page-view-NXOJHNW6.js +0 -1
  453. package/dist/m/plugins/quickcrop-QNY3PKLD.js +0 -26
  454. package/dist/m/plugins/sortable-GWT4PWDV.js +0 -1
  455. package/dist/m/plugins/source-4ZRSAIYM.js +0 -1
  456. package/dist/m/plugins/undo-VDA5P7FH.js +0 -1
  457. package/dist/m/plugins/upgrade-W7FG5VFG.js +0 -1
  458. package/dist/m/plugins/visitor-drafts-LIDY7NH6.js +0 -21
  459. package/dist/m/plugins/visitor-drafts-owner-ZXCKBZE7.js +0 -1
  460. package/dist/m/reader-core-DXYCURFF.js +0 -68
  461. package/dist/m/route/route-KOCZSEGN.js +0 -1
  462. package/dist/m/sync/live-sync-J7VMYTKS.js +0 -1
  463. package/dist/m/ui/index-3JLVI27O.js +0 -1
  464. package/dist/m/vendor/sap-core.vendor-A7BMUYZT.js +0 -6
  465. /package/dist/m/{c-FILLTJA7.js → c-QTTJYJMA.js} +0 -0
  466. /package/dist/m/plugins/{quickcrop-QNY3PKLD.js.LEGAL.txt → quickcrop-BQ5YYFCH.js.LEGAL.txt} +0 -0
  467. /package/dist/m/plugins/{richclay-NRQDSWX6.js.LEGAL.txt → richclay-KLGETK77.js.LEGAL.txt} +0 -0
@@ -20,6 +20,7 @@ import { sameAuthority, hostIdentityOf, pathIdentityOf } from "../../recovery/id
20
20
  import { sameLineage, sharesAncestry, lineageOf } from "./lineage.js";
21
21
  import { canonicalPageBytes, containsUpdate, digestHex, validatePageCheckpoint } from "./recovery-proof.js";
22
22
  import { RECOVERY_ORIGIN } from "./provider.js";
23
+ import { registerUnsavedState } from "../../lib/unsaved-state.js";
23
24
 
24
25
  /** The channel this adapter journals and replays: one name, one version, everywhere. */
25
26
  export const PAGE_CHANNEL = "page.yjs";
@@ -193,11 +194,22 @@ export function applyCapsule(doc, parts) {
193
194
  *
194
195
  * Nothing is applied for a capsule that fails admission: a changed authority, a changed
195
196
  * generation, a base the page no longer contains or no shared ancestry is review, not a
196
- * merge. A capsule with no owned work has nothing to restore and is skipped silently.
197
+ * merge. A capsule with no owned work has nothing to restore and is skipped silently, and
198
+ * so is one from the generation before a save that already holds all its work: that save
199
+ * reached the file after its tab stopped listening for the answer.
197
200
  *
198
201
  * @param {{records?: Array<Object>, current: {context: Object, state: Object}, into: Object}} options
199
202
  * @returns {{replayed: Array<Object>, review: Array<Object>}}
200
203
  */
204
+ function alreadyHeld(entry, current) {
205
+ try {
206
+ const { owned } = accumulateCapsule({ capsule: entry.capsule, batches: entry.batches });
207
+ return owned.length > 0 && containsUpdate(canonicalPageBytes(current.state), owned);
208
+ } catch {
209
+ return false;
210
+ }
211
+ }
212
+
201
213
  export function replayJournals({ records = [], current, into } = {}) {
202
214
  const replayed = [];
203
215
  const review = [];
@@ -206,8 +218,8 @@ export function replayJournals({ records = [], current, into } = {}) {
206
218
  try {
207
219
  parts = admitCapsule({ capsule: entry.capsule, batches: entry.batches, current });
208
220
  } catch (error) {
209
- if (error?.code === "nothing-owned") continue;
210
- review.push({ capsule: entry.capsule, code: error?.code ?? "invalid-capsule", message: error?.message ?? "This journal cannot be replayed" });
221
+ if (error?.code === "nothing-owned" || (error?.code === "foreign-generation" && alreadyHeld(entry, current))) continue;
222
+ review.push({ capsule: entry.capsule, batches: entry.batches, code: error?.code ?? "invalid-capsule", message: error?.message ?? "This journal cannot be replayed" });
211
223
  continue;
212
224
  }
213
225
  applyCapsule(into, parts);
@@ -244,25 +256,49 @@ export async function pageJournalContext({ authority, generation = null, base =
244
256
  * host with no people support gives its document to whoever holds it, so the document's
245
257
  * own host identity is the principal there.
246
258
  *
259
+ * `scope` records how the document was named. A host that gives the document an identity
260
+ * of its own is `"host"`; a host that gives it none, a local file among them, is `"path"`,
261
+ * and then the exact pathname is the whole of what this page's identity is.
262
+ *
247
263
  * @param {Object} [options]
248
264
  * @param {?Object} [options.meta] - the host's discovery answer
249
265
  * @param {?Location} [options.location]
250
266
  * @param {?Element} [options.root]
251
- * @returns {?{host: string, documentId: string, principalId: string}}
267
+ * @returns {?{host: string, documentId: string, principalId: string, scope: "host"|"path"}}
252
268
  */
253
269
  export function pageAuthority({ meta = null, location: where = globalThis.location, root = globalThis.document?.documentElement } = {}) {
254
270
  const host = where?.origin ?? "";
255
271
  if (!host) return null;
256
- const documentId = hostIdentityOf(root)?.identity ?? pathIdentityOf(where?.pathname ?? "/").identity;
272
+ const hostIdentity = hostIdentityOf(root);
273
+ const scope = hostIdentity ? "host" : "path";
274
+ const documentId = hostIdentity?.identity ?? pathIdentityOf(where?.pathname ?? "/").identity;
257
275
  if (!documentId) return null;
258
276
  const people = meta?.document?.people;
259
277
  const principalId = people && typeof people === "object" ? people.me?.id ?? null : documentId;
260
278
  if (!principalId) return null;
261
- return { host, documentId, principalId };
279
+ return { host, documentId, principalId, scope };
262
280
  }
263
281
 
264
282
  const journalKey = (id) => (typeof id === "string" ? id : JSON.stringify(id));
265
283
 
284
+ // Is `next` a later accepted state than `base`? A state vector only grows, so one that
285
+ // is behind the base anywhere is an older save that finished late.
286
+ function newer(next, base) {
287
+ if (!base.length) return true;
288
+ const ahead = Y.decodeStateVector(Y.encodeStateVectorFromUpdateV2(base));
289
+ const behind = Y.decodeStateVector(Y.encodeStateVectorFromUpdateV2(next));
290
+ for (const [client, clock] of ahead) if ((behind.get(client) ?? 0) < clock) return false;
291
+ return true;
292
+ }
293
+
294
+ // The part of a capsule the new base does not already hold: work a compaction merged in
295
+ // before the save that covered it reads as nothing once that save is the base.
296
+ function unsaved(base, part) {
297
+ const bytes = bytesOf(part);
298
+ if (!bytes || !bytes.length) return new Uint8Array(0);
299
+ return containsUpdate(base, bytes) ? new Uint8Array(0) : bytes;
300
+ }
301
+
266
302
  /**
267
303
  * The accepted page state an episode starts from, as the bytes its capsule keeps.
268
304
  *
@@ -315,13 +351,17 @@ export function pageChannel(service) {
315
351
  export function createPageJournal({ channel, key, context, base = null, onChange = () => {} }) {
316
352
  const records = new Map();
317
353
  const waiting = [];
354
+ const awaiting = [];
318
355
  const inflight = new Set();
319
- const bytesOfBase = bytesOf(base) ?? new Uint8Array(0);
356
+ let bytesOfBase = bytesOf(base) ?? new Uint8Array(0);
320
357
  let resolved = null;
321
358
  let failure = null;
322
359
  let batches = 0;
323
360
  let batchBytes = 0;
324
361
  let episode = false;
362
+ let carryBase = true;
363
+ let holding = false;
364
+ let rebasing = Promise.resolve(null);
325
365
  let compacting = null;
326
366
  let closed = false;
327
367
 
@@ -329,16 +369,38 @@ export function createPageJournal({ channel, key, context, base = null, onChange
329
369
  channel,
330
370
  key,
331
371
  onChange,
332
- context: () => ready,
372
+ // The context of the base the journal holds now: an accepted save moves it.
373
+ context: () => ready.then(() => resolved),
333
374
  local: (bytes) => send(bytes, "local"),
334
375
  // Only while an episode is open: a peer's updates on their own are not this tab's work
335
376
  // to keep, and a page that has journaled nothing owns nothing.
336
377
  dependency: (bytes) => { if (episode) send(bytes, "dependency"); },
337
378
  records: () => [...records.values()],
338
379
  names,
339
- pending: () => inflight.size + (failure ? 1 : 0),
380
+ // The next record this journal commits, or null when the store refuses one or the
381
+ // session closes: the destination a copy names is only real once it is on this device.
382
+ next: () => new Promise((resolve) => { awaiting.push(resolve); }),
383
+ // Does the work this journal holds carry exactly this block's text? The episode's own
384
+ // base is here, so the answer needs no store and no page.
385
+ covers: (blockId, text) => {
386
+ const owned = [...records.values()].map((record) => record.update).filter((update) => update && update.byteLength);
387
+ const capsule = {
388
+ kind: "binary",
389
+ version: 1,
390
+ id: "journal",
391
+ revision: 0,
392
+ payload: {
393
+ base: bytesOfBase,
394
+ dependencies: new Uint8Array(0),
395
+ owned: owned.length ? Y.mergeUpdatesV2(owned) : new Uint8Array(0),
396
+ },
397
+ };
398
+ return coversText([capsule], blockId, text);
399
+ },
400
+ pending: () => inflight.size + (holding ? waiting.length : 0) + (failure ? 1 : 0),
340
401
  get failure() { return failure; },
341
402
  retire,
403
+ rebase,
342
404
  compact,
343
405
  // An acknowledgement is metadata: it says a room holds the bytes and retires nothing.
344
406
  acknowledged: () => {
@@ -348,6 +410,7 @@ export function createPageJournal({ channel, key, context, base = null, onChange
348
410
  close: () => {
349
411
  closed = true;
350
412
  waiting.length = 0;
413
+ settle(null);
351
414
  },
352
415
  };
353
416
 
@@ -367,8 +430,61 @@ export function createPageJournal({ channel, key, context, base = null, onChange
367
430
  return resolved;
368
431
  });
369
432
 
433
+ function settle(value) {
434
+ for (const resolve of awaiting.splice(0)) resolve(value);
435
+ }
436
+
370
437
  function retire(list) {
371
- for (const item of list || []) records.delete(journalKey(item?.id));
438
+ for (const item of list || []) {
439
+ const key = journalKey(item?.id);
440
+ const record = records.get(key);
441
+ if (!record) continue;
442
+ records.delete(key);
443
+ batches = Math.max(0, batches - 1);
444
+ batchBytes = Math.max(0, batchBytes - (record.update?.byteLength ?? 0));
445
+ }
446
+ }
447
+
448
+ // An accepted save is the base the next replay starts from: the page reopens on it,
449
+ // and the batches it retired are no longer there to build on. Rebases run one at a
450
+ // time, appends wait for them, and a save older than the base already held is ignored.
451
+ function rebase(state) {
452
+ rebasing = rebasing.then(() => moveBase(bytesOf(state))).catch(() => null);
453
+ return rebasing;
454
+ }
455
+
456
+ async function moveBase(next) {
457
+ if (closed || !resolved || !next || !next.length || !newer(next, bytesOfBase)) return null;
458
+ holding = true;
459
+ try {
460
+ while (inflight.size && !closed) await journal.next();
461
+ if (closed) return null;
462
+ const moved = typeof context === "function" ? await context(next) : context;
463
+ if (!moved || !moved.authority) return null;
464
+ if (episode) {
465
+ const answer = typeof channel.rebase === "function"
466
+ ? await channel.rebase({
467
+ key,
468
+ context: moved,
469
+ rebuild: (payload) => ({
470
+ base: next,
471
+ dependencies: unsaved(next, payload.dependencies),
472
+ owned: unsaved(next, payload.owned),
473
+ }),
474
+ })
475
+ : null;
476
+ // A writer whose capsule never reached the store starts its next one on this base.
477
+ if (answer?.code === "missing") carryBase = true;
478
+ else if (!answer?.ok) return null;
479
+ }
480
+ bytesOfBase = next;
481
+ resolved = moved;
482
+ for (const record of records.values()) record.context = moved;
483
+ return moved;
484
+ } finally {
485
+ holding = false;
486
+ if (resolved) for (const run of waiting.splice(0)) run(resolved);
487
+ }
372
488
  }
373
489
 
374
490
  function names() {
@@ -383,8 +499,6 @@ export function createPageJournal({ channel, key, context, base = null, onChange
383
499
  .then((receipt) => {
384
500
  if (!receipt?.ok) return receipt;
385
501
  retire(receipt.retired);
386
- batches = Math.max(0, batches - (receipt.retired?.length ?? 0));
387
- if (!batches) batchBytes = 0;
388
502
  return receipt;
389
503
  })
390
504
  .catch(() => null)
@@ -402,6 +516,7 @@ export function createPageJournal({ channel, key, context, base = null, onChange
402
516
  answer = channel.append({ key, context, update: bytes, origin, base: first ? bytesOfBase : null });
403
517
  } catch (_) {
404
518
  failure = "error";
519
+ settle(null);
405
520
  journal.onChange();
406
521
  return;
407
522
  }
@@ -412,6 +527,7 @@ export function createPageJournal({ channel, key, context, base = null, onChange
412
527
  inflight.delete(token);
413
528
  if (!receipt || receipt.ok !== true) {
414
529
  failure = receipt?.code ?? "error";
530
+ settle(null);
415
531
  journal.onChange();
416
532
  return;
417
533
  }
@@ -422,22 +538,29 @@ export function createPageJournal({ channel, key, context, base = null, onChange
422
538
  batchBytes += bytes.byteLength;
423
539
  }
424
540
  journal.onChange();
541
+ settle({ id: receipt.id, revision: receipt.revision });
425
542
  if (batches >= COMPACT_BATCHES || batchBytes >= COMPACT_BYTES) compact();
426
543
  },
427
544
  () => {
428
545
  inflight.delete(token);
429
546
  failure = "error";
547
+ settle(null);
430
548
  journal.onChange();
431
549
  },
432
550
  );
433
551
  }
434
552
 
553
+ function dispatch(context, bytes, origin) {
554
+ const first = carryBase;
555
+ carryBase = false;
556
+ write(context, bytes, origin, first);
557
+ }
558
+
435
559
  function send(bytes, origin) {
436
560
  if (closed || !bytes || !bytes.byteLength || typeof channel.append !== "function") return;
437
- const first = !episode;
438
561
  episode = true;
439
- if (resolved) write(resolved, bytes, origin, first);
440
- else waiting.push((context) => write(context, bytes, origin, first));
562
+ if (resolved && !holding) dispatch(resolved, bytes, origin);
563
+ else waiting.push((context) => dispatch(context, bytes, origin));
441
564
  }
442
565
 
443
566
  return journal;
@@ -472,6 +595,90 @@ export function openPageJournal({ meta = null, service = null, base = null, onCh
472
595
  });
473
596
  }
474
597
 
598
+ /**
599
+ * Where this tab's typing lives, and the wait that lets an exit leave edit.
600
+ *
601
+ * The answer is only ever as strong as the evidence behind it: a write whose transaction
602
+ * has not committed is Pending, committed records are Stored, a room's confirmation makes
603
+ * them Shared, and only a checkpoint that covers them makes the page Saved. The same
604
+ * handle is the exit's barrier for the journal: every append already handed over commits,
605
+ * and the store's own writes are down, before it answers. A journal that cannot write at
606
+ * all holds nothing and says so once, as Unavailable, so a person is never stranded in
607
+ * edit by a device that has stopped taking bytes.
608
+ *
609
+ * @param {Object} options
610
+ * @param {Object} options.journal - this session's page journal
611
+ * @param {?Object} [options.store] - the live page store, for its confirmations
612
+ * @param {Function} [options.reduce] - the precedence reducer, (facts) => state or null
613
+ * @param {Function} [options.onState] - (state) for every state the facts change to
614
+ * @param {Function} [options.onUnavailable] - (code) when the journal cannot write
615
+ * @returns {{checkpoint: Function, state: Function, dispose: Function}}
616
+ */
617
+ export function watchJournal({ journal, store = null, reduce = () => null, onState = () => {}, onUnavailable = () => {} } = {}) {
618
+ if (!journal) return { checkpoint() {}, state: () => null, dispose() {} };
619
+ const seen = { acknowledged: false, checkpointed: false };
620
+ let last = null;
621
+ const state = () => {
622
+ const failure = journal.failure ?? null;
623
+ const records = journal.records().length;
624
+ return reduce({
625
+ unavailable: Boolean(failure),
626
+ pending: failure ? 0 : journal.pending(),
627
+ journaled: seen.acknowledged ? 0 : records,
628
+ acknowledged: seen.acknowledged ? records : 0,
629
+ checkpointed: seen.checkpointed && !records,
630
+ });
631
+ };
632
+ const publish = () => {
633
+ const next = state();
634
+ if (!next || next === last) return last;
635
+ last = next;
636
+ if (next === "unavailable") onUnavailable(journal.failure);
637
+ else onState(next);
638
+ return next;
639
+ };
640
+ // The binding owns this callback and hears every append and refusal through it.
641
+ const inner = journal.onChange;
642
+ journal.onChange = () => { inner?.(); publish(); };
643
+ // Work handed over now is work no room has seen and no checkpoint covers.
644
+ const offLocal = store?.onLocal?.(() => {
645
+ seen.acknowledged = false;
646
+ seen.checkpointed = false;
647
+ publish();
648
+ });
649
+ const offConfirm = store?.onConfirm?.(() => {
650
+ seen.acknowledged = true;
651
+ publish();
652
+ });
653
+ const blocked = () => Boolean(journal.failure);
654
+ const source = registerUnsavedState({
655
+ id: "page:journal",
656
+ isPending: () => !blocked() && journal.pending() > 0,
657
+ flush: async () => {
658
+ if (blocked()) return { ok: true };
659
+ while (!blocked() && journal.pending() > 0) {
660
+ if (!(await journal.next())) break;
661
+ }
662
+ await Promise.resolve(journal.channel?.flush?.()).catch(() => null);
663
+ return { ok: blocked() || journal.pending() === 0 };
664
+ },
665
+ });
666
+ publish();
667
+ return {
668
+ checkpoint() {
669
+ seen.checkpointed = true;
670
+ publish();
671
+ },
672
+ state,
673
+ dispose() {
674
+ journal.onChange = inner;
675
+ offLocal?.();
676
+ offConfirm?.();
677
+ source.dispose();
678
+ },
679
+ };
680
+ }
681
+
475
682
  /**
476
683
  * Replay this page's journal once, after the authority snapshot is adopted.
477
684
  *
@@ -486,3 +693,339 @@ export async function replayInto({ journal, state, into } = {}) {
486
693
  if (!listed?.ok) return { replayed: [], review: [] };
487
694
  return replayJournals({ records: listed.records, current: { context, state }, into });
488
695
  }
696
+
697
+ const posOf = (block) => (typeof block?.pos === "string" ? block.pos : "");
698
+ const idsInOrder = (blocks) =>
699
+ Object.keys(blocks).sort((a, b) => {
700
+ const left = posOf(blocks[a]);
701
+ const right = posOf(blocks[b]);
702
+ if (left !== right) return left < right ? -1 : 1;
703
+ return a < b ? -1 : a > b ? 1 : 0;
704
+ });
705
+
706
+ // The text of one block, cells included: what a person reads in a review, and what an
707
+ // insert-as-text offers back.
708
+ function blockText(block) {
709
+ if (typeof block?.c === "string") return block.c;
710
+ if (!block?.rows) return "";
711
+ const parts = [];
712
+ for (const row of Object.values(block.rows)) {
713
+ for (const cell of Object.values(row?.cells ?? {})) {
714
+ const text = typeof cell === "string" ? cell : typeof cell?.c === "string" ? cell.c : "";
715
+ if (text) parts.push(text);
716
+ }
717
+ }
718
+ return parts.join(" ");
719
+ }
720
+
721
+ /**
722
+ * The page a set of records reconstructs, block by block.
723
+ *
724
+ * Records arrive in the store's shape — capsules with their batches — or as the updates
725
+ * a live journal holds, and both read back as one page: they share a lineage, and Yjs
726
+ * merges them by operation identity.
727
+ *
728
+ * @param {Array<Object>} [records]
729
+ * @returns {?Object} the block model, by block id, or null when it cannot be decoded
730
+ */
731
+ function pageOf(records = []) {
732
+ const doc = new Y.Doc();
733
+ try {
734
+ for (const entry of capsulesOf(records)) {
735
+ let parts;
736
+ try {
737
+ parts = accumulateCapsule(entry);
738
+ } catch (_) {
739
+ continue;
740
+ }
741
+ for (const bytes of [parts.base, parts.dependencies, parts.owned]) if (bytes.length) Y.applyUpdateV2(doc, bytes, NET);
742
+ }
743
+ for (const record of records || []) {
744
+ const update = bytesOf(record?.update);
745
+ if (update && update.length) Y.applyUpdateV2(doc, update, NET);
746
+ }
747
+ return blocksFromYValue({ v: 1, g: 0, y: { seed: toBase64(Y.encodeStateAsUpdateV2(doc)) } });
748
+ } catch (_) {
749
+ return null;
750
+ } finally {
751
+ doc.destroy();
752
+ }
753
+ }
754
+
755
+ /**
756
+ * The readable page a capsule reconstructs: one line per block, in the page's own order.
757
+ *
758
+ * A capsule that cannot be decoded at all reads as no text rather than as an error: a
759
+ * review still says what it can, and nothing here decides whether it may be replayed.
760
+ *
761
+ * @param {{capsule: Object, batches?: Array<Object>}} options
762
+ * @returns {string}
763
+ */
764
+ export function reconstructText({ capsule, batches = [] } = {}) {
765
+ let blocks;
766
+ try {
767
+ blocks = reconstructPage({ capsule, batches });
768
+ } catch (_) {
769
+ return "";
770
+ }
771
+ const lines = [];
772
+ for (const id of idsInOrder(blocks)) {
773
+ const text = blockText(blocks[id]);
774
+ if (text) lines.push(text);
775
+ }
776
+ return lines.join("\n");
777
+ }
778
+
779
+ /**
780
+ * The review rows a page draws for the journals it may not replay.
781
+ *
782
+ * Each row keeps the capsule it came from, so the person can read it, download it or
783
+ * copy its content, and nothing in a row is applied to the page.
784
+ *
785
+ * @param {Array<{capsule: Object, batches?: Array<Object>, code: string, message: string}>} [review]
786
+ * @returns {Array<Object>}
787
+ */
788
+ export function describeReview(review = []) {
789
+ return (review || []).map((entry) => ({
790
+ id: entry?.capsule?.id,
791
+ revision: entry?.capsule?.revision ?? 0,
792
+ code: entry?.code ?? "invalid-capsule",
793
+ message: entry?.message ?? "This journal cannot be replayed",
794
+ text: reconstructText({ capsule: entry?.capsule, batches: entry?.batches ?? [] }),
795
+ capsule: entry?.capsule ?? null,
796
+ batches: entry?.batches ?? [],
797
+ }));
798
+ }
799
+
800
+ /**
801
+ * Whether a capsule's own blocks reconstruct to exactly this text.
802
+ *
803
+ * Composition end asks this before retiring a shadow: an update the journal holds is a
804
+ * transaction, and only a transaction carrying the same visible text means the typing
805
+ * reached the document.
806
+ *
807
+ * @param {Array<Object>} records - the journal's records
808
+ * @param {?string} blockId
809
+ * @param {string} text
810
+ * @returns {boolean}
811
+ */
812
+ export function coversText(records = [], blockId = null, text = "") {
813
+ const id = blockId == null ? "" : String(blockId);
814
+ const want = typeof text === "string" ? text : "";
815
+ if (!id || !want) return false;
816
+ const blocks = pageOf(records);
817
+ return Boolean(blocks && blocks[id] && blockText(blocks[id]) === want);
818
+ }
819
+
820
+ /** The channel that records where a reviewed capsule was copied to: one name, one version. */
821
+ export const COPY_CHANNEL = "page.copy";
822
+ export const COPY_VERSION = 1;
823
+
824
+ // One channel per service, registered the first time something is actually copied.
825
+ const copyChannels = new WeakMap();
826
+
827
+ /**
828
+ * The copy-marker channel of one recovery service.
829
+ *
830
+ * @param {Object} service - `clay.recovery`
831
+ * @returns {?Object} the channel api
832
+ */
833
+ export function pageCopyChannel(service) {
834
+ if (!service) return null;
835
+ const existing = copyChannels.get(service);
836
+ if (existing) return existing;
837
+ const channel = service.register({ channel: COPY_CHANNEL, version: COPY_VERSION });
838
+ copyChannels.set(service, channel);
839
+ return channel;
840
+ }
841
+
842
+ const copyKey = (source) => `copy:${source?.key ?? ""}:${identityKey(source?.id)}`;
843
+
844
+ /**
845
+ * The transfer marker naming one capsule's copy, if this store has one.
846
+ *
847
+ * @param {{records?: Array<Object>, source: {key: string, id: *}}} options
848
+ * @returns {?{record: Object, source: Object, destination: Object}}
849
+ */
850
+ export function markerFor({ records = [], source = null } = {}) {
851
+ if (!source || source.id === undefined) return null;
852
+ for (const record of records || []) {
853
+ const payload = record?.payload;
854
+ if (!payload?.source || !payload?.destination) continue;
855
+ if (payload.source.key !== source.key) continue;
856
+ if (identityKey(payload.source.id) !== identityKey(source.id)) continue;
857
+ return { record, source: payload.source, destination: payload.destination };
858
+ }
859
+ return null;
860
+ }
861
+
862
+ /**
863
+ * What one marker says about the copy it names.
864
+ *
865
+ * "copied" while the destination record is still in the journal: the copy is on this
866
+ * device and nothing has proved it saved. "covered" once the destination is gone from
867
+ * the journal, which only a checkpoint (or a dismissed destination) can do.
868
+ *
869
+ * @param {{marker: ?Object, records?: Array<Object>}} options
870
+ * @returns {string} "open", "copied" or "covered"
871
+ */
872
+ export function copyState({ marker = null, records = [] } = {}) {
873
+ if (!marker?.destination) return "open";
874
+ const id = marker.destination.id;
875
+ const kept = (records || []).some((record) => record && identityKey(record.id) === identityKey(id));
876
+ return kept ? "copied" : "covered";
877
+ }
878
+
879
+ /**
880
+ * Insert a reviewed capsule's content as a copy, with fresh block ids.
881
+ *
882
+ * The content goes in through `insert`, which is the page's own document command and
883
+ * names the destination record it committed; nothing of the capsule's Yjs operations is
884
+ * merged into this page, so a changed lineage is never merged blindly. The source
885
+ * capsule is not retired here: it survives until a checkpoint covers the destination,
886
+ * and the marker written last says where that destination is. A copy already on this
887
+ * device is never made twice, and a refused or failed insert leaves the source exactly
888
+ * where it was.
889
+ *
890
+ * @param {Object} options
891
+ * @param {Object} options.channel - the `page.yjs` channel api
892
+ * @param {Object} options.copies - the copy-marker channel api
893
+ * @param {{key: string, id: *, revision: number}} options.source
894
+ * @param {Function} options.insert - () => Promise<{key: string, id: *, revision: number}>
895
+ * @returns {Promise<{ok: boolean, code?: string, destination?: Object, marker?: Object}>}
896
+ */
897
+ export async function recoverAsCopy({ channel, copies, source, insert } = {}) {
898
+ if (!channel || !copies || !source || typeof insert !== "function") return { ok: false, code: "unavailable" };
899
+ let list;
900
+ let marks;
901
+ try {
902
+ [list, marks] = await Promise.all([channel.list(), copies.list()]);
903
+ } catch (_) {
904
+ return { ok: false, code: "unavailable" };
905
+ }
906
+ const marker = markerFor({ records: marks?.ok ? marks.records : [], source });
907
+ if (marker) {
908
+ const state = copyState({ marker, records: list?.ok ? list.records : [] });
909
+ if (state === "copied") return { ok: false, code: "already-copied", marker };
910
+ if (state === "covered") return { ok: false, code: "covered", marker };
911
+ }
912
+ let destination;
913
+ try {
914
+ destination = await insert();
915
+ } catch (_) {
916
+ return { ok: false, code: "insert-failed" };
917
+ }
918
+ if (!destination || destination.revision === undefined || destination.id === undefined) return { ok: false, code: "insert-failed" };
919
+ copies.stage({
920
+ key: copyKey(source),
921
+ kind: "text",
922
+ payload: {
923
+ source: { key: source.key ?? "", id: source.id, revision: source.revision ?? 0 },
924
+ destination: { key: destination.key ?? "", id: destination.id, revision: destination.revision },
925
+ },
926
+ });
927
+ const written = await copies.flush();
928
+ if (!written?.ok) return { ok: false, code: written?.code || "error", destination };
929
+ return { ok: true, destination };
930
+ }
931
+
932
+ /** The channel a composition's visible text is kept on while it is not a transaction. */
933
+ export const IME_CHANNEL = "page.ime";
934
+ export const IME_VERSION = 1;
935
+
936
+ const IME_PREFIX = "composing:";
937
+
938
+ const imeChannels = new WeakMap();
939
+
940
+ /**
941
+ * The composition-shadow channel of one recovery service.
942
+ *
943
+ * @param {Object} service - `clay.recovery`
944
+ * @returns {?Object} the channel api
945
+ */
946
+ export function pageImeChannel(service) {
947
+ if (!service) return null;
948
+ const existing = imeChannels.get(service);
949
+ if (existing) return existing;
950
+ const channel = service.register({ channel: IME_CHANNEL, version: IME_VERSION });
951
+ imeChannels.set(service, channel);
952
+ return channel;
953
+ }
954
+
955
+ /**
956
+ * The shadow one composing block's visible text is kept under.
957
+ *
958
+ * Composition is never forced into a transaction: while it is active the block's text is
959
+ * staged on a text channel, so a crash offers it as text. Composition end retires the
960
+ * shadow only when a journaled update carries the same text — `covered`, or answered
961
+ * from the records given — and anything else is kept and offered, because that typing
962
+ * never reached the document.
963
+ *
964
+ * @param {{channel: ?Object, key: string}} options
965
+ * @returns {?Object}
966
+ */
967
+ export function openImeShadow({ channel = null, key = "" } = {}) {
968
+ if (!channel) return null;
969
+
970
+ async function drop(blockId) {
971
+ let listed;
972
+ try {
973
+ listed = await channel.list();
974
+ } catch (_) {
975
+ return 0;
976
+ }
977
+ if (!listed?.ok) return 0;
978
+ let retired = 0;
979
+ for (const record of listed.records) {
980
+ if (record?.payload?.blockId !== blockId) continue;
981
+ try {
982
+ const receipt = await channel.dismiss(record.id, { revision: record.revision });
983
+ if (receipt?.ok) retired += 1;
984
+ } catch (_) {}
985
+ }
986
+ return retired;
987
+ }
988
+
989
+ return {
990
+ key,
991
+ channel,
992
+ keep({ blockId = null, text = "" } = {}) {
993
+ const id = blockId == null ? "" : String(blockId);
994
+ const value = typeof text === "string" ? text : "";
995
+ if (!id || !value) return null;
996
+ return channel.stage({ key: `${IME_PREFIX}${id}`, kind: "text", payload: { blockId: id, text: value } });
997
+ },
998
+ async clear({ blockId = null, text = "", covered = null, records = [] } = {}) {
999
+ const id = blockId == null ? "" : String(blockId);
1000
+ if (!id) return { ok: false, code: "invalid" };
1001
+ const carried = typeof covered === "boolean" ? covered : coversText(records, id, text);
1002
+ if (!carried) return { ok: false, code: "not-covered" };
1003
+ const retired = await drop(id);
1004
+ return retired ? { ok: true, retired } : { ok: false, code: "not-covered" };
1005
+ },
1006
+ async list() {
1007
+ return channel.list();
1008
+ },
1009
+ dispose() {
1010
+ return channel.flush();
1011
+ },
1012
+ };
1013
+ }
1014
+
1015
+ /**
1016
+ * The composition shadows a page's store holds as reviewable text.
1017
+ *
1018
+ * @param {Array<Object>} [records] - the composition channel's records
1019
+ * @returns {Array<{id: *, revision: number, blockId: string, text: string}>}
1020
+ */
1021
+ export function imeEntries(records = []) {
1022
+ const out = [];
1023
+ for (const record of records || []) {
1024
+ if (!record || record.supported === false) continue;
1025
+ const payload = record.payload;
1026
+ if (!payload || typeof payload.text !== "string" || !payload.text) continue;
1027
+ if (typeof payload.blockId !== "string" || !payload.blockId) continue;
1028
+ out.push({ id: record.id, revision: record.revision, blockId: payload.blockId, text: payload.text });
1029
+ }
1030
+ return out;
1031
+ }