@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
@@ -16,9 +16,23 @@ import * as Y from "../../vendor/yjs.vendor.js";
16
16
  import { toBase64 } from "../../vendor/yjs.vendor.js";
17
17
  import { NET } from "./layout.js";
18
18
  import { blocksFromYValue } from "./decode.js";
19
- import { sameAuthority } from "../../recovery/identity.js";
20
- import { sameLineage, sharesAncestry } from "./lineage.js";
21
- import { canonicalPageBytes, containsUpdate } from "./recovery-proof.js";
19
+ import { sameAuthority, hostIdentityOf, pathIdentityOf } from "../../recovery/identity.js";
20
+ import { sameLineage, sharesAncestry, lineageOf } from "./lineage.js";
21
+ import { canonicalPageBytes, containsUpdate, digestHex, validatePageCheckpoint } from "./recovery-proof.js";
22
+ import { RECOVERY_ORIGIN } from "./provider.js";
23
+ import { registerUnsavedState } from "../../lib/unsaved-state.js";
24
+
25
+ /** The channel this adapter journals and replays: one name, one version, everywhere. */
26
+ export const PAGE_CHANNEL = "page.yjs";
27
+ export const PAGE_CHANNEL_VERSION = 1;
28
+
29
+ // When one writer's batches are merged into its capsule: past either threshold, and never
30
+ // over a record the store has not confirmed.
31
+ export const COMPACT_BATCHES = 64;
32
+ export const COMPACT_BYTES = 1024 * 1024;
33
+
34
+ /** The channel's own merge, for the binary store's compaction. */
35
+ export const mergePageUpdates = (parts) => Y.mergeUpdatesV2(parts);
22
36
 
23
37
  export class PageReplayError extends Error {
24
38
  constructor(code, message) {
@@ -125,6 +139,893 @@ export function admitCapsule({ capsule, batches = [], current } = {}) {
125
139
  const stateBytes = canonicalPageBytes(current.state);
126
140
  if (parts.base.length && !containsUpdate(stateBytes, parts.base)) fail("missing-base", "The page no longer contains the journal's base");
127
141
  if (!parts.owned.length) fail("nothing-owned", "This capsule owns no local work");
128
- if (!sharesAncestry(current.state, { v: 1, g: 0, y: { seed: toBase64(parts.owned) } })) fail("foreign-lineage", "This journal shares no ancestry with this page");
142
+ // The reconstructed page is what has to share this page's ancestry, base included:
143
+ // this tab's own inserts carry its client alone, and never the page's.
144
+ const rebuilt = merge([parts.base, parts.dependencies, parts.owned]);
145
+ if (!sharesAncestry(current.state, { v: 1, g: 0, y: { seed: toBase64(rebuilt) } })) fail("foreign-lineage", "This journal shares no ancestry with this page");
129
146
  return parts;
130
147
  }
148
+
149
+ /**
150
+ * One writer's capsules, each with the batches that belong to it.
151
+ *
152
+ * A session that died before its batches were compacted leaves them behind, so a
153
+ * capsule is only ever read together with every batch naming it, whichever session
154
+ * wrote it.
155
+ *
156
+ * @param {Array<Object>} [records] - what the channel listed
157
+ * @returns {Array<{capsule: Object, batches: Array<Object>}>}
158
+ */
159
+ export function capsulesOf(records = []) {
160
+ const capsules = new Map();
161
+ const batches = new Map();
162
+ for (const record of records || []) {
163
+ if (!record || record.supported === false) continue;
164
+ if (record.kind === "binary") {
165
+ capsules.set(identityKey(record.id), record);
166
+ continue;
167
+ }
168
+ if (record.kind !== "binary-batch" || record.capsuleId === undefined) continue;
169
+ const key = identityKey(record.capsuleId);
170
+ const list = batches.get(key) ?? [];
171
+ list.push(record);
172
+ batches.set(key, list);
173
+ }
174
+ return [...capsules.entries()].map(([key, capsule]) => ({ capsule, batches: batches.get(key) ?? [] }));
175
+ }
176
+
177
+ /**
178
+ * Apply an admitted capsule's three parts into a live document.
179
+ *
180
+ * The recovery origin is its own: the provider offers the result to the room, and the
181
+ * journal never takes it as this tab's own work, so a recovery is never journaled twice.
182
+ *
183
+ * @param {Object} doc - the Y.Doc the page is being edited in
184
+ * @param {Object} parts - an `accumulateCapsule` result
185
+ */
186
+ export function applyCapsule(doc, parts) {
187
+ for (const bytes of [parts.base, parts.dependencies, parts.owned]) {
188
+ if (bytes && bytes.length) Y.applyUpdateV2(doc, bytes, RECOVERY_ORIGIN);
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Replay every capsule this page may replay, and name the ones it may not.
194
+ *
195
+ * Nothing is applied for a capsule that fails admission: a changed authority, a changed
196
+ * generation, a base the page no longer contains or no shared ancestry is review, not a
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.
200
+ *
201
+ * @param {{records?: Array<Object>, current: {context: Object, state: Object}, into: Object}} options
202
+ * @returns {{replayed: Array<Object>, review: Array<Object>}}
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
+
213
+ export function replayJournals({ records = [], current, into } = {}) {
214
+ const replayed = [];
215
+ const review = [];
216
+ for (const entry of capsulesOf(records)) {
217
+ let parts;
218
+ try {
219
+ parts = admitCapsule({ capsule: entry.capsule, batches: entry.batches, current });
220
+ } catch (error) {
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" });
223
+ continue;
224
+ }
225
+ applyCapsule(into, parts);
226
+ replayed.push({ id: entry.capsule.id, revision: parts.revision, owned: parts.owned.length });
227
+ }
228
+ return { replayed, review };
229
+ }
230
+
231
+ /**
232
+ * The context one episode's capsules are journaled under.
233
+ *
234
+ * A host that persists a page generation names it; without one, the generation is the
235
+ * digest of the accepted base, so a page rebuilt from other bytes is another generation
236
+ * and a page that has not changed keeps its own.
237
+ *
238
+ * @param {Object} options
239
+ * @param {{host: string, documentId: string, principalId: string}} options.authority
240
+ * @param {?string} options.generation
241
+ * @param {?Uint8Array} options.base - the accepted state captured at the episode's start
242
+ * @param {number} [options.formatFloor]
243
+ * @returns {Promise<Object>}
244
+ */
245
+ export async function pageJournalContext({ authority, generation = null, base = null, formatFloor = 0 } = {}) {
246
+ const bytes = bytesOf(base) ?? new Uint8Array(0);
247
+ const token = typeof generation === "string" && generation ? generation : await digestHex(bytes);
248
+ return { authority, lineage: await lineageOf(token, bytes), formatFloor };
249
+ }
250
+
251
+ /**
252
+ * The authority this page journals under, or null when it cannot be pinned.
253
+ *
254
+ * A host that names people names the account too, and an answer without one is not a
255
+ * principal: replay waits for the next boot rather than filing work under a guess. A
256
+ * host with no people support gives its document to whoever holds it, so the document's
257
+ * own host identity is the principal there.
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
+ *
263
+ * @param {Object} [options]
264
+ * @param {?Object} [options.meta] - the host's discovery answer
265
+ * @param {?Location} [options.location]
266
+ * @param {?Element} [options.root]
267
+ * @returns {?{host: string, documentId: string, principalId: string, scope: "host"|"path"}}
268
+ */
269
+ export function pageAuthority({ meta = null, location: where = globalThis.location, root = globalThis.document?.documentElement } = {}) {
270
+ const host = where?.origin ?? "";
271
+ if (!host) return null;
272
+ const hostIdentity = hostIdentityOf(root);
273
+ const scope = hostIdentity ? "host" : "path";
274
+ const documentId = hostIdentity?.identity ?? pathIdentityOf(where?.pathname ?? "/").identity;
275
+ if (!documentId) return null;
276
+ const people = meta?.document?.people;
277
+ const principalId = people && typeof people === "object" ? people.me?.id ?? null : documentId;
278
+ if (!principalId) return null;
279
+ return { host, documentId, principalId, scope };
280
+ }
281
+
282
+ const journalKey = (id) => (typeof id === "string" ? id : JSON.stringify(id));
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
+
302
+ /**
303
+ * The accepted page state an episode starts from, as the bytes its capsule keeps.
304
+ *
305
+ * @param {Object} state - a version 1 page value
306
+ * @returns {Uint8Array}
307
+ */
308
+ export const journalBase = (state) => canonicalPageBytes(state);
309
+
310
+ /**
311
+ * The page channel of one recovery service, registered once per page load.
312
+ *
313
+ * A channel is registered at its own version and nothing calls it until a page can
314
+ * actually journal: a reader and a page without a pinned identity register nothing.
315
+ *
316
+ * @param {Object} service - `clay.recovery`
317
+ * @returns {Object} the channel api
318
+ */
319
+ const channels = new WeakMap();
320
+ export function pageChannel(service) {
321
+ if (!service) return null;
322
+ const existing = channels.get(service);
323
+ if (existing) return existing;
324
+ const channel = service.register({
325
+ channel: PAGE_CHANNEL,
326
+ version: PAGE_CHANNEL_VERSION,
327
+ validateCheckpoint: validatePageCheckpoint,
328
+ merge: mergePageUpdates,
329
+ });
330
+ channels.set(service, channel);
331
+ return channel;
332
+ }
333
+
334
+ /**
335
+ * One session's journal in the shared recovery store.
336
+ *
337
+ * Every local update is handed over as it happens: the bytes and the id are fixed
338
+ * synchronously, so a buffer reused after the call cannot change what was written, and
339
+ * the append resolves only once its transaction committed. Nothing here waits for the
340
+ * room, the network or the editor. Compaction names only the records the store has
341
+ * confirmed, because an append still in flight or refused has no revision to swap and
342
+ * must keep exactly the bytes it was given.
343
+ *
344
+ * @param {Object} options
345
+ * @param {Object} options.channel - the `page.yjs` channel api
346
+ * @param {string} options.key - the document this episode belongs to
347
+ * @param {Function|Object} options.context - (base) => Promise<context>, or the context
348
+ * @param {?Uint8Array} [options.base] - the accepted state the episode started from
349
+ * @returns {Object}
350
+ */
351
+ export function createPageJournal({ channel, key, context, base = null, onChange = () => {} }) {
352
+ const records = new Map();
353
+ const waiting = [];
354
+ const awaiting = [];
355
+ const inflight = new Set();
356
+ let bytesOfBase = bytesOf(base) ?? new Uint8Array(0);
357
+ let resolved = null;
358
+ let failure = null;
359
+ let batches = 0;
360
+ let batchBytes = 0;
361
+ let episode = false;
362
+ let carryBase = true;
363
+ let holding = false;
364
+ let rebasing = Promise.resolve(null);
365
+ let compacting = null;
366
+ let closed = false;
367
+
368
+ const journal = {
369
+ channel,
370
+ key,
371
+ onChange,
372
+ // The context of the base the journal holds now: an accepted save moves it.
373
+ context: () => ready.then(() => resolved),
374
+ local: (bytes) => send(bytes, "local"),
375
+ // Only while an episode is open: a peer's updates on their own are not this tab's work
376
+ // to keep, and a page that has journaled nothing owns nothing.
377
+ dependency: (bytes) => { if (episode) send(bytes, "dependency"); },
378
+ records: () => [...records.values()],
379
+ names,
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),
401
+ get failure() { return failure; },
402
+ retire,
403
+ rebase,
404
+ compact,
405
+ // An acknowledgement is metadata: it says a room holds the bytes and retires nothing.
406
+ acknowledged: () => {
407
+ if (closed || !resolved || !records.size || typeof channel.markAcknowledged !== "function") return Promise.resolve(null);
408
+ return Promise.resolve(channel.markAcknowledged({ records: names(), authority: resolved.authority })).catch(() => null);
409
+ },
410
+ close: () => {
411
+ closed = true;
412
+ waiting.length = 0;
413
+ settle(null);
414
+ },
415
+ };
416
+
417
+ const ready = Promise.resolve()
418
+ .then(() => (typeof context === "function" ? context(bytesOfBase) : context))
419
+ .catch(() => null)
420
+ .then((value) => {
421
+ resolved = value && value.authority ? value : null;
422
+ if (!resolved) {
423
+ closed = true;
424
+ failure = "unavailable";
425
+ waiting.length = 0;
426
+ journal.onChange();
427
+ return null;
428
+ }
429
+ for (const run of waiting.splice(0)) run(resolved);
430
+ return resolved;
431
+ });
432
+
433
+ function settle(value) {
434
+ for (const resolve of awaiting.splice(0)) resolve(value);
435
+ }
436
+
437
+ function retire(list) {
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
+ }
488
+ }
489
+
490
+ function names() {
491
+ return [...records.values()].map(({ id, revision }) => ({ id, revision }));
492
+ }
493
+
494
+ function compact() {
495
+ if (compacting || closed || !records.size || typeof channel.compact !== "function") return compacting;
496
+ const named = names();
497
+ compacting = Promise.resolve()
498
+ .then(() => channel.compact({ records: named }))
499
+ .then((receipt) => {
500
+ if (!receipt?.ok) return receipt;
501
+ retire(receipt.retired);
502
+ return receipt;
503
+ })
504
+ .catch(() => null)
505
+ .then((receipt) => {
506
+ compacting = null;
507
+ journal.onChange();
508
+ return receipt;
509
+ });
510
+ return compacting;
511
+ }
512
+
513
+ function write(context, bytes, origin, first) {
514
+ let answer;
515
+ try {
516
+ answer = channel.append({ key, context, update: bytes, origin, base: first ? bytesOfBase : null });
517
+ } catch (_) {
518
+ failure = "error";
519
+ settle(null);
520
+ journal.onChange();
521
+ return;
522
+ }
523
+ const token = {};
524
+ inflight.add(token);
525
+ Promise.resolve(answer).then(
526
+ (receipt) => {
527
+ inflight.delete(token);
528
+ if (!receipt || receipt.ok !== true) {
529
+ failure = receipt?.code ?? "error";
530
+ settle(null);
531
+ journal.onChange();
532
+ return;
533
+ }
534
+ failure = null;
535
+ if (!receipt.skipped) {
536
+ records.set(journalKey(receipt.id), { id: receipt.id, revision: receipt.revision, update: bytes, context });
537
+ batches += 1;
538
+ batchBytes += bytes.byteLength;
539
+ }
540
+ journal.onChange();
541
+ settle({ id: receipt.id, revision: receipt.revision });
542
+ if (batches >= COMPACT_BATCHES || batchBytes >= COMPACT_BYTES) compact();
543
+ },
544
+ () => {
545
+ inflight.delete(token);
546
+ failure = "error";
547
+ settle(null);
548
+ journal.onChange();
549
+ },
550
+ );
551
+ }
552
+
553
+ function dispatch(context, bytes, origin) {
554
+ const first = carryBase;
555
+ carryBase = false;
556
+ write(context, bytes, origin, first);
557
+ }
558
+
559
+ function send(bytes, origin) {
560
+ if (closed || !bytes || !bytes.byteLength || typeof channel.append !== "function") return;
561
+ episode = true;
562
+ if (resolved && !holding) dispatch(resolved, bytes, origin);
563
+ else waiting.push((context) => dispatch(context, bytes, origin));
564
+ }
565
+
566
+ return journal;
567
+ }
568
+
569
+ /**
570
+ * This session's journal, opened from the host's own answer, or null.
571
+ *
572
+ * A host that cannot name who is writing, or a tab with no recovery service, journals
573
+ * nothing: a page that cannot pin its identity never replays across sessions.
574
+ *
575
+ * @param {Object} options
576
+ * @param {?Object} options.meta - the host's discovery answer
577
+ * @param {Object} options.service - `clay.recovery`
578
+ * @param {?Uint8Array} options.base - the accepted state captured before input
579
+ * @param {Function} [options.onChange]
580
+ * @returns {?Object}
581
+ */
582
+ export function openPageJournal({ meta = null, service = null, base = null, onChange = () => {}, location, root } = {}) {
583
+ const authority = pageAuthority({ meta, location, root });
584
+ if (!authority) return null;
585
+ const channel = pageChannel(service);
586
+ if (!channel) return null;
587
+ const generation = meta?.document?.pageState?.generation ?? null;
588
+ const formatFloor = Array.isArray(meta?.extensions) && meta.extensions.includes("page-format-v1") ? 1 : 0;
589
+ return createPageJournal({
590
+ channel,
591
+ key: [authority.host, authority.documentId].join(":"),
592
+ context: (accepted) => pageJournalContext({ authority, generation, base: accepted, formatFloor }),
593
+ base,
594
+ onChange,
595
+ });
596
+ }
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
+
682
+ /**
683
+ * Replay this page's journal once, after the authority snapshot is adopted.
684
+ *
685
+ * @param {{journal: Object, state: Object, into: Object}} options
686
+ * @returns {Promise<{replayed: Array<Object>, review: Array<Object>}>}
687
+ */
688
+ export async function replayInto({ journal, state, into } = {}) {
689
+ if (!journal) return { replayed: [], review: [] };
690
+ const context = await journal.context();
691
+ if (!context) return { replayed: [], review: [] };
692
+ const listed = await journal.channel.list();
693
+ if (!listed?.ok) return { replayed: [], review: [] };
694
+ return replayJournals({ records: listed.records, current: { context, state }, into });
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
+ }