@panphora/clayjs 1.15.2 → 1.17.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 (551) hide show
  1. package/README.md +4 -5
  2. package/THIRD-PARTY-NOTICES.md +16 -0
  3. package/compat/highlight-11.9.0.min.js +1213 -0
  4. package/compat/highlight-LICENSE +29 -0
  5. package/compat/journal-assets.lock.json +22 -0
  6. package/compat/overtype-2.6.1.min.js +116 -0
  7. package/compat/overtype-LICENSE +21 -0
  8. package/dist/all.js +90 -0
  9. package/dist/clay-dom.js +84 -0
  10. package/dist/clay-events.js +103 -0
  11. package/dist/clay-internals.js +85 -0
  12. package/dist/clay-options.js +93 -0
  13. package/dist/clay-router.js +98 -0
  14. package/dist/clay-ui.js +95 -0
  15. package/dist/clay-utils.js +84 -0
  16. package/dist/clay.js +146 -0
  17. package/dist/clay.standalone.js +84291 -57611
  18. package/dist/m/attrs/add-from-T5LJXFFI.js +1 -0
  19. package/dist/m/attrs/onaftersave-OKSWEJQF.js +1 -0
  20. package/dist/m/attrs/refetch-on-save-ZUE7PSCI.js +1 -0
  21. package/dist/m/attrs/save-freeze-MYNVXREN.js +1 -0
  22. package/dist/m/c-2AZDZWCK.js +1 -0
  23. package/dist/m/c-2SPVQSHC.js +1 -0
  24. package/dist/m/c-2YYDMLNZ.js +1 -0
  25. package/dist/m/c-336SGU5W.js +1 -0
  26. package/dist/m/c-33JJ6CBX.js +2 -0
  27. package/dist/m/c-3GJ4SDE2.js +1 -0
  28. package/dist/m/c-3H6BZDNJ.js +6 -0
  29. package/dist/m/c-3OWDMNJR.js +2 -0
  30. package/dist/m/c-3RUU4TPX.js +91 -0
  31. package/dist/m/c-3XIRD4NU.js +1 -0
  32. package/dist/m/c-46XYM55L.js +1 -0
  33. package/dist/m/c-4DFEJ2PO.js +3 -0
  34. package/dist/m/c-4JZDZFF6.js +1 -0
  35. package/dist/m/c-4L3BTFRK.js +1 -0
  36. package/dist/m/c-4Q2XTQ2N.js +2 -0
  37. package/dist/m/c-4WKFKEKF.js +1 -0
  38. package/dist/m/c-56QRZTZH.js +1 -0
  39. package/dist/m/c-5J4E7O4Y.js +4 -0
  40. package/dist/m/c-5LKDWQTL.js +19 -0
  41. package/dist/m/c-5QWQ7A5L.js +1 -0
  42. package/dist/m/c-5RJFTLSF.js +1 -0
  43. package/dist/m/c-5V3MOVJT.js +1 -0
  44. package/dist/m/c-5VYOWDTI.js +1 -0
  45. package/dist/m/c-5YIHJ3TJ.js +1 -0
  46. package/dist/m/c-6AZOGAPE.js +1 -0
  47. package/dist/m/c-6CRUUXOX.js +5 -0
  48. package/dist/m/c-6HMB24EV.js +1 -0
  49. package/dist/m/c-6KEVEQ7L.js +1 -0
  50. package/dist/m/c-6NC5I2TV.js +1 -0
  51. package/dist/m/c-74MGYWZF.js +1 -0
  52. package/dist/m/c-7DJDKPGL.js +1 -0
  53. package/dist/m/c-7OLLJPWP.js +1 -0
  54. package/dist/m/c-7SWMYKAI.js +1 -0
  55. package/dist/m/c-A3E3NIMC.js +1 -0
  56. package/dist/m/c-A7LS5UZW.js +1 -0
  57. package/dist/m/c-AMSNPOCX.js +1 -0
  58. package/dist/m/c-AQLFEATB.js +1 -0
  59. package/dist/m/c-B3JM5ZXC.js +1 -0
  60. package/dist/m/c-BBVGTQG7.js +1 -0
  61. package/dist/m/c-BPF2NSPV.js +1 -0
  62. package/dist/m/c-BQ6LOBEE.js +1 -0
  63. package/dist/m/c-BTDI4Y23.js +1 -0
  64. package/dist/m/c-BWFVSBDF.js +1 -0
  65. package/dist/m/c-BYTKL6UX.js +1 -0
  66. package/dist/m/c-CFHW5AMY.js +6 -0
  67. package/dist/m/c-CJ3A7ZKE.js +1 -0
  68. package/dist/m/c-CQHPGKX2.js +1 -0
  69. package/dist/m/c-CYWLPPUS.js +1 -0
  70. package/dist/m/c-CZUSOB7V.js +1 -0
  71. package/dist/m/c-DCFSEJZG.js +1 -0
  72. package/dist/m/c-DCXOXBF3.js +1 -0
  73. package/dist/m/c-DFS4XZJV.js +2 -0
  74. package/dist/m/c-DNENWY4O.js +1 -0
  75. package/dist/m/c-DNZ5IFM6.js +1 -0
  76. package/dist/m/c-E4LIR5X5.js +1 -0
  77. package/dist/m/c-EJTGYSLN.js +1 -0
  78. package/dist/m/c-EVCQ3BKA.js +1 -0
  79. package/dist/m/c-EZU56NQV.js +34 -0
  80. package/dist/m/c-FDNHHZY5.js +1 -0
  81. package/dist/m/c-FILLTJA7.js +1 -0
  82. package/dist/m/c-FRLHSY7E.js +1 -0
  83. package/dist/m/c-FROJUZMV.js +1 -0
  84. package/dist/m/c-G2JPKCTP.js +1 -0
  85. package/dist/m/c-GB6TU7WV.js +1 -0
  86. package/dist/m/c-GFJJEBTU.js +1 -0
  87. package/dist/m/c-GJAEGGXY.js +1 -0
  88. package/dist/m/c-GOWBDM6J.js +1 -0
  89. package/dist/m/c-GQ6F6IDK.js +1 -0
  90. package/dist/m/c-GSFIGKOP.js +1 -0
  91. package/dist/m/c-GY422N3Q.js +1 -0
  92. package/dist/m/c-H4LG442Q.js +1 -0
  93. package/dist/m/c-HA6INT6J.js +1 -0
  94. package/dist/m/c-HAWFNBYG.js +1 -0
  95. package/dist/m/c-HDDEEL77.js +1 -0
  96. package/dist/m/c-HFKUVACH.js +1 -0
  97. package/dist/m/c-HHDFCNHV.js +1 -0
  98. package/dist/m/c-HPURYVZS.js +2 -0
  99. package/dist/m/c-I2ZATQCD.js +2 -0
  100. package/dist/m/c-IEM4NTCS.js +1 -0
  101. package/dist/m/c-IJUK2GPW.js +1 -0
  102. package/dist/m/c-IW6GGOU2.js +1 -0
  103. package/dist/m/c-JAX2667S.js +6 -0
  104. package/dist/m/c-JDCX26UP.js +25 -0
  105. package/dist/m/c-JRKY44DK.js +1 -0
  106. package/dist/m/c-JU3NK25H.js +1 -0
  107. package/dist/m/c-KAU6E43D.js +1 -0
  108. package/dist/m/c-KBS3UV6B.js +1 -0
  109. package/dist/m/c-KGVGROZT.js +1 -0
  110. package/dist/m/c-KI7M4AY3.js +1 -0
  111. package/dist/m/c-KIPQJX4B.js +1 -0
  112. package/dist/m/c-KQ6TYJAP.js +135 -0
  113. package/dist/m/c-KU3ZFADW.js +1 -0
  114. package/dist/m/c-L4WQSC27.js +1 -0
  115. package/dist/m/c-LFPHCXY3.js +1 -0
  116. package/dist/m/c-LRPUO2MX.js +1 -0
  117. package/dist/m/c-LWLBHLVC.js +3 -0
  118. package/dist/m/c-LZJPNR2W.js +1 -0
  119. package/dist/m/c-M2L47S2X.js +7 -0
  120. package/dist/m/c-MFP2ZWD4.js +1 -0
  121. package/dist/m/c-MKSQ4Y57.js +1 -0
  122. package/dist/m/c-MTQHWQC3.js +1 -0
  123. package/dist/m/c-N326R2JD.js +1 -0
  124. package/dist/m/c-N7JPUPGC.js +1 -0
  125. package/dist/m/c-NRPJEVJS.js +1 -0
  126. package/dist/m/c-NUFFJVVJ.js +1 -0
  127. package/dist/m/c-NUHP2XDB.js +1 -0
  128. package/dist/m/c-NUHSPT5P.js +1 -0
  129. package/dist/m/c-O72HVYSA.js +1 -0
  130. package/dist/m/c-OCSZ4QGN.js +1 -0
  131. package/dist/m/c-OIBUTDXI.js +1 -0
  132. package/dist/m/c-OIDRZKAF.js +1 -0
  133. package/dist/m/c-OW5QIGG3.js +1 -0
  134. package/dist/m/c-P7KNDG34.js +1 -0
  135. package/dist/m/c-PGRCK73F.js +1 -0
  136. package/dist/m/c-PQ5LK6QE.js +993 -0
  137. package/dist/m/c-PS5ARVWR.js +9 -0
  138. package/dist/m/c-PZ4XEPUP.js +2 -0
  139. package/dist/m/c-Q622NS2Y.js +1 -0
  140. package/dist/m/c-Q7NQD65V.js +1 -0
  141. package/dist/m/c-QH63LQOS.js +1 -0
  142. package/dist/m/c-QHIWXHRO.js +1 -0
  143. package/dist/m/c-QHWJ5ZIJ.js +1 -0
  144. package/dist/m/c-QICP2YR5.js +1 -0
  145. package/dist/m/c-QJ4OM3G6.js +1 -0
  146. package/dist/m/c-QNKREOL6.js +1 -0
  147. package/dist/m/c-R3L7ZJGB.js +2 -0
  148. package/dist/m/c-R7IWVINQ.js +1 -0
  149. package/dist/m/c-RM7J7BV2.js +1 -0
  150. package/dist/m/c-RSMOODDU.js +1 -0
  151. package/dist/m/c-RVMWGXEJ.js +1 -0
  152. package/dist/m/c-RYUXUUBX.js +1 -0
  153. package/dist/m/c-S4ZEH46B.js +9 -0
  154. package/dist/m/c-S7H2DRCB.js +1 -0
  155. package/dist/m/c-S7LK4KUE.js +1 -0
  156. package/dist/m/c-SDAXDM2D.js +1 -0
  157. package/dist/m/c-SGIA4IEQ.js +188 -0
  158. package/dist/m/c-SMBZG5X4.js +1 -0
  159. package/dist/m/c-SMGU7RT3.js +235 -0
  160. package/dist/m/c-SPIORRYS.js +1 -0
  161. package/dist/m/c-SWLRT66R.js +1 -0
  162. package/dist/m/c-SZGYW5HT.js +1 -0
  163. package/dist/m/c-TCCA5BZN.js +2 -0
  164. package/dist/m/c-TCJTVAFN.js +1 -0
  165. package/dist/m/c-TJ6WOJL3.js +67 -0
  166. package/dist/m/c-TQEMVJCS.js +1 -0
  167. package/dist/m/c-TRS6TM6O.js +1 -0
  168. package/dist/m/c-TYLXX4NF.js +21 -0
  169. package/dist/m/c-U62OB4BT.js +1 -0
  170. package/dist/m/c-U6YWUAR3.js +8 -0
  171. package/dist/m/c-UDCSU5G5.js +2 -0
  172. package/dist/m/c-UDUUJPFZ.js +1 -0
  173. package/dist/m/c-UGHH4MB3.js +1 -0
  174. package/dist/m/c-UL2YUY52.js +1 -0
  175. package/dist/m/c-VAK7SG37.js +1 -0
  176. package/dist/m/c-VDSS4K3Z.js +1 -0
  177. package/dist/m/c-VE6ZMYMC.js +1 -0
  178. package/dist/m/c-VG5XIN4T.js +1 -0
  179. package/dist/m/c-VHM5XQHK.js +1 -0
  180. package/dist/m/c-VHNSBD2M.js +1 -0
  181. package/dist/m/c-VI5RMSVY.js +1 -0
  182. package/dist/m/c-VKEDWYDF.js +1 -0
  183. package/dist/m/c-VRZN56XY.js +1 -0
  184. package/dist/m/c-W5MCHEGA.js +1 -0
  185. package/dist/m/c-WDC5GVKU.js +1 -0
  186. package/dist/m/c-WN5AY3OZ.js +1 -0
  187. package/dist/m/c-WNYBLIOZ.js +1 -0
  188. package/dist/m/c-WRBKEJD4.js +1 -0
  189. package/dist/m/c-WWXIDH4O.js +28 -0
  190. package/dist/m/c-WYFDLITL.js +1 -0
  191. package/dist/m/c-XCNTK76U.js +1 -0
  192. package/dist/m/c-XMS6DPBR.js +1 -0
  193. package/dist/m/c-XN6MBKI6.js +67 -0
  194. package/dist/m/c-XPELB6E5.js +1 -0
  195. package/dist/m/c-XQXZAO7N.js +1 -0
  196. package/dist/m/c-Y5HBBIQ4.js +1 -0
  197. package/dist/m/c-ZE46QDIH.js +1 -0
  198. package/dist/m/c-ZUCOKOHT.js +1 -0
  199. package/dist/m/c-ZVPVXTJF.js +4 -0
  200. package/dist/m/c-ZVRKXKM7.js +1 -0
  201. package/dist/m/c-ZWIOJGMT.js +1 -0
  202. package/dist/m/core/admin-attrs-QHOKPJ7W.js +1 -0
  203. package/dist/m/core/autosave-HRYGUWV6.js +1 -0
  204. package/dist/m/core/conflict-notice-6MROJN2L.js +1 -0
  205. package/dist/m/core/draft-mode-WSQAT67S.js +1 -0
  206. package/dist/m/core/host-permission-FNXVEFNE.js +1 -0
  207. package/dist/m/core/is-edit-mode-KTWA3O5N.js +1 -0
  208. package/dist/m/core/persist-JFYRHMRJ.js +3 -0
  209. package/dist/m/core/recovery-FLTCC2OC.js +1 -0
  210. package/dist/m/core/save-HQZ4S667.js +1 -0
  211. package/dist/m/core/save-core-KGQWFUNH.js +1 -0
  212. package/dist/m/core/snapshot-EGQZNT45.js +1 -0
  213. package/dist/m/core/unsaved-warning-IDJLD66H.js +1 -0
  214. package/dist/m/dom/all-APS5ZSKP.js +1 -0
  215. package/dist/m/dom/dom-helpers-L7OSR6S4.js +1 -0
  216. package/dist/m/events/index-RN2JNGBV.js +1 -0
  217. package/dist/m/internals/index-PVXGPUDA.js +1 -0
  218. package/dist/m/lib/cache-bust-MUMFXZK6.js +1 -0
  219. package/dist/m/lib/region-policy-SB2AWVG5.js +1 -0
  220. package/dist/m/loader-S4I7GKXR.js +1 -0
  221. package/dist/m/manifest-4C419D67.js +1 -0
  222. package/dist/m/manifest-edit-B077F288.js +1 -0
  223. package/dist/m/manifest.json +3988 -0
  224. package/dist/m/options/options-EJMS3K24.js +1 -0
  225. package/dist/m/page/editor-NNJA4OKD.js +202 -0
  226. package/dist/m/plugins/ai-edit-QN4PMMNL.js +6 -0
  227. package/dist/m/plugins/bind-text-NX46N5UV.js +1 -0
  228. package/dist/m/plugins/cms-HP3LSTPN.js +2293 -0
  229. package/dist/m/plugins/demo-RM5IR6AO.js +1 -0
  230. package/dist/m/plugins/derive-UAAA4V4S.js +1 -0
  231. package/dist/m/plugins/indicator-URDBBOQO.js +1 -0
  232. package/dist/m/plugins/live-HFWD4OIH.js +1 -0
  233. package/dist/m/plugins/menu-3HIEQSI6.js +138 -0
  234. package/dist/m/plugins/message-LQ6EDH4V.js +1 -0
  235. package/dist/m/plugins/page-JOKB6XPR.js +2 -0
  236. package/dist/m/plugins/page-view-NXOJHNW6.js +1 -0
  237. package/dist/m/plugins/people-SGPJKANK.js +1 -0
  238. package/dist/m/plugins/quickcrop-QNY3PKLD.js +26 -0
  239. package/dist/m/plugins/quickcrop-QNY3PKLD.js.LEGAL.txt +7 -0
  240. package/dist/m/plugins/richclay-NRQDSWX6.js +517 -0
  241. package/dist/m/plugins/richclay-NRQDSWX6.js.LEGAL.txt +1 -0
  242. package/dist/m/plugins/sortable-GWT4PWDV.js +1 -0
  243. package/dist/m/plugins/source-4ZRSAIYM.js +1 -0
  244. package/dist/m/plugins/undo-VDA5P7FH.js +1 -0
  245. package/dist/m/plugins/unsent-HJFLQJ2O.js +1 -0
  246. package/dist/m/plugins/upgrade-W7FG5VFG.js +1 -0
  247. package/dist/m/plugins/upload-WLYKK6TL.js +1 -0
  248. package/dist/m/plugins/visitor-drafts-LIDY7NH6.js +21 -0
  249. package/dist/m/plugins/visitor-drafts-owner-ZXCKBZE7.js +1 -0
  250. package/dist/m/plugins/wire-4N6GYAF6.js +1 -0
  251. package/dist/m/reader-core-DXYCURFF.js +68 -0
  252. package/dist/m/route/route-KOCZSEGN.js +1 -0
  253. package/dist/m/sync/live-sync-J7VMYTKS.js +1 -0
  254. package/dist/m/ui/index-3JLVI27O.js +1 -0
  255. package/dist/m/utils/index-4TJCVLT7.js +1 -0
  256. package/dist/m/vendor/Sortable.vendor-A755ITLN.js +3 -0
  257. package/dist/m/vendor/Sortable.vendor-A755ITLN.js.LEGAL.txt +1 -0
  258. package/dist/m/vendor/sap-core.vendor-A7BMUYZT.js +6 -0
  259. package/dist/offline-manifest.json +4034 -0
  260. package/dist/page/themes/light-1.css +430 -0
  261. package/dist/server/core.cjs +23748 -22855
  262. package/dist/server/offline-runtime.cjs +4954 -0
  263. package/dist/server/offline-runtime.mjs +4923 -0
  264. package/dist/server/rooms.mjs +9 -0
  265. package/docs/guide.md +1078 -669
  266. package/entries/all.js +2 -0
  267. package/entries/clay-data.js +1 -1
  268. package/entries/clay-dom.js +2 -0
  269. package/entries/clay-events.js +2 -0
  270. package/entries/clay-internals.js +2 -0
  271. package/entries/clay-options.js +2 -0
  272. package/entries/clay-router.js +2 -0
  273. package/entries/clay-ui.js +2 -0
  274. package/entries/clay-utils.js +2 -0
  275. package/entries/clay.js +2 -0
  276. package/entries/sap.js +6 -6
  277. package/fonts/ibm-plex-mono-LICENSE +93 -0
  278. package/fonts/ibm-plex-mono-latin-300-normal.woff2 +0 -0
  279. package/fonts/ibm-plex-mono-latin-400-italic.woff2 +0 -0
  280. package/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  281. package/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  282. package/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
  283. package/fonts/ibm-plex-sans-LICENSE +93 -0
  284. package/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
  285. package/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
  286. package/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
  287. package/fonts/ibm-plex.css +71 -0
  288. package/fonts/inter-LICENSE +93 -0
  289. package/fonts/inter-cyrillic-ext-wght-normal.woff2 +0 -0
  290. package/fonts/inter-cyrillic-wght-normal.woff2 +0 -0
  291. package/fonts/inter-greek-ext-wght-normal.woff2 +0 -0
  292. package/fonts/inter-greek-wght-normal.woff2 +0 -0
  293. package/fonts/inter-latin-ext-wght-normal.woff2 +0 -0
  294. package/fonts/inter-latin-wght-normal.woff2 +0 -0
  295. package/fonts/inter-vietnamese-wght-normal.woff2 +0 -0
  296. package/fonts/inter.css +62 -0
  297. package/fonts/newsreader-LICENSE +93 -0
  298. package/fonts/newsreader-latin-ext-opsz-normal.woff2 +0 -0
  299. package/fonts/newsreader-latin-opsz-normal.woff2 +0 -0
  300. package/fonts/newsreader-vietnamese-opsz-normal.woff2 +0 -0
  301. package/fonts/newsreader.css +26 -0
  302. package/fonts/work-sans-LICENSE +93 -0
  303. package/fonts/work-sans-latin-wght-normal.woff2 +0 -0
  304. package/fonts/work-sans.css +8 -0
  305. package/package.json +20 -3
  306. package/packed-contract.json +1 -0
  307. package/src/attrs/add-from.js +27 -4
  308. package/src/attrs/save-freeze.js +7 -52
  309. package/src/core/admin-attrs.js +119 -12
  310. package/src/core/admin-contenteditable.js +12 -10
  311. package/src/core/admin-inputs.js +14 -15
  312. package/src/core/admin-onclick.js +2 -0
  313. package/src/core/autosave.js +74 -21
  314. package/src/core/boot-phase.js +2 -1
  315. package/src/core/conflict-download.js +19 -23
  316. package/src/core/conflict-notice-ui.js +1173 -0
  317. package/src/core/conflict-notice.js +140 -697
  318. package/src/core/download.js +32 -0
  319. package/src/core/edit-mode.js +147 -14
  320. package/src/core/etag.js +30 -12
  321. package/src/core/host-meta.js +90 -8
  322. package/src/core/host-permission.js +29 -0
  323. package/src/core/is-edit-mode.js +145 -12
  324. package/src/core/mode-key.js +125 -0
  325. package/src/core/mode-lifecycle.js +544 -0
  326. package/src/core/mode-receipt.js +30 -0
  327. package/src/core/mode-refusal.js +13 -0
  328. package/src/core/mode-resources.js +52 -0
  329. package/src/core/mode-support.js +82 -0
  330. package/src/core/mode-switch.js +259 -0
  331. package/src/core/mode-transition.js +112 -0
  332. package/src/core/page-data-write.js +305 -0
  333. package/src/core/page-data.js +26 -227
  334. package/src/core/page-format-wire.js +4 -0
  335. package/src/core/page-format.js +58 -0
  336. package/src/core/persist.js +37 -9
  337. package/src/core/preserve-authored.js +114 -0
  338. package/src/core/recovery.js +772 -0
  339. package/src/core/runtime-url.js +105 -0
  340. package/src/core/save-core.js +113 -12
  341. package/src/core/save-replace.js +72 -0
  342. package/src/core/save-size-notice.js +134 -0
  343. package/src/core/save-size.js +140 -0
  344. package/src/core/save.js +806 -119
  345. package/src/core/snapshot.js +159 -11
  346. package/src/core/source-map.js +157 -9
  347. package/src/core/unsaved-warning.js +14 -4
  348. package/src/core/upgrade-run.js +408 -0
  349. package/src/core/version.js +18 -0
  350. package/src/lib/assets.js +30 -0
  351. package/src/lib/autosave-debug-gate.js +50 -0
  352. package/src/lib/autosave-scheduler.js +140 -0
  353. package/src/lib/boot-imports.js +8 -0
  354. package/src/lib/derived.js +3 -1
  355. package/src/lib/dirty-gate.js +15 -0
  356. package/src/lib/doc-storage.js +33 -0
  357. package/src/lib/effect.js +9 -4
  358. package/src/lib/mutation.js +4 -2
  359. package/src/lib/offline-runtime.js +226 -0
  360. package/src/lib/preload.js +48 -0
  361. package/src/lib/root-attrs.js +1 -1
  362. package/src/lib/trash.js +18 -0
  363. package/src/lib/undo/capture-move.js +152 -0
  364. package/src/live/local-channel.js +61 -0
  365. package/src/live/local-lane.js +60 -9
  366. package/src/live/server-lane.js +6 -3
  367. package/src/loader-logic.js +76 -15
  368. package/src/loader.js +218 -30
  369. package/src/page/adopt-boot.js +76 -22
  370. package/src/page/adopt.js +25 -4
  371. package/src/page/awareness.js +230 -0
  372. package/src/page/blocks.js +113 -6
  373. package/src/page/blocksel.js +20 -4
  374. package/src/page/board-face.js +13 -0
  375. package/src/page/board-panel.js +5 -17
  376. package/src/page/board-style.js +2 -13
  377. package/src/page/board-view.js +1 -11
  378. package/src/page/board-views.js +3 -0
  379. package/src/page/board.js +2 -2
  380. package/src/page/boot-pack.js +26 -0
  381. package/src/page/boot.js +237 -0
  382. package/src/page/chart-tables.js +11 -0
  383. package/src/page/chart.js +6 -10
  384. package/src/page/checklist.js +1 -1
  385. package/src/page/checkpoint-proof.js +289 -0
  386. package/src/page/chrome.js +431 -63
  387. package/src/page/csv-import.js +17 -2
  388. package/src/page/document-title-dom.js +52 -0
  389. package/src/page/document-title-runtime.js +90 -0
  390. package/src/page/document-title.js +12 -0
  391. package/src/page/drag.js +48 -13
  392. package/src/page/drop.js +2 -1
  393. package/src/page/editor.js +399 -90
  394. package/src/page/emoji-data.js +162 -0
  395. package/src/page/feature-loader.js +72 -0
  396. package/src/page/features/board.js +21 -0
  397. package/src/page/features/chart.js +11 -0
  398. package/src/page/features/code.js +13 -0
  399. package/src/page/features/dates.js +180 -0
  400. package/src/page/features/emoji.js +187 -0
  401. package/src/page/features/media.js +290 -0
  402. package/src/page/features/mentions.js +188 -0
  403. package/src/page/features/registry.js +28 -0
  404. package/src/page/features/table.js +383 -0
  405. package/src/page/{tool-view.js → features/tool.js} +33 -2
  406. package/src/page/format-codec.js +215 -0
  407. package/src/page/format-command.js +9 -0
  408. package/src/page/format-gate.js +179 -0
  409. package/src/page/format-host.js +54 -0
  410. package/src/page/format-level.js +157 -0
  411. package/src/page/format-native-commands.js +142 -0
  412. package/src/page/format-native.js +42 -0
  413. package/src/page/highlight-slot.js +23 -0
  414. package/src/page/highlight.js +30 -16
  415. package/src/page/history-intent.js +78 -0
  416. package/src/page/inline.js +45 -28
  417. package/src/page/items.js +67 -14
  418. package/src/page/link.js +1 -0
  419. package/src/page/markdown.js +112 -3
  420. package/src/page/media-views.js +3 -0
  421. package/src/page/media.js +33 -245
  422. package/src/page/mention-picker.js +99 -0
  423. package/src/page/merge.js +1 -1
  424. package/src/page/outline-cache.js +11 -0
  425. package/src/page/placeholder.js +27 -7
  426. package/src/page/projection.js +240 -51
  427. package/src/page/reader-patch.js +306 -0
  428. package/src/page/recover.js +1 -1
  429. package/src/page/rich/page-tool.js +1 -4
  430. package/src/page/runtime-owner.js +27 -0
  431. package/src/page/save-projection.js +46 -4
  432. package/src/page/save-states.js +1 -1
  433. package/src/page/schema.js +47 -0
  434. package/src/page/table-rect-key.js +3 -0
  435. package/src/page/table-select.js +4 -3
  436. package/src/page/table-style.js +1 -1
  437. package/src/page/table-views.js +3 -0
  438. package/src/page/table.js +8 -293
  439. package/src/page/themes/content.css +25 -1
  440. package/src/page/themes/light-1.css +163 -2
  441. package/src/page/themes/light-1.source.css +269 -0
  442. package/src/page/tool-capability.js +3 -0
  443. package/src/page/tool-views.js +3 -0
  444. package/src/page/view-hooks.js +32 -0
  445. package/src/page/view.js +390 -107
  446. package/src/page/ybind/awareness.js +172 -0
  447. package/src/page/ybind/binding.js +40 -5
  448. package/src/page/ybind/bridge.js +50 -47
  449. package/src/page/ybind/decode.js +52 -19
  450. package/src/page/ybind/doc.js +27 -2
  451. package/src/page/ybind/group.js +12 -3
  452. package/src/page/ybind/index.js +2 -0
  453. package/src/page/ybind/lineage.js +71 -0
  454. package/src/page/ybind/marks.js +26 -1
  455. package/src/page/ybind/migrate.js +48 -13
  456. package/src/page/ybind/moves.js +32 -21
  457. package/src/page/ybind/positions.js +50 -0
  458. package/src/page/ybind/provider.js +97 -24
  459. package/src/page/ybind/recovery-proof.js +191 -0
  460. package/src/page/ybind/recovery-replay.js +488 -0
  461. package/src/page/ybind/tokens.js +15 -6
  462. package/src/page/ybind/walk.js +10 -3
  463. package/src/page/ybind/writer.js +48 -4
  464. package/src/plugins/ai-edit.js +167 -32
  465. package/src/plugins/bind-text.js +218 -0
  466. package/src/plugins/cms.js +93 -0
  467. package/src/plugins/indicator.js +57 -5
  468. package/src/plugins/live.js +319 -35
  469. package/src/plugins/menu/claygoo.svg.js +1 -0
  470. package/src/plugins/menu/seo-head.js +323 -0
  471. package/src/plugins/menu/seo.js +849 -0
  472. package/src/plugins/menu.js +425 -0
  473. package/src/plugins/page-view.js +131 -0
  474. package/src/plugins/page.js +1131 -270
  475. package/src/plugins/people.js +158 -31
  476. package/src/plugins/richclay.js +119 -9
  477. package/src/plugins/sortable.js +370 -60
  478. package/src/plugins/source.js +189 -366
  479. package/src/plugins/undo.js +289 -19
  480. package/src/plugins/unsent.js +132 -0
  481. package/src/plugins/upgrade-run.js +1 -0
  482. package/src/plugins/upgrade.js +210 -0
  483. package/src/plugins/upload-task.js +571 -0
  484. package/src/plugins/upload-ui.js +265 -0
  485. package/src/plugins/upload.js +34 -153
  486. package/src/plugins/visitor-drafts/inert.js +32 -0
  487. package/src/plugins/visitor-drafts/intro.js +2 -1
  488. package/src/plugins/visitor-drafts/owner-chip.js +3 -0
  489. package/src/plugins/visitor-drafts/prompt.js +2 -2
  490. package/src/plugins/visitor-drafts-owner.js +85 -0
  491. package/src/plugins/visitor-drafts.js +31 -42
  492. package/src/plugins/wire.js +82 -1
  493. package/src/reader-core.js +26 -0
  494. package/src/recovery/conflicts.js +287 -0
  495. package/src/recovery/identity.js +154 -0
  496. package/src/recovery/store.js +792 -0
  497. package/src/recovery/undo.js +257 -0
  498. package/src/server/checkpoint/index.js +68 -4
  499. package/src/server/offline-runtime/index.js +214 -0
  500. package/src/server/rooms/index.js +2 -1
  501. package/src/server/rooms/live-data-rooms.js +2 -2
  502. package/src/server/rooms/page-admission.js +33 -0
  503. package/src/server/rooms/page-lineage.js +56 -0
  504. package/src/server/rooms/page-rebase.js +11 -5
  505. package/src/server/rooms/page-state.js +2 -0
  506. package/src/server/rooms/page-write-admission.js +9 -3
  507. package/src/source/session.js +705 -0
  508. package/src/sync/conflict-revert.js +5 -4
  509. package/src/sync/conflicts-edit.js +119 -0
  510. package/src/sync/conflicts-facade.js +49 -0
  511. package/src/sync/conflicts.js +99 -1
  512. package/src/sync/editing-marker.js +37 -6
  513. package/src/sync/editing-sender.js +8 -1
  514. package/src/sync/live-sync-edit.js +63 -0
  515. package/src/sync/live-sync.js +364 -119
  516. package/src/sync/local-presence.js +187 -0
  517. package/src/sync/merge-declarations.js +10 -0
  518. package/src/sync/merge-engine.js +12 -0
  519. package/src/sync/presence-store.js +412 -0
  520. package/src/sync/presence-target.js +243 -0
  521. package/src/sync/presence.js +72 -41
  522. package/src/sync/saved-handoff.js +200 -0
  523. package/src/sync/saved-stream.js +544 -0
  524. package/src/sync/version-guard.js +92 -0
  525. package/src/sync/wire-profile.js +81 -0
  526. package/src/ui/bevel-controls.js +2 -8
  527. package/src/ui/page-scheme.js +7 -0
  528. package/src/ui/skins/cms.js +1 -1
  529. package/src/ui/theme-config.js +150 -0
  530. package/src/ui/theme.js +6 -138
  531. package/src/ui/toast-lazy.js +11 -0
  532. package/src/vendor/LICENSES-optional.md +253 -0
  533. package/src/vendor/highlight.vendor.js +3 -0
  534. package/src/vendor/hljs-bash.vendor.js +2 -0
  535. package/src/vendor/hljs-css.vendor.js +2 -0
  536. package/src/vendor/hljs-javascript.vendor.js +2 -0
  537. package/src/vendor/hljs-json.vendor.js +2 -0
  538. package/src/vendor/hljs-python.vendor.js +2 -0
  539. package/src/vendor/hljs-xml.vendor.js +2 -0
  540. package/src/vendor/hyper-html-api-data.vendor.js +1 -1
  541. package/src/vendor/hyper-html-api-upgrade.vendor.js +5 -0
  542. package/src/vendor/hyper-morph-identity.vendor.js +2 -0
  543. package/src/vendor/hyper-morph-json.vendor.js +3 -0
  544. package/src/vendor/hyper-morph.vendor.js +6 -6
  545. package/src/vendor/hyper-undo.vendor.js +1 -1
  546. package/src/vendor/hypercms.vendor.js +169 -84
  547. package/src/vendor/marked.vendor.js +68 -0
  548. package/src/vendor/overtype.vendor.js +994 -0
  549. package/src/vendor/richclay.vendor.js +17 -17
  550. package/src/vendor/sap-core.vendor.js +5 -5
  551. package/src/vendor/sap-ownership.vendor.js +11 -7
@@ -0,0 +1,191 @@
1
+ /**
2
+ * recovery-proof.js — what a checkpoint covers, and what makes a proof a proof.
3
+ *
4
+ * A checkpoint covers a journaled record only when merging that record's owned bytes
5
+ * into the accepted page state changes neither an insertion nor a deletion. A state
6
+ * vector cannot answer that: inserting `abc` and then deleting `b` leaves the state
7
+ * vector where it was while the bytes carry the deletion. So coverage here is a byte
8
+ * comparison, it is sufficient but not necessary, and malformed input is an error that
9
+ * keeps the record rather than retiring it.
10
+ *
11
+ * A proof is made by the save integration from the capture it saved under and the bytes
12
+ * it read back. No room acknowledgement, `requestCheckpoint()` success, state vector or
13
+ * ETag order becomes a proof, and nothing here asks a host anything: the accepted page
14
+ * state and the receipt arrive in the proof itself.
15
+ */
16
+
17
+ import { encodeStateAsUpdateV2, mergeUpdatesV2 } from "../../vendor/yjs.vendor.js";
18
+ import { docFromValue } from "./provider.js";
19
+
20
+ export class PageProofError extends Error {
21
+ constructor(code, message) {
22
+ super(message);
23
+ this.name = "PageProofError";
24
+ this.code = code;
25
+ }
26
+ }
27
+
28
+ const fail = (code, message) => { throw new PageProofError(code, message); };
29
+ const HEX = /^[0-9a-f]{64}$/;
30
+ const encoder = typeof TextEncoder === "function" ? new TextEncoder() : null;
31
+ const bytesOf = (value) => {
32
+ if (value instanceof Uint8Array) return value;
33
+ if (value instanceof ArrayBuffer) return new Uint8Array(value);
34
+ return null;
35
+ };
36
+ const identityKey = (id) => (typeof id === "string" ? id : JSON.stringify(id));
37
+ const copyId = (id) => (Array.isArray(id) ? [...id] : id);
38
+ const copyState = (state) => ({ v: state.v, g: state.g, y: { ...(state.y ?? {}) } });
39
+
40
+ export const equalBytes = (a, b) => a.length === b.length && a.every((value, index) => value === b[index]);
41
+
42
+ /**
43
+ * Whether merging `update` into `checkpoint` adds nothing: no insertion, no deletion.
44
+ *
45
+ * @param {Uint8Array} checkpoint - canonical or merged Yjs V2 update bytes
46
+ * @param {Uint8Array} update - the bytes to look for
47
+ * @param {Object} [Yjs] - the vendored Yjs module
48
+ * @returns {boolean}
49
+ */
50
+ export function containsUpdate(checkpoint, update, Yjs = null) {
51
+ const merge = Yjs?.mergeUpdatesV2 ?? mergeUpdatesV2;
52
+ const canonical = merge([checkpoint]);
53
+ return equalBytes(canonical, merge([canonical, update]));
54
+ }
55
+
56
+ export async function digestHex(bytes) {
57
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
58
+ return [...new Uint8Array(digest)].map((value) => value.toString(16).padStart(2, "0")).join("");
59
+ }
60
+
61
+ /**
62
+ * The accepted page state as one canonical update, decoded strictly.
63
+ *
64
+ * Pending structs, missing part and a delete set naming an unknown item all refuse:
65
+ * a checkpoint nobody can decode proves nothing about a journaled record.
66
+ *
67
+ * @param {Object} state - a version 1 page value
68
+ * @returns {Uint8Array}
69
+ */
70
+ export function canonicalPageBytes(state) {
71
+ let doc;
72
+ try {
73
+ doc = docFromValue(state, { strict: true });
74
+ } catch (error) {
75
+ fail("invalid-state", `The accepted page state does not decode: ${error.message}`);
76
+ }
77
+ try {
78
+ return encodeStateAsUpdateV2(doc);
79
+ } finally {
80
+ doc.destroy();
81
+ }
82
+ }
83
+
84
+ // The same answer `sameAuthority` gives in recovery/identity.js, written here because the
85
+ // edit bundle already pays for this module through the provider: importing the generic
86
+ // recovery layer from the page proof would split it into a chunk every editor load pays.
87
+ const sameAuthority = (a, b) => Boolean(a && b) && Boolean(a.host && a.documentId && a.principalId) && a.host === b.host && a.documentId === b.documentId && a.principalId === b.principalId;
88
+ const sameGeneration = (a, b) => Boolean(a && b) && typeof a.generation === "string" && Boolean(a.generation) && a.generation === b.generation;
89
+
90
+ /**
91
+ * Assemble the proof a checkpoint is retired with.
92
+ *
93
+ * Each named record carries the bytes it owns and the context it was journaled under, so
94
+ * the validator can refuse a record written under another generation without asking the
95
+ * store or a host. `state` must be the exact accepted page state and `article` the final
96
+ * outgoing canonical article; their digests are taken here, over these bytes.
97
+ *
98
+ * @param {Object} options
99
+ * @param {string} options.capture - the capture the save belongs to
100
+ * @param {Array<{id: *, revision: number, update: Uint8Array, context: ?Object}>} options.records
101
+ * @param {{authority: Object, lineage: Object}} options.accepted - the identity and generation it was saved under
102
+ * @param {string} options.article - the final outgoing canonical article
103
+ * @param {Object} options.state - the accepted page state
104
+ * @param {{file: string, pageState: string}} options.receipt - the persisted pair
105
+ * @param {boolean} [options.saved]
106
+ * @returns {Promise<Object>}
107
+ */
108
+ export async function pageProof({ capture, records = [], accepted, article, state, receipt, saved = true } = {}) {
109
+ const entries = [];
110
+ for (const record of records) {
111
+ if (!record || record.id === undefined || typeof record.revision !== "number") fail("invalid-record", "A proof names each record by id and revision");
112
+ const update = bytesOf(record.update);
113
+ if (!update || !update.length) fail("invalid-record", "A proof needs the bytes each record owns");
114
+ entries.push({ id: copyId(record.id), revision: record.revision, update: update.slice(), context: record.context ?? null });
115
+ }
116
+ if (typeof article !== "string") fail("missing-article", "A proof names the final outgoing article");
117
+ const stateBytes = canonicalPageBytes(state);
118
+ return {
119
+ saved: saved === true,
120
+ capture,
121
+ accepted,
122
+ article,
123
+ state: copyState(state),
124
+ stateDigest: await digestHex(stateBytes),
125
+ articleDigest: await digestHex(encoder.encode(article)),
126
+ receipt,
127
+ records: entries,
128
+ };
129
+ }
130
+
131
+ // Checklist items 1, 2, 4 and 5, over the proof alone. Item 3 is the per-record coverage
132
+ // test below, and the projection of the state onto the article is the producer's to make:
133
+ // a proof whose state is not the one it was made over fails its own digest.
134
+ async function checkProof(proof) {
135
+ if (!proof || typeof proof !== "object" || Array.isArray(proof)) fail("missing-proof", "A page checkpoint needs a proof");
136
+ if (proof.saved !== true) fail("not-saved", "Only a save that succeeded under admission makes a proof");
137
+ if (typeof proof.capture !== "string" || !proof.capture) fail("missing-capture", "A proof belongs to the capture it saved");
138
+ const receipt = proof.receipt;
139
+ if (!receipt || typeof receipt.file !== "string" || !receipt.file || typeof receipt.pageState !== "string" || !receipt.pageState) {
140
+ fail("missing-receipt", "A proof names the persisted file and page-state pair");
141
+ }
142
+ const accepted = proof.accepted;
143
+ if (!accepted || !accepted.authority || !accepted.lineage || typeof accepted.lineage.generation !== "string" || !accepted.lineage.generation) {
144
+ fail("missing-identity", "A proof names the authority and generation it was saved under");
145
+ }
146
+ if (typeof proof.article !== "string") fail("missing-article", "A proof names the final outgoing article");
147
+ if (!Array.isArray(proof.records)) fail("missing-records", "A proof names the records it covers");
148
+ if (typeof proof.stateDigest !== "string" || !HEX.test(proof.stateDigest)) fail("missing-digest", "A proof carries the accepted state digest");
149
+ if (typeof proof.articleDigest !== "string" || !HEX.test(proof.articleDigest)) fail("missing-digest", "A proof carries the final article digest");
150
+ const stateBytes = canonicalPageBytes(proof.state);
151
+ if (await digestHex(stateBytes) !== proof.stateDigest) fail("state-digest-mismatch", "The page state is not the one the proof was made over");
152
+ if (await digestHex(encoder.encode(proof.article)) !== proof.articleDigest) fail("article-digest-mismatch", "The article is not the one the proof was made over");
153
+ const entries = new Map();
154
+ for (const entry of proof.records) {
155
+ if (!entry || entry.id === undefined || typeof entry.revision !== "number") fail("invalid-record", "A proof names each record by id and revision");
156
+ const update = bytesOf(entry.update);
157
+ if (!update || !update.length) fail("invalid-record", "A proof needs the bytes each record owns");
158
+ entries.set(identityKey(entry.id), entry);
159
+ }
160
+ return { stateBytes, entries, accepted };
161
+ }
162
+
163
+ // A record is covered only by a state that still carries it, saved under the same
164
+ // authority and the same generation it was journaled under.
165
+ function covers(checked, entry) {
166
+ const context = entry.context;
167
+ if (!context || !sameAuthority(context.authority, checked.accepted.authority)) return false;
168
+ if (!sameGeneration(context.lineage, checked.accepted.lineage)) return false;
169
+ const update = bytesOf(entry.update);
170
+ return Boolean(update) && update.length > 0 && containsUpdate(checked.stateBytes, update);
171
+ }
172
+
173
+ /**
174
+ * The records a page checkpoint proof covers, and no others.
175
+ *
176
+ * Registered as `validateCheckpoint` on the `page.yjs` channel. It runs before the
177
+ * store's transaction, may only narrow the caller's list, and throws on a proof it
178
+ * cannot decode so that an error keeps every record.
179
+ *
180
+ * @param {Object} proof
181
+ * @param {Array<{id: *, revision: number}>} records - what the caller observed
182
+ * @returns {Promise<Array<{id: *, revision: number}>>}
183
+ */
184
+ export async function validatePageCheckpoint(proof, records = []) {
185
+ const checked = await checkProof(proof);
186
+ return (records || []).filter((record) => {
187
+ if (!record || record.id === undefined || typeof record.revision !== "number") return false;
188
+ const entry = checked.entries.get(identityKey(record.id));
189
+ return Boolean(entry) && entry.revision === record.revision && covers(checked, entry);
190
+ });
191
+ }
@@ -0,0 +1,488 @@
1
+ /**
2
+ * recovery-replay.js — rebuild a page from one journaled capsule, and decide whether it
3
+ * may be replayed at all.
4
+ *
5
+ * A capsule is one immutable base and everything the episode put on top of it: the
6
+ * dependencies it received (a peer update a later local insert was built on) and the
7
+ * local work it owns. Replaying all three into a fresh Y.Doc rebuilds the page as this
8
+ * tab had it, read back through the strict decoder — no room, no host, no store.
9
+ *
10
+ * Admission is a separate question from reconstruction: a journal replays only into the
11
+ * authority, principal and generation it was written under, only while the current page
12
+ * still contains the capsule's base, and only into a page sharing its ancestry.
13
+ */
14
+
15
+ import * as Y from "../../vendor/yjs.vendor.js";
16
+ import { toBase64 } from "../../vendor/yjs.vendor.js";
17
+ import { NET } from "./layout.js";
18
+ import { blocksFromYValue } from "./decode.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
+
24
+ /** The channel this adapter journals and replays: one name, one version, everywhere. */
25
+ export const PAGE_CHANNEL = "page.yjs";
26
+ export const PAGE_CHANNEL_VERSION = 1;
27
+
28
+ // When one writer's batches are merged into its capsule: past either threshold, and never
29
+ // over a record the store has not confirmed.
30
+ export const COMPACT_BATCHES = 64;
31
+ export const COMPACT_BYTES = 1024 * 1024;
32
+
33
+ /** The channel's own merge, for the binary store's compaction. */
34
+ export const mergePageUpdates = (parts) => Y.mergeUpdatesV2(parts);
35
+
36
+ export class PageReplayError extends Error {
37
+ constructor(code, message) {
38
+ super(message);
39
+ this.name = "PageReplayError";
40
+ this.code = code;
41
+ }
42
+ }
43
+
44
+ const fail = (code, message) => { throw new PageReplayError(code, message); };
45
+ const identityKey = (id) => (typeof id === "string" ? id : JSON.stringify(id));
46
+ const bytesOf = (value) => {
47
+ if (value instanceof Uint8Array) return value;
48
+ if (value instanceof ArrayBuffer) return new Uint8Array(value);
49
+ return null;
50
+ };
51
+ const sameId = (a, b) => identityKey(a) === identityKey(b);
52
+
53
+ function merge(values) {
54
+ const parts = values.map(bytesOf).filter((bytes) => bytes && bytes.length);
55
+ if (!parts.length) return new Uint8Array(0);
56
+ if (parts.length === 1) return parts[0].slice();
57
+ return Y.mergeUpdatesV2(parts);
58
+ }
59
+
60
+ /**
61
+ * One writer's capsule and its batches as three self-contained updates.
62
+ *
63
+ * Batches are folded in by origin, so a duplicate batch or an update already compacted
64
+ * into the capsule changes nothing: Yjs merges by operation identity. A batch naming
65
+ * another capsule is an error rather than another page's bytes.
66
+ *
67
+ * @param {{capsule: Object, batches?: Array<Object>}} options
68
+ * @returns {Object}
69
+ */
70
+ export function accumulateCapsule({ capsule, batches = [] } = {}) {
71
+ if (!capsule || capsule.kind !== "binary" || !capsule.payload) fail("invalid-capsule", "Replay needs a binary capsule");
72
+ if (capsule.version !== undefined && capsule.version !== 1) fail("unknown-version", `Unknown page recovery capsule version: ${capsule.version}`);
73
+ const dependencies = [capsule.payload.dependencies];
74
+ const owned = [capsule.payload.owned];
75
+ const seen = new Set();
76
+ for (const batch of batches) {
77
+ if (!batch || batch.kind !== "binary-batch" || !batch.payload) fail("invalid-batch", "Replay reads binary batches only");
78
+ if (batch.capsuleId !== undefined && !sameId(batch.capsuleId, capsule.id)) fail("foreign-batch", "A batch belongs to another capsule");
79
+ if (batch.id !== undefined) {
80
+ if (seen.has(identityKey(batch.id))) continue;
81
+ seen.add(identityKey(batch.id));
82
+ }
83
+ if (batch.origin === "dependency") dependencies.push(batch.payload);
84
+ else owned.push(batch.payload);
85
+ }
86
+ return {
87
+ id: capsule.id,
88
+ context: capsule.context ?? null,
89
+ revision: capsule.revision ?? 0,
90
+ acknowledged: capsule.acknowledged ?? null,
91
+ base: merge([capsule.payload.base]),
92
+ dependencies: merge(dependencies),
93
+ owned: merge(owned),
94
+ };
95
+ }
96
+
97
+ // The capsule as one page value: the exact bytes a save would carry, so the strict
98
+ // decoder reads the same state a checkpoint would.
99
+ export function replayPageValue({ capsule, batches = [] } = {}) {
100
+ const parts = accumulateCapsule({ capsule, batches });
101
+ const doc = new Y.Doc();
102
+ try {
103
+ for (const bytes of [parts.base, parts.dependencies, parts.owned]) if (bytes.length) Y.applyUpdateV2(doc, bytes, NET);
104
+ if (doc.store.pendingStructs || doc.store.pendingDs) fail("unresolved-update", "The capsule's updates are missing a prerequisite");
105
+ return { v: 1, g: 0, y: { seed: toBase64(Y.encodeStateAsUpdateV2(doc)) } };
106
+ } finally {
107
+ doc.destroy();
108
+ }
109
+ }
110
+
111
+ /**
112
+ * The whole page a capsule reconstructs, decoded through the strict decoder.
113
+ *
114
+ * @param {{capsule: Object, batches?: Array<Object>}} options
115
+ * @returns {Object} the block model, by block id
116
+ */
117
+ export function reconstructPage({ capsule, batches = [] } = {}) {
118
+ return blocksFromYValue(replayPageValue({ capsule, batches }));
119
+ }
120
+
121
+ /**
122
+ * The capsule's parts, if this page may replay them.
123
+ *
124
+ * `current` is the page as it is now: its own authority, generation and accepted state.
125
+ * A journal from another authority or principal is never applied, a journal from another
126
+ * generation is a page that was replaced while the tab was away, and a page that no
127
+ * longer contains the capsule's base has nothing to replay it onto.
128
+ *
129
+ * @param {{capsule: Object, batches?: Array<Object>, current: {context: Object, state: Object}}} options
130
+ * @returns {Object} the accumulated parts, ready to apply
131
+ */
132
+ export function admitCapsule({ capsule, batches = [], current } = {}) {
133
+ const parts = accumulateCapsule({ capsule, batches });
134
+ if (!parts.context?.authority) fail("missing-identity", "A capsule with no authority is never replayed");
135
+ if (!current?.context?.authority) fail("missing-identity", "Replay needs the page's own authority");
136
+ if (!sameAuthority(parts.context.authority, current.context.authority)) fail("foreign-authority", "This journal belongs to another document or account");
137
+ if (!sameLineage(parts.context.lineage, current.context.lineage)) fail("foreign-generation", "This journal belongs to another generation of this page");
138
+ const stateBytes = canonicalPageBytes(current.state);
139
+ if (parts.base.length && !containsUpdate(stateBytes, parts.base)) fail("missing-base", "The page no longer contains the journal's base");
140
+ if (!parts.owned.length) fail("nothing-owned", "This capsule owns no local work");
141
+ // The reconstructed page is what has to share this page's ancestry, base included:
142
+ // this tab's own inserts carry its client alone, and never the page's.
143
+ const rebuilt = merge([parts.base, parts.dependencies, parts.owned]);
144
+ if (!sharesAncestry(current.state, { v: 1, g: 0, y: { seed: toBase64(rebuilt) } })) fail("foreign-lineage", "This journal shares no ancestry with this page");
145
+ return parts;
146
+ }
147
+
148
+ /**
149
+ * One writer's capsules, each with the batches that belong to it.
150
+ *
151
+ * A session that died before its batches were compacted leaves them behind, so a
152
+ * capsule is only ever read together with every batch naming it, whichever session
153
+ * wrote it.
154
+ *
155
+ * @param {Array<Object>} [records] - what the channel listed
156
+ * @returns {Array<{capsule: Object, batches: Array<Object>}>}
157
+ */
158
+ export function capsulesOf(records = []) {
159
+ const capsules = new Map();
160
+ const batches = new Map();
161
+ for (const record of records || []) {
162
+ if (!record || record.supported === false) continue;
163
+ if (record.kind === "binary") {
164
+ capsules.set(identityKey(record.id), record);
165
+ continue;
166
+ }
167
+ if (record.kind !== "binary-batch" || record.capsuleId === undefined) continue;
168
+ const key = identityKey(record.capsuleId);
169
+ const list = batches.get(key) ?? [];
170
+ list.push(record);
171
+ batches.set(key, list);
172
+ }
173
+ return [...capsules.entries()].map(([key, capsule]) => ({ capsule, batches: batches.get(key) ?? [] }));
174
+ }
175
+
176
+ /**
177
+ * Apply an admitted capsule's three parts into a live document.
178
+ *
179
+ * The recovery origin is its own: the provider offers the result to the room, and the
180
+ * journal never takes it as this tab's own work, so a recovery is never journaled twice.
181
+ *
182
+ * @param {Object} doc - the Y.Doc the page is being edited in
183
+ * @param {Object} parts - an `accumulateCapsule` result
184
+ */
185
+ export function applyCapsule(doc, parts) {
186
+ for (const bytes of [parts.base, parts.dependencies, parts.owned]) {
187
+ if (bytes && bytes.length) Y.applyUpdateV2(doc, bytes, RECOVERY_ORIGIN);
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Replay every capsule this page may replay, and name the ones it may not.
193
+ *
194
+ * Nothing is applied for a capsule that fails admission: a changed authority, a changed
195
+ * 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
+ *
198
+ * @param {{records?: Array<Object>, current: {context: Object, state: Object}, into: Object}} options
199
+ * @returns {{replayed: Array<Object>, review: Array<Object>}}
200
+ */
201
+ export function replayJournals({ records = [], current, into } = {}) {
202
+ const replayed = [];
203
+ const review = [];
204
+ for (const entry of capsulesOf(records)) {
205
+ let parts;
206
+ try {
207
+ parts = admitCapsule({ capsule: entry.capsule, batches: entry.batches, current });
208
+ } 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" });
211
+ continue;
212
+ }
213
+ applyCapsule(into, parts);
214
+ replayed.push({ id: entry.capsule.id, revision: parts.revision, owned: parts.owned.length });
215
+ }
216
+ return { replayed, review };
217
+ }
218
+
219
+ /**
220
+ * The context one episode's capsules are journaled under.
221
+ *
222
+ * A host that persists a page generation names it; without one, the generation is the
223
+ * digest of the accepted base, so a page rebuilt from other bytes is another generation
224
+ * and a page that has not changed keeps its own.
225
+ *
226
+ * @param {Object} options
227
+ * @param {{host: string, documentId: string, principalId: string}} options.authority
228
+ * @param {?string} options.generation
229
+ * @param {?Uint8Array} options.base - the accepted state captured at the episode's start
230
+ * @param {number} [options.formatFloor]
231
+ * @returns {Promise<Object>}
232
+ */
233
+ export async function pageJournalContext({ authority, generation = null, base = null, formatFloor = 0 } = {}) {
234
+ const bytes = bytesOf(base) ?? new Uint8Array(0);
235
+ const token = typeof generation === "string" && generation ? generation : await digestHex(bytes);
236
+ return { authority, lineage: await lineageOf(token, bytes), formatFloor };
237
+ }
238
+
239
+ /**
240
+ * The authority this page journals under, or null when it cannot be pinned.
241
+ *
242
+ * A host that names people names the account too, and an answer without one is not a
243
+ * principal: replay waits for the next boot rather than filing work under a guess. A
244
+ * host with no people support gives its document to whoever holds it, so the document's
245
+ * own host identity is the principal there.
246
+ *
247
+ * @param {Object} [options]
248
+ * @param {?Object} [options.meta] - the host's discovery answer
249
+ * @param {?Location} [options.location]
250
+ * @param {?Element} [options.root]
251
+ * @returns {?{host: string, documentId: string, principalId: string}}
252
+ */
253
+ export function pageAuthority({ meta = null, location: where = globalThis.location, root = globalThis.document?.documentElement } = {}) {
254
+ const host = where?.origin ?? "";
255
+ if (!host) return null;
256
+ const documentId = hostIdentityOf(root)?.identity ?? pathIdentityOf(where?.pathname ?? "/").identity;
257
+ if (!documentId) return null;
258
+ const people = meta?.document?.people;
259
+ const principalId = people && typeof people === "object" ? people.me?.id ?? null : documentId;
260
+ if (!principalId) return null;
261
+ return { host, documentId, principalId };
262
+ }
263
+
264
+ const journalKey = (id) => (typeof id === "string" ? id : JSON.stringify(id));
265
+
266
+ /**
267
+ * The accepted page state an episode starts from, as the bytes its capsule keeps.
268
+ *
269
+ * @param {Object} state - a version 1 page value
270
+ * @returns {Uint8Array}
271
+ */
272
+ export const journalBase = (state) => canonicalPageBytes(state);
273
+
274
+ /**
275
+ * The page channel of one recovery service, registered once per page load.
276
+ *
277
+ * A channel is registered at its own version and nothing calls it until a page can
278
+ * actually journal: a reader and a page without a pinned identity register nothing.
279
+ *
280
+ * @param {Object} service - `clay.recovery`
281
+ * @returns {Object} the channel api
282
+ */
283
+ const channels = new WeakMap();
284
+ export function pageChannel(service) {
285
+ if (!service) return null;
286
+ const existing = channels.get(service);
287
+ if (existing) return existing;
288
+ const channel = service.register({
289
+ channel: PAGE_CHANNEL,
290
+ version: PAGE_CHANNEL_VERSION,
291
+ validateCheckpoint: validatePageCheckpoint,
292
+ merge: mergePageUpdates,
293
+ });
294
+ channels.set(service, channel);
295
+ return channel;
296
+ }
297
+
298
+ /**
299
+ * One session's journal in the shared recovery store.
300
+ *
301
+ * Every local update is handed over as it happens: the bytes and the id are fixed
302
+ * synchronously, so a buffer reused after the call cannot change what was written, and
303
+ * the append resolves only once its transaction committed. Nothing here waits for the
304
+ * room, the network or the editor. Compaction names only the records the store has
305
+ * confirmed, because an append still in flight or refused has no revision to swap and
306
+ * must keep exactly the bytes it was given.
307
+ *
308
+ * @param {Object} options
309
+ * @param {Object} options.channel - the `page.yjs` channel api
310
+ * @param {string} options.key - the document this episode belongs to
311
+ * @param {Function|Object} options.context - (base) => Promise<context>, or the context
312
+ * @param {?Uint8Array} [options.base] - the accepted state the episode started from
313
+ * @returns {Object}
314
+ */
315
+ export function createPageJournal({ channel, key, context, base = null, onChange = () => {} }) {
316
+ const records = new Map();
317
+ const waiting = [];
318
+ const inflight = new Set();
319
+ const bytesOfBase = bytesOf(base) ?? new Uint8Array(0);
320
+ let resolved = null;
321
+ let failure = null;
322
+ let batches = 0;
323
+ let batchBytes = 0;
324
+ let episode = false;
325
+ let compacting = null;
326
+ let closed = false;
327
+
328
+ const journal = {
329
+ channel,
330
+ key,
331
+ onChange,
332
+ context: () => ready,
333
+ local: (bytes) => send(bytes, "local"),
334
+ // Only while an episode is open: a peer's updates on their own are not this tab's work
335
+ // to keep, and a page that has journaled nothing owns nothing.
336
+ dependency: (bytes) => { if (episode) send(bytes, "dependency"); },
337
+ records: () => [...records.values()],
338
+ names,
339
+ pending: () => inflight.size + (failure ? 1 : 0),
340
+ get failure() { return failure; },
341
+ retire,
342
+ compact,
343
+ // An acknowledgement is metadata: it says a room holds the bytes and retires nothing.
344
+ acknowledged: () => {
345
+ if (closed || !resolved || !records.size || typeof channel.markAcknowledged !== "function") return Promise.resolve(null);
346
+ return Promise.resolve(channel.markAcknowledged({ records: names(), authority: resolved.authority })).catch(() => null);
347
+ },
348
+ close: () => {
349
+ closed = true;
350
+ waiting.length = 0;
351
+ },
352
+ };
353
+
354
+ const ready = Promise.resolve()
355
+ .then(() => (typeof context === "function" ? context(bytesOfBase) : context))
356
+ .catch(() => null)
357
+ .then((value) => {
358
+ resolved = value && value.authority ? value : null;
359
+ if (!resolved) {
360
+ closed = true;
361
+ failure = "unavailable";
362
+ waiting.length = 0;
363
+ journal.onChange();
364
+ return null;
365
+ }
366
+ for (const run of waiting.splice(0)) run(resolved);
367
+ return resolved;
368
+ });
369
+
370
+ function retire(list) {
371
+ for (const item of list || []) records.delete(journalKey(item?.id));
372
+ }
373
+
374
+ function names() {
375
+ return [...records.values()].map(({ id, revision }) => ({ id, revision }));
376
+ }
377
+
378
+ function compact() {
379
+ if (compacting || closed || !records.size || typeof channel.compact !== "function") return compacting;
380
+ const named = names();
381
+ compacting = Promise.resolve()
382
+ .then(() => channel.compact({ records: named }))
383
+ .then((receipt) => {
384
+ if (!receipt?.ok) return receipt;
385
+ retire(receipt.retired);
386
+ batches = Math.max(0, batches - (receipt.retired?.length ?? 0));
387
+ if (!batches) batchBytes = 0;
388
+ return receipt;
389
+ })
390
+ .catch(() => null)
391
+ .then((receipt) => {
392
+ compacting = null;
393
+ journal.onChange();
394
+ return receipt;
395
+ });
396
+ return compacting;
397
+ }
398
+
399
+ function write(context, bytes, origin, first) {
400
+ let answer;
401
+ try {
402
+ answer = channel.append({ key, context, update: bytes, origin, base: first ? bytesOfBase : null });
403
+ } catch (_) {
404
+ failure = "error";
405
+ journal.onChange();
406
+ return;
407
+ }
408
+ const token = {};
409
+ inflight.add(token);
410
+ Promise.resolve(answer).then(
411
+ (receipt) => {
412
+ inflight.delete(token);
413
+ if (!receipt || receipt.ok !== true) {
414
+ failure = receipt?.code ?? "error";
415
+ journal.onChange();
416
+ return;
417
+ }
418
+ failure = null;
419
+ if (!receipt.skipped) {
420
+ records.set(journalKey(receipt.id), { id: receipt.id, revision: receipt.revision, update: bytes, context });
421
+ batches += 1;
422
+ batchBytes += bytes.byteLength;
423
+ }
424
+ journal.onChange();
425
+ if (batches >= COMPACT_BATCHES || batchBytes >= COMPACT_BYTES) compact();
426
+ },
427
+ () => {
428
+ inflight.delete(token);
429
+ failure = "error";
430
+ journal.onChange();
431
+ },
432
+ );
433
+ }
434
+
435
+ function send(bytes, origin) {
436
+ if (closed || !bytes || !bytes.byteLength || typeof channel.append !== "function") return;
437
+ const first = !episode;
438
+ episode = true;
439
+ if (resolved) write(resolved, bytes, origin, first);
440
+ else waiting.push((context) => write(context, bytes, origin, first));
441
+ }
442
+
443
+ return journal;
444
+ }
445
+
446
+ /**
447
+ * This session's journal, opened from the host's own answer, or null.
448
+ *
449
+ * A host that cannot name who is writing, or a tab with no recovery service, journals
450
+ * nothing: a page that cannot pin its identity never replays across sessions.
451
+ *
452
+ * @param {Object} options
453
+ * @param {?Object} options.meta - the host's discovery answer
454
+ * @param {Object} options.service - `clay.recovery`
455
+ * @param {?Uint8Array} options.base - the accepted state captured before input
456
+ * @param {Function} [options.onChange]
457
+ * @returns {?Object}
458
+ */
459
+ export function openPageJournal({ meta = null, service = null, base = null, onChange = () => {}, location, root } = {}) {
460
+ const authority = pageAuthority({ meta, location, root });
461
+ if (!authority) return null;
462
+ const channel = pageChannel(service);
463
+ if (!channel) return null;
464
+ const generation = meta?.document?.pageState?.generation ?? null;
465
+ const formatFloor = Array.isArray(meta?.extensions) && meta.extensions.includes("page-format-v1") ? 1 : 0;
466
+ return createPageJournal({
467
+ channel,
468
+ key: [authority.host, authority.documentId].join(":"),
469
+ context: (accepted) => pageJournalContext({ authority, generation, base: accepted, formatFloor }),
470
+ base,
471
+ onChange,
472
+ });
473
+ }
474
+
475
+ /**
476
+ * Replay this page's journal once, after the authority snapshot is adopted.
477
+ *
478
+ * @param {{journal: Object, state: Object, into: Object}} options
479
+ * @returns {Promise<{replayed: Array<Object>, review: Array<Object>}>}
480
+ */
481
+ export async function replayInto({ journal, state, into } = {}) {
482
+ if (!journal) return { replayed: [], review: [] };
483
+ const context = await journal.context();
484
+ if (!context) return { replayed: [], review: [] };
485
+ const listed = await journal.channel.list();
486
+ if (!listed?.ok) return { replayed: [], review: [] };
487
+ return replayJournals({ records: listed.records, current: { context, state }, into });
488
+ }