@panphora/clayjs 1.15.2 → 1.16.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 (506) 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 +83090 -58706
  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-NWPPXOCZ.js +1 -0
  21. package/dist/m/attrs/save-freeze-FUP4Q4X3.js +1 -0
  22. package/dist/m/c-2AZDZWCK.js +1 -0
  23. package/dist/m/c-2JL6E75N.js +1 -0
  24. package/dist/m/c-2M73252O.js +1 -0
  25. package/dist/m/c-2SPVQSHC.js +1 -0
  26. package/dist/m/c-33HUYEDI.js +1 -0
  27. package/dist/m/c-33JJ6CBX.js +2 -0
  28. package/dist/m/c-35NBGMZB.js +1 -0
  29. package/dist/m/c-3GJ4SDE2.js +1 -0
  30. package/dist/m/c-3IQRECYS.js +1 -0
  31. package/dist/m/c-3OWDMNJR.js +2 -0
  32. package/dist/m/c-46XYM55L.js +1 -0
  33. package/dist/m/c-4B6H4G5V.js +1 -0
  34. package/dist/m/c-4DRMS4QG.js +9 -0
  35. package/dist/m/c-4FFHZM32.js +1 -0
  36. package/dist/m/c-4JZDZFF6.js +1 -0
  37. package/dist/m/c-4L3BTFRK.js +1 -0
  38. package/dist/m/c-4Q2XTQ2N.js +2 -0
  39. package/dist/m/c-4WKFKEKF.js +1 -0
  40. package/dist/m/c-4YSCRICO.js +1 -0
  41. package/dist/m/c-54RCDCBU.js +1 -0
  42. package/dist/m/c-5M4YEKCX.js +4 -0
  43. package/dist/m/c-5R4PTRG2.js +1 -0
  44. package/dist/m/c-5V3MOVJT.js +1 -0
  45. package/dist/m/c-5VYOWDTI.js +1 -0
  46. package/dist/m/c-5Y6TNX5W.js +34 -0
  47. package/dist/m/c-5YIHJ3TJ.js +1 -0
  48. package/dist/m/c-66O32MV3.js +28 -0
  49. package/dist/m/c-6HMB24EV.js +1 -0
  50. package/dist/m/c-7DJDKPGL.js +1 -0
  51. package/dist/m/c-7O6AFR65.js +1 -0
  52. package/dist/m/c-7SWMYKAI.js +1 -0
  53. package/dist/m/c-7VGZ7RU5.js +1 -0
  54. package/dist/m/c-A7LS5UZW.js +1 -0
  55. package/dist/m/c-ANQTJ3B5.js +2 -0
  56. package/dist/m/c-AQLFEATB.js +1 -0
  57. package/dist/m/c-AYVOREBE.js +1 -0
  58. package/dist/m/c-B2ZXPOS3.js +3 -0
  59. package/dist/m/c-BBVGTQG7.js +1 -0
  60. package/dist/m/c-BMIZKMPV.js +1 -0
  61. package/dist/m/c-BTDI4Y23.js +1 -0
  62. package/dist/m/c-BWFVSBDF.js +1 -0
  63. package/dist/m/c-BYTKL6UX.js +1 -0
  64. package/dist/m/c-CFHW5AMY.js +6 -0
  65. package/dist/m/c-CJZSLMVQ.js +1 -0
  66. package/dist/m/c-CYWLPPUS.js +1 -0
  67. package/dist/m/c-CZUSOB7V.js +1 -0
  68. package/dist/m/c-DBE6ORSI.js +1 -0
  69. package/dist/m/c-DCFSEJZG.js +1 -0
  70. package/dist/m/c-DNZ5IFM6.js +1 -0
  71. package/dist/m/c-EDHUR4MX.js +1 -0
  72. package/dist/m/c-EJTGYSLN.js +1 -0
  73. package/dist/m/c-EY6UOM54.js +1 -0
  74. package/dist/m/c-F3CKJTJS.js +1 -0
  75. package/dist/m/c-F7Y6FLPY.js +1 -0
  76. package/dist/m/c-FA4DZVTC.js +1 -0
  77. package/dist/m/c-FILLTJA7.js +1 -0
  78. package/dist/m/c-G2BUZITR.js +1 -0
  79. package/dist/m/c-GB3JTV6E.js +1 -0
  80. package/dist/m/c-GBGWXTX4.js +1 -0
  81. package/dist/m/c-GHBCCVG4.js +1 -0
  82. package/dist/m/c-GJAEGGXY.js +1 -0
  83. package/dist/m/c-GOWBDM6J.js +1 -0
  84. package/dist/m/c-GQ6F6IDK.js +1 -0
  85. package/dist/m/c-H4LG442Q.js +1 -0
  86. package/dist/m/c-HA6INT6J.js +1 -0
  87. package/dist/m/c-HPURYVZS.js +2 -0
  88. package/dist/m/c-HRDCD3WN.js +67 -0
  89. package/dist/m/c-HT33EQRN.js +1 -0
  90. package/dist/m/c-HVYXIS72.js +1 -0
  91. package/dist/m/c-IB52R237.js +1 -0
  92. package/dist/m/c-IJUK2GPW.js +1 -0
  93. package/dist/m/c-IKWNE4R4.js +1 -0
  94. package/dist/m/c-IU3IYEU6.js +1 -0
  95. package/dist/m/c-IZ2EBRAJ.js +1 -0
  96. package/dist/m/c-J7CA54IP.js +1 -0
  97. package/dist/m/c-JAX2667S.js +6 -0
  98. package/dist/m/c-KI7M4AY3.js +1 -0
  99. package/dist/m/c-KIPQJX4B.js +1 -0
  100. package/dist/m/c-KKWLDOPM.js +1 -0
  101. package/dist/m/c-KR4UW4KW.js +1 -0
  102. package/dist/m/c-KY3VJGUA.js +8 -0
  103. package/dist/m/c-L4WQSC27.js +1 -0
  104. package/dist/m/c-LFPHCXY3.js +1 -0
  105. package/dist/m/c-LR4CGEPT.js +91 -0
  106. package/dist/m/c-LRPUO2MX.js +1 -0
  107. package/dist/m/c-LTGMXXKO.js +1 -0
  108. package/dist/m/c-LXENEBV7.js +1 -0
  109. package/dist/m/c-LZJPNR2W.js +1 -0
  110. package/dist/m/c-MMUIUUZG.js +2 -0
  111. package/dist/m/c-MTQHWQC3.js +1 -0
  112. package/dist/m/c-N326R2JD.js +1 -0
  113. package/dist/m/c-N7JPUPGC.js +1 -0
  114. package/dist/m/c-NMJRXKPC.js +8 -0
  115. package/dist/m/c-NRPJEVJS.js +1 -0
  116. package/dist/m/c-NUHP2XDB.js +1 -0
  117. package/dist/m/c-NZMNXVF6.js +1 -0
  118. package/dist/m/c-OCSZ4QGN.js +1 -0
  119. package/dist/m/c-OIBUTDXI.js +1 -0
  120. package/dist/m/c-OIDRZKAF.js +1 -0
  121. package/dist/m/c-OYTFP5QN.js +1 -0
  122. package/dist/m/c-P7KNDG34.js +1 -0
  123. package/dist/m/c-PPOSCAQD.js +1 -0
  124. package/dist/m/c-PQ5LK6QE.js +993 -0
  125. package/dist/m/c-PZ4XEPUP.js +2 -0
  126. package/dist/m/c-QHIWXHRO.js +1 -0
  127. package/dist/m/c-QHWJ5ZIJ.js +1 -0
  128. package/dist/m/c-QIX5BJYN.js +1 -0
  129. package/dist/m/c-QNKREOL6.js +1 -0
  130. package/dist/m/c-QQHEX6HA.js +1 -0
  131. package/dist/m/c-R7RMANJI.js +9 -0
  132. package/dist/m/c-RI4NT75Y.js +1 -0
  133. package/dist/m/c-RM7J7BV2.js +1 -0
  134. package/dist/m/c-RUJKSUQW.js +1 -0
  135. package/dist/m/c-RVMWGXEJ.js +1 -0
  136. package/dist/m/c-S7H2DRCB.js +1 -0
  137. package/dist/m/c-S7LK4KUE.js +1 -0
  138. package/dist/m/c-SGCCC4FW.js +25 -0
  139. package/dist/m/c-SGIA4IEQ.js +188 -0
  140. package/dist/m/c-SWRJFCNN.js +1 -0
  141. package/dist/m/c-T4VO4OCB.js +1 -0
  142. package/dist/m/c-TCCA5BZN.js +2 -0
  143. package/dist/m/c-TQEMVJCS.js +1 -0
  144. package/dist/m/c-TQZ2JYHD.js +1 -0
  145. package/dist/m/c-TR6RMDQ7.js +3 -0
  146. package/dist/m/c-TRS6TM6O.js +1 -0
  147. package/dist/m/c-UGHH4MB3.js +1 -0
  148. package/dist/m/c-UKHTLWUW.js +1 -0
  149. package/dist/m/c-UL2YUY52.js +1 -0
  150. package/dist/m/c-UNN7LWCT.js +1 -0
  151. package/dist/m/c-UXBMRDZB.js +235 -0
  152. package/dist/m/c-V7PZZRG7.js +1 -0
  153. package/dist/m/c-VAK7SG37.js +1 -0
  154. package/dist/m/c-VDSS4K3Z.js +1 -0
  155. package/dist/m/c-VFX45IZQ.js +1 -0
  156. package/dist/m/c-VHNSBD2M.js +1 -0
  157. package/dist/m/c-VKEDWYDF.js +1 -0
  158. package/dist/m/c-VL52ADDR.js +1 -0
  159. package/dist/m/c-WN5AY3OZ.js +1 -0
  160. package/dist/m/c-WNYBLIOZ.js +1 -0
  161. package/dist/m/c-WRBKEJD4.js +1 -0
  162. package/dist/m/c-WWDBYXLK.js +1 -0
  163. package/dist/m/c-WYFDLITL.js +1 -0
  164. package/dist/m/c-XN6MBKI6.js +67 -0
  165. package/dist/m/c-XNNW2CWK.js +1 -0
  166. package/dist/m/c-XPELB6E5.js +1 -0
  167. package/dist/m/c-XQXZAO7N.js +1 -0
  168. package/dist/m/c-Y6USGTS5.js +5 -0
  169. package/dist/m/c-YDH7Q3NY.js +1 -0
  170. package/dist/m/c-YG2NJYS6.js +135 -0
  171. package/dist/m/c-Z36J63PP.js +1 -0
  172. package/dist/m/c-Z4VRC7GU.js +7 -0
  173. package/dist/m/c-Z6K52OLP.js +19 -0
  174. package/dist/m/c-ZDY2MDAA.js +21 -0
  175. package/dist/m/c-ZE46QDIH.js +1 -0
  176. package/dist/m/c-ZUCOKOHT.js +1 -0
  177. package/dist/m/core/admin-attrs-VGNLAUDK.js +1 -0
  178. package/dist/m/core/autosave-E4PEFPZ6.js +1 -0
  179. package/dist/m/core/conflict-notice-VQ6Y64PH.js +1 -0
  180. package/dist/m/core/draft-mode-WSQAT67S.js +1 -0
  181. package/dist/m/core/host-permission-XN3K2IW3.js +1 -0
  182. package/dist/m/core/is-edit-mode-JXHDVGUL.js +1 -0
  183. package/dist/m/core/persist-HKHA66IY.js +3 -0
  184. package/dist/m/core/recovery-TBBSICE2.js +1 -0
  185. package/dist/m/core/save-WUCHTQBE.js +1 -0
  186. package/dist/m/core/save-core-FJVHT5M3.js +1 -0
  187. package/dist/m/core/snapshot-F3SWDZMM.js +1 -0
  188. package/dist/m/core/unsaved-warning-STKBHE6G.js +1 -0
  189. package/dist/m/dom/all-APS5ZSKP.js +1 -0
  190. package/dist/m/dom/dom-helpers-L7OSR6S4.js +1 -0
  191. package/dist/m/events/index-23DPTUT4.js +1 -0
  192. package/dist/m/internals/index-WOVAP3RU.js +1 -0
  193. package/dist/m/lib/cache-bust-CGWT6ZJI.js +1 -0
  194. package/dist/m/lib/region-policy-SB2AWVG5.js +1 -0
  195. package/dist/m/loader-HXX6YNH5.js +1 -0
  196. package/dist/m/manifest-F22DEDAC.js +1 -0
  197. package/dist/m/manifest-edit-C3C7376B.js +1 -0
  198. package/dist/m/manifest.json +3491 -0
  199. package/dist/m/options/options-2LH76JQ3.js +1 -0
  200. package/dist/m/page/editor-XUTB2HB2.js +189 -0
  201. package/dist/m/plugins/ai-edit-ZQ4CGWIP.js +6 -0
  202. package/dist/m/plugins/bind-text-NX46N5UV.js +1 -0
  203. package/dist/m/plugins/cms-HP3LSTPN.js +2293 -0
  204. package/dist/m/plugins/demo-ZIBFAA3R.js +1 -0
  205. package/dist/m/plugins/derive-UAAA4V4S.js +1 -0
  206. package/dist/m/plugins/indicator-PV4YW2ID.js +1 -0
  207. package/dist/m/plugins/live-NUTTXSBS.js +1 -0
  208. package/dist/m/plugins/menu-HCKO3DSP.js +138 -0
  209. package/dist/m/plugins/message-LQ6EDH4V.js +1 -0
  210. package/dist/m/plugins/page-KSUH7G6J.js +2 -0
  211. package/dist/m/plugins/page-view-W73OW7WI.js +1 -0
  212. package/dist/m/plugins/people-AJJ32YCG.js +1 -0
  213. package/dist/m/plugins/quickcrop-QNY3PKLD.js +26 -0
  214. package/dist/m/plugins/quickcrop-QNY3PKLD.js.LEGAL.txt +7 -0
  215. package/dist/m/plugins/richclay-RC3L2F6X.js +517 -0
  216. package/dist/m/plugins/richclay-RC3L2F6X.js.LEGAL.txt +1 -0
  217. package/dist/m/plugins/sortable-YRMQX3ZU.js +1 -0
  218. package/dist/m/plugins/source-KHK5UUSK.js +1 -0
  219. package/dist/m/plugins/undo-OF2L3CXU.js +1 -0
  220. package/dist/m/plugins/unsent-HJFLQJ2O.js +1 -0
  221. package/dist/m/plugins/upgrade-ZNYODP5Y.js +1 -0
  222. package/dist/m/plugins/upload-XSGNILUY.js +1 -0
  223. package/dist/m/plugins/visitor-drafts-XK5GW55Q.js +21 -0
  224. package/dist/m/plugins/visitor-drafts-owner-KSSH75EV.js +1 -0
  225. package/dist/m/plugins/wire-ZWVWKF76.js +1 -0
  226. package/dist/m/reader-core-ILWLXVMP.js +68 -0
  227. package/dist/m/route/route-KOCZSEGN.js +1 -0
  228. package/dist/m/sync/live-sync-4AUVXVJU.js +1 -0
  229. package/dist/m/ui/index-EVHKMKBL.js +1 -0
  230. package/dist/m/utils/index-4TJCVLT7.js +1 -0
  231. package/dist/m/vendor/Sortable.vendor-A755ITLN.js +3 -0
  232. package/dist/m/vendor/Sortable.vendor-A755ITLN.js.LEGAL.txt +1 -0
  233. package/dist/m/vendor/sap-core.vendor-A7BMUYZT.js +6 -0
  234. package/dist/offline-manifest.json +3800 -0
  235. package/dist/page/themes/light-1.css +406 -0
  236. package/dist/server/core.cjs +23662 -22860
  237. package/dist/server/offline-runtime.cjs +4954 -0
  238. package/dist/server/offline-runtime.mjs +4923 -0
  239. package/dist/server/rooms.mjs +9 -0
  240. package/docs/guide.md +1078 -669
  241. package/entries/all.js +2 -0
  242. package/entries/clay-data.js +1 -1
  243. package/entries/clay-dom.js +2 -0
  244. package/entries/clay-events.js +2 -0
  245. package/entries/clay-internals.js +2 -0
  246. package/entries/clay-options.js +2 -0
  247. package/entries/clay-router.js +2 -0
  248. package/entries/clay-ui.js +2 -0
  249. package/entries/clay-utils.js +2 -0
  250. package/entries/clay.js +2 -0
  251. package/entries/sap.js +6 -6
  252. package/fonts/ibm-plex-mono-LICENSE +93 -0
  253. package/fonts/ibm-plex-mono-latin-300-normal.woff2 +0 -0
  254. package/fonts/ibm-plex-mono-latin-400-italic.woff2 +0 -0
  255. package/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  256. package/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  257. package/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
  258. package/fonts/ibm-plex-sans-LICENSE +93 -0
  259. package/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
  260. package/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
  261. package/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
  262. package/fonts/ibm-plex.css +71 -0
  263. package/fonts/inter-LICENSE +93 -0
  264. package/fonts/inter-cyrillic-ext-wght-normal.woff2 +0 -0
  265. package/fonts/inter-cyrillic-wght-normal.woff2 +0 -0
  266. package/fonts/inter-greek-ext-wght-normal.woff2 +0 -0
  267. package/fonts/inter-greek-wght-normal.woff2 +0 -0
  268. package/fonts/inter-latin-ext-wght-normal.woff2 +0 -0
  269. package/fonts/inter-latin-wght-normal.woff2 +0 -0
  270. package/fonts/inter-vietnamese-wght-normal.woff2 +0 -0
  271. package/fonts/inter.css +62 -0
  272. package/fonts/newsreader-LICENSE +93 -0
  273. package/fonts/newsreader-latin-ext-opsz-normal.woff2 +0 -0
  274. package/fonts/newsreader-latin-opsz-normal.woff2 +0 -0
  275. package/fonts/newsreader-vietnamese-opsz-normal.woff2 +0 -0
  276. package/fonts/newsreader.css +26 -0
  277. package/fonts/work-sans-LICENSE +93 -0
  278. package/fonts/work-sans-latin-wght-normal.woff2 +0 -0
  279. package/fonts/work-sans.css +8 -0
  280. package/package.json +20 -3
  281. package/packed-contract.json +1 -0
  282. package/src/attrs/add-from.js +27 -4
  283. package/src/attrs/save-freeze.js +7 -52
  284. package/src/core/admin-attrs.js +119 -12
  285. package/src/core/admin-contenteditable.js +12 -10
  286. package/src/core/admin-inputs.js +14 -15
  287. package/src/core/admin-onclick.js +2 -0
  288. package/src/core/autosave.js +74 -21
  289. package/src/core/conflict-download.js +22 -0
  290. package/src/core/conflict-notice-ui.js +1173 -0
  291. package/src/core/conflict-notice.js +140 -697
  292. package/src/core/edit-mode.js +147 -14
  293. package/src/core/etag.js +30 -12
  294. package/src/core/host-meta.js +72 -8
  295. package/src/core/host-permission.js +29 -0
  296. package/src/core/is-edit-mode.js +145 -12
  297. package/src/core/mode-key.js +125 -0
  298. package/src/core/mode-lifecycle.js +544 -0
  299. package/src/core/mode-receipt.js +30 -0
  300. package/src/core/mode-refusal.js +13 -0
  301. package/src/core/mode-resources.js +52 -0
  302. package/src/core/mode-support.js +82 -0
  303. package/src/core/mode-switch.js +259 -0
  304. package/src/core/mode-transition.js +112 -0
  305. package/src/core/page-data-write.js +305 -0
  306. package/src/core/page-data.js +26 -227
  307. package/src/core/page-format-wire.js +4 -0
  308. package/src/core/page-format.js +58 -0
  309. package/src/core/persist.js +37 -9
  310. package/src/core/preserve-authored.js +114 -0
  311. package/src/core/recovery.js +772 -0
  312. package/src/core/runtime-url.js +105 -0
  313. package/src/core/save-core.js +109 -12
  314. package/src/core/save-replace.js +72 -0
  315. package/src/core/save-size-notice.js +134 -0
  316. package/src/core/save-size.js +140 -0
  317. package/src/core/save.js +805 -118
  318. package/src/core/snapshot.js +121 -11
  319. package/src/core/source-map.js +157 -9
  320. package/src/core/unsaved-warning.js +14 -4
  321. package/src/core/upgrade-run.js +408 -0
  322. package/src/core/version.js +18 -0
  323. package/src/lib/assets.js +30 -0
  324. package/src/lib/autosave-debug-gate.js +50 -0
  325. package/src/lib/autosave-scheduler.js +141 -0
  326. package/src/lib/boot-imports.js +8 -0
  327. package/src/lib/derived.js +3 -1
  328. package/src/lib/dirty-gate.js +15 -0
  329. package/src/lib/doc-storage.js +33 -0
  330. package/src/lib/effect.js +9 -4
  331. package/src/lib/mutation.js +4 -2
  332. package/src/lib/offline-runtime.js +226 -0
  333. package/src/lib/preload.js +48 -0
  334. package/src/lib/root-attrs.js +1 -1
  335. package/src/lib/trash.js +18 -0
  336. package/src/lib/undo/capture-move.js +152 -0
  337. package/src/live/local-channel.js +61 -0
  338. package/src/live/local-lane.js +60 -9
  339. package/src/live/server-lane.js +6 -3
  340. package/src/loader-logic.js +76 -15
  341. package/src/loader.js +218 -30
  342. package/src/page/adopt-boot.js +76 -22
  343. package/src/page/adopt.js +25 -4
  344. package/src/page/awareness.js +230 -0
  345. package/src/page/blocks.js +79 -5
  346. package/src/page/blocksel.js +20 -4
  347. package/src/page/board-face.js +13 -0
  348. package/src/page/board-panel.js +5 -17
  349. package/src/page/board-style.js +2 -13
  350. package/src/page/board-view.js +1 -11
  351. package/src/page/board-views.js +17 -0
  352. package/src/page/board.js +2 -2
  353. package/src/page/boot-pack.js +26 -0
  354. package/src/page/boot.js +237 -0
  355. package/src/page/checklist.js +1 -1
  356. package/src/page/chrome.js +411 -61
  357. package/src/page/csv-import.js +17 -2
  358. package/src/page/document-title-dom.js +40 -0
  359. package/src/page/document-title-runtime.js +76 -0
  360. package/src/page/document-title.js +9 -0
  361. package/src/page/drag.js +48 -13
  362. package/src/page/drop.js +2 -1
  363. package/src/page/editor.js +330 -86
  364. package/src/page/feature-loader.js +72 -0
  365. package/src/page/features/board.js +12 -0
  366. package/src/page/features/code.js +13 -0
  367. package/src/page/features/media.js +290 -0
  368. package/src/page/features/registry.js +23 -0
  369. package/src/page/features/table.js +383 -0
  370. package/src/page/{tool-view.js → features/tool.js} +33 -2
  371. package/src/page/format-codec.js +215 -0
  372. package/src/page/format-gate.js +179 -0
  373. package/src/page/format-level.js +157 -0
  374. package/src/page/highlight-slot.js +23 -0
  375. package/src/page/highlight.js +30 -16
  376. package/src/page/history-intent.js +78 -0
  377. package/src/page/inline.js +18 -25
  378. package/src/page/items.js +52 -11
  379. package/src/page/link.js +1 -0
  380. package/src/page/markdown.js +86 -1
  381. package/src/page/media-views.js +3 -0
  382. package/src/page/media.js +33 -245
  383. package/src/page/outline-cache.js +11 -0
  384. package/src/page/placeholder.js +27 -7
  385. package/src/page/projection.js +196 -50
  386. package/src/page/reader-patch.js +306 -0
  387. package/src/page/recover.js +1 -1
  388. package/src/page/rich/page-tool.js +1 -4
  389. package/src/page/runtime-owner.js +27 -0
  390. package/src/page/save-projection.js +7 -4
  391. package/src/page/schema.js +14 -0
  392. package/src/page/table-rect-key.js +3 -0
  393. package/src/page/table-select.js +4 -3
  394. package/src/page/table-style.js +1 -1
  395. package/src/page/table-views.js +3 -0
  396. package/src/page/table.js +8 -293
  397. package/src/page/themes/light-1.css +139 -2
  398. package/src/page/themes/light-1.source.css +269 -0
  399. package/src/page/tool-capability.js +3 -0
  400. package/src/page/tool-views.js +3 -0
  401. package/src/page/view.js +390 -107
  402. package/src/page/ybind/awareness.js +172 -0
  403. package/src/page/ybind/binding.js +9 -1
  404. package/src/page/ybind/bridge.js +50 -47
  405. package/src/page/ybind/decode.js +52 -19
  406. package/src/page/ybind/doc.js +27 -2
  407. package/src/page/ybind/group.js +12 -3
  408. package/src/page/ybind/index.js +2 -0
  409. package/src/page/ybind/lineage.js +71 -0
  410. package/src/page/ybind/marks.js +25 -1
  411. package/src/page/ybind/migrate.js +48 -13
  412. package/src/page/ybind/moves.js +32 -21
  413. package/src/page/ybind/positions.js +50 -0
  414. package/src/page/ybind/provider.js +84 -22
  415. package/src/page/ybind/recovery-proof.js +191 -0
  416. package/src/page/ybind/recovery-replay.js +130 -0
  417. package/src/page/ybind/tokens.js +15 -6
  418. package/src/page/ybind/walk.js +10 -3
  419. package/src/page/ybind/writer.js +48 -4
  420. package/src/plugins/ai-edit.js +167 -32
  421. package/src/plugins/bind-text.js +218 -0
  422. package/src/plugins/cms.js +93 -0
  423. package/src/plugins/indicator.js +57 -5
  424. package/src/plugins/live.js +282 -34
  425. package/src/plugins/menu/claygoo.svg.js +1 -0
  426. package/src/plugins/menu/seo-head.js +323 -0
  427. package/src/plugins/menu/seo.js +849 -0
  428. package/src/plugins/menu.js +425 -0
  429. package/src/plugins/page-view.js +131 -0
  430. package/src/plugins/page.js +1022 -275
  431. package/src/plugins/people.js +158 -31
  432. package/src/plugins/richclay.js +119 -9
  433. package/src/plugins/sortable.js +370 -60
  434. package/src/plugins/source.js +189 -366
  435. package/src/plugins/undo.js +289 -19
  436. package/src/plugins/unsent.js +132 -0
  437. package/src/plugins/upgrade-run.js +1 -0
  438. package/src/plugins/upgrade.js +210 -0
  439. package/src/plugins/upload-task.js +571 -0
  440. package/src/plugins/upload-ui.js +265 -0
  441. package/src/plugins/upload.js +34 -153
  442. package/src/plugins/visitor-drafts/inert.js +32 -0
  443. package/src/plugins/visitor-drafts/owner-chip.js +3 -0
  444. package/src/plugins/visitor-drafts/prompt.js +2 -2
  445. package/src/plugins/visitor-drafts-owner.js +85 -0
  446. package/src/plugins/visitor-drafts.js +31 -42
  447. package/src/plugins/wire.js +82 -1
  448. package/src/reader-core.js +26 -0
  449. package/src/recovery/conflicts.js +287 -0
  450. package/src/recovery/identity.js +154 -0
  451. package/src/recovery/store.js +792 -0
  452. package/src/recovery/undo.js +257 -0
  453. package/src/server/checkpoint/index.js +68 -4
  454. package/src/server/offline-runtime/index.js +214 -0
  455. package/src/server/rooms/index.js +2 -1
  456. package/src/server/rooms/live-data-rooms.js +2 -2
  457. package/src/server/rooms/page-admission.js +33 -0
  458. package/src/server/rooms/page-lineage.js +56 -0
  459. package/src/server/rooms/page-rebase.js +11 -5
  460. package/src/server/rooms/page-state.js +2 -0
  461. package/src/server/rooms/page-write-admission.js +9 -3
  462. package/src/source/session.js +705 -0
  463. package/src/sync/conflict-revert.js +5 -4
  464. package/src/sync/conflicts-edit.js +119 -0
  465. package/src/sync/conflicts-facade.js +49 -0
  466. package/src/sync/conflicts.js +99 -1
  467. package/src/sync/editing-marker.js +37 -6
  468. package/src/sync/editing-sender.js +8 -1
  469. package/src/sync/live-sync-edit.js +63 -0
  470. package/src/sync/live-sync.js +364 -119
  471. package/src/sync/local-presence.js +187 -0
  472. package/src/sync/merge-declarations.js +10 -0
  473. package/src/sync/merge-engine.js +12 -0
  474. package/src/sync/presence-store.js +412 -0
  475. package/src/sync/presence-target.js +243 -0
  476. package/src/sync/presence.js +72 -41
  477. package/src/sync/saved-handoff.js +200 -0
  478. package/src/sync/saved-stream.js +535 -0
  479. package/src/sync/version-guard.js +92 -0
  480. package/src/sync/wire-profile.js +81 -0
  481. package/src/ui/bevel-controls.js +2 -8
  482. package/src/ui/page-scheme.js +7 -0
  483. package/src/ui/skins/cms.js +1 -1
  484. package/src/ui/theme-config.js +150 -0
  485. package/src/ui/theme.js +6 -138
  486. package/src/ui/toast-lazy.js +11 -0
  487. package/src/vendor/LICENSES-optional.md +253 -0
  488. package/src/vendor/highlight.vendor.js +3 -0
  489. package/src/vendor/hljs-bash.vendor.js +2 -0
  490. package/src/vendor/hljs-css.vendor.js +2 -0
  491. package/src/vendor/hljs-javascript.vendor.js +2 -0
  492. package/src/vendor/hljs-json.vendor.js +2 -0
  493. package/src/vendor/hljs-python.vendor.js +2 -0
  494. package/src/vendor/hljs-xml.vendor.js +2 -0
  495. package/src/vendor/hyper-html-api-data.vendor.js +1 -1
  496. package/src/vendor/hyper-html-api-upgrade.vendor.js +5 -0
  497. package/src/vendor/hyper-morph-identity.vendor.js +2 -0
  498. package/src/vendor/hyper-morph-json.vendor.js +3 -0
  499. package/src/vendor/hyper-morph.vendor.js +6 -6
  500. package/src/vendor/hyper-undo.vendor.js +1 -1
  501. package/src/vendor/hypercms.vendor.js +169 -84
  502. package/src/vendor/marked.vendor.js +68 -0
  503. package/src/vendor/overtype.vendor.js +994 -0
  504. package/src/vendor/richclay.vendor.js +17 -17
  505. package/src/vendor/sap-core.vendor.js +5 -5
  506. package/src/vendor/sap-ownership.vendor.js +11 -7
@@ -0,0 +1,772 @@
1
+ /**
2
+ * recovery.js — `clay.recovery`, the shared local recovery service.
3
+ *
4
+ * One store per document and mode, and one API every edit-mode document can use:
5
+ * an app registers a channel for the records it can journal and gets back an object
6
+ * whose methods work immediately. The core here is the queue and the status; the
7
+ * IndexedDB implementation and the identity that scopes it are imported only when a
8
+ * channel registers, so a page that journals nothing downloads neither, and a reader
9
+ * never opens the database at all.
10
+ *
11
+ * This is a recovery copy, not a save. Nothing here is allowed to become proof that
12
+ * the host accepted anything: a completed transaction means this browser kept a
13
+ * copy, and that is all it means. HTML stays the saved source, and a page never
14
+ * renders from these records.
15
+ *
16
+ * A document is covered only for the channels its adapters register. There is no
17
+ * whole-document observer behind this, by design.
18
+ */
19
+
20
+ import { mode as pageMode } from "./is-edit-mode.js";
21
+
22
+ // Replaceable text is debounced after typing stops, and forced out after five
23
+ // seconds of continuous typing so a fast writer is never more than that behind.
24
+ const DEBOUNCE_MS = 500;
25
+ const MAX_INTERVAL_MS = 5000;
26
+
27
+ const TEXT = "text";
28
+
29
+ // Compaction thresholds: 64 batch records or 1 MiB of batch bytes, whichever comes
30
+ // first, merged into the writer's capsule. A merge is never in the typing path.
31
+ const COMPACT_BATCHES = 64;
32
+ const COMPACT_BYTES = 1024 * 1024;
33
+
34
+ // How much uncommitted work one channel holds in memory when the database refuses a
35
+ // write. Refusing a further append is loud and visible; dropping an older one is not.
36
+ const MAX_QUEUED_BATCHES = 256;
37
+ const MAX_QUEUED_BYTES = 1024 * 1024;
38
+
39
+ function pageWriterId() {
40
+ const minted = globalThis.crypto?.randomUUID?.();
41
+ if (minted) return minted;
42
+ return `wr-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
43
+ }
44
+
45
+ function recordKey(id) {
46
+ return Array.isArray(id) ? id.join("\u0000") : String(id);
47
+ }
48
+
49
+ function sameId(left, right) {
50
+ if (Array.isArray(left) && Array.isArray(right)) {
51
+ return left.length === right.length && left.every((part, at) => part === right[at]);
52
+ }
53
+ return left === right;
54
+ }
55
+
56
+ function mintedId(revision) {
57
+ return globalThis.crypto?.randomUUID?.() ?? `rec-${revision}-${Math.random().toString(36).slice(2)}`;
58
+ }
59
+
60
+ function copyPayload(value) {
61
+ if (value instanceof Uint8Array) return new Uint8Array(value);
62
+ if (value instanceof ArrayBuffer) return value.slice(0);
63
+ return value;
64
+ }
65
+
66
+ /**
67
+ * Is this saved payload exactly the writing that was staged?
68
+ *
69
+ * Both sides use the adapter's fixed field order and every field is compared as it
70
+ * stands, so a body that differs anywhere is different writing. Nothing is trimmed,
71
+ * normalized or parsed: a save proves it holds these exact bytes or it proves
72
+ * nothing, and a near match is what would delete the only copy of something.
73
+ *
74
+ * @param {*} saved - what the adapter read out of the accepted HTML
75
+ * @param {*} staged - what the caller staged
76
+ * @returns {boolean}
77
+ */
78
+ function payloadsMatch(saved, staged) {
79
+ if (saved === staged) return true;
80
+ if (!saved || !staged || typeof saved !== "object" || typeof staged !== "object") return false;
81
+ if (saved instanceof Uint8Array || staged instanceof Uint8Array) {
82
+ if (!(saved instanceof Uint8Array) || !(staged instanceof Uint8Array)) return false;
83
+ if (saved.length !== staged.length) return false;
84
+ for (let at = 0; at < saved.length; at++) if (saved[at] !== staged[at]) return false;
85
+ return true;
86
+ }
87
+ const left = Object.keys(staged);
88
+ const right = Object.keys(saved);
89
+ if (left.length !== right.length) return false;
90
+ for (let at = 0; at < left.length; at++) {
91
+ if (left[at] !== right[at]) return false;
92
+ if (staged[left[at]] !== saved[right[at]]) return false;
93
+ }
94
+ return true;
95
+ }
96
+
97
+ // Every service in this page, so a save can hand its capture to each of them
98
+ // without save.js importing one particular instance. A service with no channel
99
+ // registered has no candidates and answers without touching the HTML.
100
+ const services = new Set();
101
+
102
+ /**
103
+ * Pin what one save capture's exact bytes cover, before the request goes out.
104
+ *
105
+ * Called by save.js with the capture's own serialized bytes and its id. Nothing is
106
+ * acknowledged here: only the settled result of this very capture can clear a copy.
107
+ *
108
+ * @param {{captureId: *, html: string}} capture
109
+ */
110
+ export function noteRecoveryCapture({ captureId, html } = {}) {
111
+ if (!services.size) return;
112
+ for (const service of services) service.capture(captureId, html);
113
+ }
114
+
115
+ /**
116
+ * Report how a capture settled.
117
+ *
118
+ * Only a confirmed success acknowledges, and only for the capture whose exact bytes
119
+ * covered a revision. A skip, a refusal, an error and an unknown outcome release the
120
+ * capture and clear nothing.
121
+ *
122
+ * @param {{captureId: *, ok: boolean, etag: ?string}} settled
123
+ */
124
+ export function noteRecoverySettled({ captureId, ok = false, etag = null } = {}) {
125
+ if (!services.size) return;
126
+ for (const service of services) service.settle(captureId, ok === true, etag);
127
+ }
128
+
129
+ /**
130
+ * Build one recovery service.
131
+ *
132
+ * The returned object is what the loader publishes as `clay.recovery`. Its own
133
+ * state is per page load: the writer id, the open database and the queue. Tests
134
+ * and, later, an embedded caller can build another one without touching this page's.
135
+ *
136
+ * @param {Object} [options]
137
+ * @param {string} [options.mode] - "edit" or "draft"
138
+ * @param {string} [options.writerId]
139
+ * @param {?IDBFactory} [options.indexedDB]
140
+ * @param {number} [options.maxScopeBytes]
141
+ * @param {number} [options.maxDbBytes]
142
+ * @param {Function} [options.now]
143
+ * @returns {Object}
144
+ */
145
+ export function createRecovery({
146
+ mode = pageMode === "draft" ? "draft" : "edit",
147
+ writerId = pageWriterId(),
148
+ indexedDB,
149
+ maxScopeBytes,
150
+ maxDbBytes,
151
+ now,
152
+ } = {}) {
153
+ const channels = new Map();
154
+ const listeners = new Set();
155
+ const captures = new Map();
156
+ let prepared = null;
157
+ let identity = null;
158
+ let store = null;
159
+ let status = { ok: true, code: null, durable: false };
160
+
161
+ function snapshot() {
162
+ return {
163
+ ok: status.ok,
164
+ code: status.code,
165
+ durable: status.durable,
166
+ usage: store?.status().usage ?? null,
167
+ identityMismatch: identity ? identity.check().ok === false : false,
168
+ };
169
+ }
170
+
171
+ function announce(entry) {
172
+ const detail = {
173
+ channel: entry.channel,
174
+ status: snapshot(),
175
+ count: entry.pending.size,
176
+ revisions: [...entry.pending.values()].map(({ id, revision }) => ({ id, revision })),
177
+ };
178
+ for (const fn of entry.listeners) {
179
+ try {
180
+ fn(detail);
181
+ } catch (_) {}
182
+ }
183
+ for (const fn of listeners) {
184
+ try {
185
+ fn(detail);
186
+ } catch (_) {}
187
+ }
188
+ }
189
+
190
+ // Once per failure episode, and once when it clears. A page whose storage is gone
191
+ // must not queue a console line per keystroke, and a warning may only clear after
192
+ // a transaction actually completed.
193
+ function note(result) {
194
+ if (result && result.ok) {
195
+ const wasOk = status.ok;
196
+ status = { ok: true, code: null, durable: true };
197
+ if (!wasOk) for (const entry of channels.values()) announce(entry);
198
+ return;
199
+ }
200
+ const code = result?.code || "error";
201
+ if (status.ok || status.code !== code) {
202
+ status = { ok: false, code, durable: false };
203
+ for (const entry of channels.values()) announce(entry);
204
+ return;
205
+ }
206
+ status = { ok: false, code, durable: false };
207
+ }
208
+
209
+ async function prepare() {
210
+ const [{ createIdentity }, { createStore }] = await Promise.all([
211
+ import("../recovery/identity.js"),
212
+ import("../recovery/store.js"),
213
+ ]);
214
+ identity = createIdentity({ mode, writerId });
215
+ store = createStore({ scope: identity.scope, indexedDB, maxScopeBytes, maxDbBytes, now });
216
+ const opened = await store.open();
217
+ // Reaching the database is not a durable copy. Only a completed write is.
218
+ if (opened.ok) status = { ok: true, code: null, durable: false };
219
+ else note(opened);
220
+ return opened;
221
+ }
222
+
223
+ function ensurePrepared() {
224
+ if (!prepared) {
225
+ prepared = prepare().catch((err) => ({ ok: false, code: err?.name === "DataCloneError" ? "uncloneable" : "unavailable" }));
226
+ }
227
+ return prepared;
228
+ }
229
+
230
+ // What this service holds that one capture's exact bytes could cover. The saved
231
+ // map is read once per channel per capture, and only when this service has a copy
232
+ // that could be covered at all.
233
+ function coverageOf(entry, html) {
234
+ let saved;
235
+ try {
236
+ saved = entry.readSaved(html);
237
+ } catch (_) {
238
+ return [];
239
+ }
240
+ if (!saved || typeof saved.get !== "function") return [];
241
+ const covered = [];
242
+ for (const record of entry.held.values()) {
243
+ if (record.kind !== TEXT) continue;
244
+ if (!saved.has(record.key)) continue;
245
+ if (!payloadsMatch(saved.get(record.key), record.payload)) continue;
246
+ covered.push({ id: record.id, revision: record.revision });
247
+ }
248
+ return covered;
249
+ }
250
+
251
+ function capture(captureId, html) {
252
+ if (captureId == null || typeof html !== "string") return;
253
+ const covered = new Map();
254
+ for (const entry of channels.values()) {
255
+ if (typeof entry.readSaved !== "function" || !entry.held.size) continue;
256
+ const list = coverageOf(entry, html);
257
+ if (list.length) covered.set(entry.channel, list);
258
+ }
259
+ if (covered.size) captures.set(captureId, covered);
260
+ }
261
+
262
+ async function acknowledgeCovered(entry, covered, etag, captureId) {
263
+ const opened = await ensurePrepared();
264
+ if (!opened.ok) return;
265
+ for (const { id, revision } of covered) {
266
+ const receipt = await store.acknowledge(id, { revision, etag, captureId });
267
+ note(receipt);
268
+ const slot = recordKey(id);
269
+ const held = entry.held.get(slot);
270
+ if (held && held.revision <= revision) entry.held.delete(slot);
271
+ }
272
+ announce(entry);
273
+ }
274
+
275
+ // A settled save is the only thing that clears anything, and it clears exactly
276
+ // what its own capture covered. The work joins the channel's write queue, so an
277
+ // acknowledgement is ordered against every put this service has already queued:
278
+ // a copy that reaches the database late meets the watermark it was refused by.
279
+ function settle(captureId, ok, etag) {
280
+ const covered = captures.get(captureId);
281
+ if (!covered) return Promise.resolve();
282
+ captures.delete(captureId);
283
+ if (!ok) return Promise.resolve();
284
+ const waits = [];
285
+ for (const [channel, list] of covered) {
286
+ const entry = channels.get(channel);
287
+ if (!entry) continue;
288
+ const run = () => acknowledgeCovered(entry, list, etag, captureId);
289
+ entry.writing = entry.writing.then(run, run);
290
+ waits.push(entry.writing);
291
+ }
292
+ return Promise.all(waits).then(() => undefined, () => undefined);
293
+ }
294
+
295
+ function schedule(entry) {
296
+ const at = now?.() ?? Date.now();
297
+ if (!entry.burstStart) entry.burstStart = at;
298
+ const wait = Math.max(0, Math.min(DEBOUNCE_MS, entry.burstStart + MAX_INTERVAL_MS - at));
299
+ if (entry.timer) clearTimeout(entry.timer);
300
+ entry.timer = setTimeout(() => {
301
+ entry.timer = null;
302
+ entry.burstStart = 0;
303
+ flush(entry).catch(() => {});
304
+ }, wait);
305
+ }
306
+
307
+ function stage(entry, { key, kind = TEXT, baseEtag = null, base = null, payload = null, lineage = null } = {}) {
308
+ const name = key == null ? "" : String(key);
309
+ if (!name) throw new TypeError("clay.recovery.stage needs a key");
310
+ const at = now?.() ?? Date.now();
311
+ const revision = ++entry.revision;
312
+ const id = kind === TEXT ? [entry.channel, name, writerId] : (globalThis.crypto?.randomUUID?.() ?? `rec-${revision}-${Math.random().toString(36).slice(2)}`);
313
+ const slot = recordKey(id);
314
+ const previous = entry.pending.get(slot);
315
+ const record = {
316
+ id,
317
+ kind,
318
+ channel: entry.channel,
319
+ version: entry.version,
320
+ key: name,
321
+ writerId,
322
+ revision,
323
+ status: "journaled",
324
+ baseEtag,
325
+ base: copyPayload(base),
326
+ payload: copyPayload(payload),
327
+ lineage,
328
+ created: previous?.created ?? at,
329
+ updated: at,
330
+ };
331
+ entry.pending.set(slot, record);
332
+ entry.held.set(slot, { id, kind, key: name, revision, payload: record.payload });
333
+ schedule(entry);
334
+ announce(entry);
335
+ return { id, revision };
336
+ }
337
+
338
+ async function writePending(entry) {
339
+ const batch = [...entry.pending.values()];
340
+ if (!batch.length) return { ok: true, stored: [] };
341
+ const opened = await ensurePrepared();
342
+ if (!opened.ok) return { ok: false, code: opened.code };
343
+ const stored = [];
344
+ let failure = null;
345
+ for (const record of batch) {
346
+ const receipt = await store.put(record);
347
+ if (receipt.ok) {
348
+ const slot = recordKey(record.id);
349
+ if (entry.pending.get(slot) === record) entry.pending.delete(slot);
350
+ if (receipt.skipped) {
351
+ const held = entry.held.get(slot);
352
+ if (held && held.revision === record.revision) entry.held.delete(slot);
353
+ } else {
354
+ stored.push({ id: record.id, revision: record.revision });
355
+ }
356
+ } else {
357
+ failure = failure || receipt.code || "error";
358
+ }
359
+ }
360
+ note(failure ? { ok: false, code: failure } : { ok: true });
361
+ announce(entry);
362
+ return failure ? { ok: false, code: failure } : { ok: true, stored };
363
+ }
364
+
365
+ // --- the binary channel -------------------------------------------------------
366
+ //
367
+ // A page channel journals Yjs updates as bytes: one immutable capsule per writer
368
+ // and key carrying the episode's base, its received dependencies and its owned
369
+ // local work, plus one small batch per append. Nothing here knows what the bytes
370
+ // mean; the merge and the checkpoint proof come from the registration.
371
+
372
+ function binaryOf(entry, key) {
373
+ return [entry.channel, key, writerId];
374
+ }
375
+
376
+ function capsuleFor(entry, capsuleId, context, at, base) {
377
+ return {
378
+ id: capsuleId,
379
+ kind: "binary",
380
+ channel: entry.channel,
381
+ version: entry.version,
382
+ writerId,
383
+ revision: 0,
384
+ context,
385
+ payload: {
386
+ base: base ?? new Uint8Array(0),
387
+ dependencies: new Uint8Array(0),
388
+ owned: new Uint8Array(0),
389
+ },
390
+ acknowledged: null,
391
+ created: at,
392
+ updated: at,
393
+ };
394
+ }
395
+
396
+ function queueBatch(entry, record, capsule) {
397
+ if (entry.batchQueue.size >= MAX_QUEUED_BATCHES) return false;
398
+ if (entry.queuedBytes + record.payload.byteLength > MAX_QUEUED_BYTES) return false;
399
+ entry.batchQueue.set(recordKey(record.id), { record, capsule });
400
+ entry.queuedBytes += record.payload.byteLength;
401
+ return true;
402
+ }
403
+
404
+ async function writeBatchQueue(entry) {
405
+ if (!entry.batchQueue.size) return { ok: true };
406
+ const opened = await ensurePrepared();
407
+ if (!opened.ok) return { ok: false, code: opened.code };
408
+ const stored = [];
409
+ let failure = null;
410
+ for (const [slot, item] of [...entry.batchQueue.entries()]) {
411
+ const receipt = await store.appendBinary(item.record, item.capsule);
412
+ if (!receipt.ok) {
413
+ failure = failure || receipt.code || "error";
414
+ continue;
415
+ }
416
+ entry.batchQueue.delete(slot);
417
+ entry.queuedBytes = Math.max(0, entry.queuedBytes - item.record.payload.byteLength);
418
+ if (receipt.skipped) continue;
419
+ entry.batches += 1;
420
+ entry.batchBytes += item.record.payload.byteLength;
421
+ stored.push({ id: item.record.id, revision: item.record.revision });
422
+ }
423
+ note(failure ? { ok: false, code: failure } : { ok: true });
424
+ announce(entry);
425
+ return failure ? { ok: false, code: failure } : { ok: true, stored };
426
+ }
427
+
428
+ /**
429
+ * Journal one binary update, and resolve only once it is on disk.
430
+ *
431
+ * The id, the revision and the bytes are fixed before the first await, so a caller
432
+ * that reuses its buffer cannot change what was handed over, and a write that fails
433
+ * keeps exactly those bytes for the next attempt. Failure is reported, never thrown:
434
+ * the caller still holds the live document and has to say it is not durable.
435
+ */
436
+ async function append(entry, { key, context = null, update, origin = "local", base = null } = {}) {
437
+ const name = key == null ? "" : String(key);
438
+ if (!name) throw new TypeError("clay.recovery.append needs a key");
439
+ if (!(update instanceof Uint8Array) && !(update instanceof ArrayBuffer)) {
440
+ throw new TypeError("clay.recovery.append needs binary bytes");
441
+ }
442
+ const at = now?.() ?? Date.now();
443
+ const revision = ++entry.revision;
444
+ const capsuleId = binaryOf(entry, name);
445
+ const record = {
446
+ id: mintedId(revision),
447
+ kind: "binary-batch",
448
+ channel: entry.channel,
449
+ version: entry.version,
450
+ writerId,
451
+ revision,
452
+ capsuleId,
453
+ origin: origin === "dependency" ? "dependency" : "local",
454
+ context,
455
+ payload: copyPayload(update),
456
+ updated: at,
457
+ };
458
+ const capsule = capsuleFor(entry, capsuleId, context, at, base == null ? null : copyPayload(base));
459
+ const opened = await ensurePrepared();
460
+ if (!opened.ok) {
461
+ if (!queueBatch(entry, record, capsule)) return { ok: false, code: "over-budget" };
462
+ return { ok: false, code: opened.code };
463
+ }
464
+ const receipt = await store.appendBinary(record, capsule);
465
+ note(receipt);
466
+ announce(entry);
467
+ if (!receipt.ok) {
468
+ if (!queueBatch(entry, record, capsule)) return { ok: false, code: receipt.code };
469
+ return { ok: false, code: receipt.code };
470
+ }
471
+ entry.batches += 1;
472
+ entry.batchBytes += record.payload.byteLength;
473
+ if (receipt.skipped) return { ok: true, id: record.id, revision, skipped: true };
474
+ planCompaction(entry);
475
+ return { ok: true, id: record.id, revision };
476
+ }
477
+
478
+ /**
479
+ * Record that a room accepted the writer's work: metadata only, no bytes moved.
480
+ */
481
+ async function markAcknowledged(entry, { records = [], authority = null, epoch = null, sequence = null } = {}) {
482
+ const observed = (records || []).filter((record) => record && record.id !== undefined && typeof record.revision === "number");
483
+ if (!observed.length) return { ok: true, acknowledged: 0 };
484
+ const opened = await ensurePrepared();
485
+ if (!opened.ok) return { ok: false, code: opened.code };
486
+ const receipt = await store.acknowledgeBinary({
487
+ channel: entry.channel,
488
+ version: entry.version,
489
+ writerId,
490
+ records: observed,
491
+ authority,
492
+ epoch,
493
+ sequence,
494
+ });
495
+ note(receipt);
496
+ announce(entry);
497
+ return receipt.ok ? { ok: true, acknowledged: receipt.acknowledged } : { ok: false, code: receipt.code };
498
+ }
499
+
500
+ /**
501
+ * Retire records a proof covers.
502
+ *
503
+ * The validator runs here, before any transaction is opened and never inside one,
504
+ * and may only narrow the caller's own list: an id and revision the caller did not
505
+ * observe can never be retired, whoever returns it. Without a validator nothing is
506
+ * retired, because nothing has been proved.
507
+ */
508
+ async function checkpoint(entry, { records = [], proof = null } = {}) {
509
+ const observed = (records || []).filter((record) => record && record.id !== undefined && typeof record.revision === "number");
510
+ if (!observed.length) return { ok: true, retired: [] };
511
+ if (typeof entry.validateCheckpoint !== "function") return { ok: true, retired: [] };
512
+ let answer;
513
+ try {
514
+ answer = await entry.validateCheckpoint(proof, observed.map(({ id, revision }) => ({ id, revision })));
515
+ } catch (_) {
516
+ return { ok: false, code: "invalid", retired: [] };
517
+ }
518
+ if (!Array.isArray(answer)) return { ok: false, code: "invalid", retired: [] };
519
+ const covered = observed.filter((record) =>
520
+ answer.some((item) => item && item.revision === record.revision && sameId(item.id, record.id))
521
+ );
522
+ if (!covered.length) return { ok: true, retired: [] };
523
+ const opened = await ensurePrepared();
524
+ if (!opened.ok) return { ok: false, code: opened.code, retired: [] };
525
+ const receipt = await store.checkpointBinary({
526
+ channel: entry.channel,
527
+ version: entry.version,
528
+ writerId,
529
+ records: covered,
530
+ });
531
+ note(receipt);
532
+ announce(entry);
533
+ return receipt.ok ? { ok: true, retired: receipt.retired } : { ok: false, code: receipt.code, retired: [] };
534
+ }
535
+
536
+ function withBytes(value) {
537
+ return value && typeof value.byteLength === "number" && value.byteLength > 0 ? value : null;
538
+ }
539
+
540
+ /**
541
+ * Merge the observed batches into their capsule, outside any transaction, then swap.
542
+ *
543
+ * A swap that lost the head re-reads everything and tries again, so a compaction
544
+ * that raced an append or another compaction never deletes input it did not merge.
545
+ */
546
+ async function swapBatches(entry, observed) {
547
+ for (let tries = 0; tries < 3; tries++) {
548
+ let capsuleId = null;
549
+ const inputs = [];
550
+ for (const { id, revision } of observed) {
551
+ const read = await store.get(id);
552
+ if (!read.ok || !read.record) continue;
553
+ const record = read.record;
554
+ if (record.revision !== revision || record.kind !== "binary-batch" || !record.capsuleId) continue;
555
+ if (!capsuleId) capsuleId = record.capsuleId;
556
+ if (!sameId(capsuleId, record.capsuleId)) continue;
557
+ inputs.push(record);
558
+ }
559
+ if (!capsuleId || !inputs.length) return { ok: true, retired: [] };
560
+ const head = await store.get(capsuleId);
561
+ if (!head.ok || !head.record) return { ok: false, code: "missing" };
562
+ const capsule = head.record;
563
+ const owned = [withBytes(capsule.payload?.owned), ...inputs.filter((record) => record.origin !== "dependency").map((record) => withBytes(record.payload))].filter(Boolean);
564
+ const dependencies = [
565
+ withBytes(capsule.payload?.dependencies),
566
+ ...inputs.filter((record) => record.origin === "dependency").map((record) => withBytes(record.payload)),
567
+ ].filter(Boolean);
568
+ const mergedOwned = owned.length ? copyPayload(await entry.merge(owned)) : new Uint8Array(0);
569
+ const mergedDependencies = dependencies.length ? copyPayload(await entry.merge(dependencies)) : new Uint8Array(0);
570
+ const revision = Math.max(entry.revision, capsule.revision) + 1;
571
+ entry.revision = revision;
572
+ const swapped = await store.compactBinary({
573
+ channel: entry.channel,
574
+ version: entry.version,
575
+ capsuleId,
576
+ expectedRevision: capsule.revision,
577
+ replacement: {
578
+ revision,
579
+ updated: now?.() ?? Date.now(),
580
+ payload: {
581
+ base: capsule.payload?.base ?? new Uint8Array(0),
582
+ dependencies: mergedDependencies,
583
+ owned: mergedOwned,
584
+ },
585
+ },
586
+ inputs: inputs.map((record) => ({ id: record.id, revision: record.revision })),
587
+ });
588
+ if (swapped.ok) {
589
+ entry.batches = 0;
590
+ entry.batchBytes = 0;
591
+ return { ok: true, retired: swapped.retired, revision: swapped.revision };
592
+ }
593
+ if (swapped.code !== "stale") return { ok: false, code: swapped.code };
594
+ }
595
+ return { ok: false, code: "stale" };
596
+ }
597
+
598
+ async function compact(entry, { records = [] } = {}) {
599
+ if (typeof entry.merge !== "function") return { ok: false, code: "invalid", retired: [] };
600
+ const observed = (records || []).filter((record) => record && record.id !== undefined && typeof record.revision === "number");
601
+ if (!observed.length) return { ok: true, retired: [] };
602
+ const opened = await ensurePrepared();
603
+ if (!opened.ok) return { ok: false, code: opened.code, retired: [] };
604
+ const receipt = await swapBatches(entry, observed);
605
+ note(receipt);
606
+ announce(entry);
607
+ return receipt.ok ? { ok: true, retired: receipt.retired } : { ok: false, code: receipt.code, retired: [] };
608
+ }
609
+
610
+ function planCompaction(entry) {
611
+ if (typeof entry.merge !== "function" || entry.compaction) return;
612
+ if (entry.batches < COMPACT_BATCHES && entry.batchBytes < COMPACT_BYTES) return;
613
+ entry.compaction = (async () => {
614
+ const listed = await store.list();
615
+ if (!listed.ok) return;
616
+ const mine = listed.records.filter(
617
+ (record) => record.channel === entry.channel && record.kind === "binary-batch" && record.writerId === writerId
618
+ );
619
+ if (!mine.length) return;
620
+ await compact(entry, { records: mine.map(({ id, revision }) => ({ id, revision })) });
621
+ })()
622
+ .catch(() => {})
623
+ .finally(() => {
624
+ entry.compaction = null;
625
+ });
626
+ }
627
+
628
+ function flush(entry) {
629
+ if (entry.timer) clearTimeout(entry.timer);
630
+ entry.timer = null;
631
+ entry.burstStart = 0;
632
+ const run = async () => {
633
+ const queued = await writeBatchQueue(entry);
634
+ if (!queued.ok) return queued;
635
+ return writePending(entry);
636
+ };
637
+ entry.writing = entry.writing.then(run, run);
638
+ return entry.writing;
639
+ }
640
+
641
+ async function list(entry) {
642
+ const opened = await ensurePrepared();
643
+ if (!opened.ok) return { ok: false, code: opened.code, records: [], status: snapshot() };
644
+ const result = await store.list();
645
+ note(result);
646
+ if (!result.ok) return { ok: false, code: result.code, records: [], status: snapshot() };
647
+ const records = result.records
648
+ .filter((record) => record.channel === entry.channel)
649
+ .map((record) => ({ ...record, supported: record.version === entry.version }));
650
+ return { ok: true, records, status: snapshot() };
651
+ }
652
+
653
+ async function dismiss(entry, id, { revision } = {}) {
654
+ const opened = await ensurePrepared();
655
+ if (!opened.ok) return { ok: false, code: opened.code };
656
+ if (entry.timer) clearTimeout(entry.timer);
657
+ entry.timer = null;
658
+ entry.burstStart = 0;
659
+ await entry.writing;
660
+ const slot = recordKey(id);
661
+ const queued = entry.pending.get(slot);
662
+ if (queued && (revision === undefined || queued.revision === revision)) entry.pending.delete(slot);
663
+ const held = entry.held.get(slot);
664
+ if (held && (revision === undefined || held.revision === revision)) entry.held.delete(slot);
665
+ const receipt = await store.remove(id, { revision });
666
+ note(receipt);
667
+ announce(entry);
668
+ return receipt.ok ? { ok: true, id, revision } : { ok: false, code: receipt.code };
669
+ }
670
+
671
+ function subscribe(entry, fn) {
672
+ entry.listeners.add(fn);
673
+ return () => entry.listeners.delete(fn);
674
+ }
675
+
676
+ function dispose(entry) {
677
+ if (entry.timer) clearTimeout(entry.timer);
678
+ entry.timer = null;
679
+ entry.burstStart = 0;
680
+ const flushed = flush(entry).catch(() => ({ ok: false, code: "aborted" }));
681
+ entry.listeners.clear();
682
+ channels.delete(entry.channel);
683
+ if (!channels.size) services.delete(handle);
684
+ return flushed;
685
+ }
686
+
687
+ const handle = { capture, settle };
688
+
689
+ function channelApi(entry) {
690
+ return {
691
+ channel: entry.channel,
692
+ version: entry.version,
693
+ stage: (spec) => stage(entry, spec),
694
+ flush: () => flush(entry),
695
+ list: () => list(entry),
696
+ dismiss: (id, options) => dismiss(entry, id, options),
697
+ append: (spec) => append(entry, spec),
698
+ markAcknowledged: (spec) => markAcknowledged(entry, spec),
699
+ checkpoint: (spec) => checkpoint(entry, spec),
700
+ compact: (spec) => compact(entry, spec),
701
+ subscribe: (fn) => subscribe(entry, fn),
702
+ dispose: () => dispose(entry),
703
+ };
704
+ }
705
+
706
+ /**
707
+ * Take responsibility for one kind of record in this document.
708
+ *
709
+ * Returns synchronously, before anything asynchronous has happened: the queue is
710
+ * in memory from the first `stage`. A channel is a name, a version and, for text,
711
+ * the two functions that let a review compare stored writing with a saved file
712
+ * without a second implementation of the entry format.
713
+ *
714
+ * @param {Object} spec
715
+ * @param {string} spec.channel
716
+ * @param {number} [spec.version]
717
+ * @param {Function} [spec.readSaved] - (html) => Map<key, payload>
718
+ * @param {Function} [spec.describe] - (record) => {label, text}
719
+ * @param {Function} [spec.validateCheckpoint] - (proof, records) => the subset a proof covers
720
+ * @param {Function} [spec.merge] - (Uint8Array[]) => Uint8Array, the channel's own merge
721
+ * @returns {Object}
722
+ */
723
+ function register({
724
+ channel,
725
+ version = 1,
726
+ readSaved = null,
727
+ describe = null,
728
+ validateCheckpoint = null,
729
+ merge = null,
730
+ } = {}) {
731
+ if (!channel || typeof channel !== "string") throw new TypeError("clay.recovery.register needs a channel name");
732
+ const existing = channels.get(channel);
733
+ if (existing) {
734
+ throw new Error(`clay.recovery: the "${channel}" channel is already registered at version ${existing.version}`);
735
+ }
736
+ const entry = {
737
+ channel,
738
+ version,
739
+ readSaved,
740
+ describe,
741
+ validateCheckpoint,
742
+ merge,
743
+ revision: 0,
744
+ pending: new Map(),
745
+ held: new Map(),
746
+ batchQueue: new Map(),
747
+ queuedBytes: 0,
748
+ batches: 0,
749
+ batchBytes: 0,
750
+ compaction: null,
751
+ timer: null,
752
+ burstStart: 0,
753
+ writing: Promise.resolve(),
754
+ listeners: new Set(),
755
+ };
756
+ channels.set(channel, entry);
757
+ services.add(handle);
758
+ ensurePrepared();
759
+ return channelApi(entry);
760
+ }
761
+
762
+ return {
763
+ register,
764
+ status: snapshot,
765
+ subscribe(fn) {
766
+ listeners.add(fn);
767
+ return () => listeners.delete(fn);
768
+ },
769
+ };
770
+ }
771
+
772
+ export const recovery = createRecovery();