@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
@@ -35,35 +35,125 @@
35
35
  * INTEGRATES WITH: snapshot.js (receives snapshot-ready events)
36
36
  */
37
37
 
38
- import { HyperMorph, morph } from "../vendor/hyper-morph.vendor.js";
39
-
40
- const { createIdentityStore } = HyperMorph;
38
+ import { createIdentityStore } from "../vendor/hyper-morph-identity.vendor.js";
39
+ import { loadedEngine, mergeEngine } from "./merge-engine.js";
41
40
  import Mutation from "../lib/mutation.js";
42
41
  import { NO_DIRTY_SELECTOR, LIVE_SELECTOR } from "../lib/region-policy.js";
43
42
  import { EXTENSION_NODE_SELECTOR, EXTENSION_ATTR_PATTERN } from '../lib/extension-noise.js';
44
- import { isEditMode } from "../core/is-edit-mode.js";
45
- import { mergeTagRecognizers } from "./merge-tags.js";
46
- import { serializeForSync, captureForComparisonAndDirty, captureSnapshot, originalSnapshotNode, captureForMerge, captureForSaveAndComparison } from '../core/snapshot.js';
43
+ import { isEditMode, modeState } from "../core/is-edit-mode.js";
44
+ import { attributeMerge } from "./merge-declarations.js";
45
+ import { serializeForSync, captureForComparisonAndDirty, canonicalizeReceived, captureSnapshot, originalSnapshotNode, captureForMerge, captureForSaveAndComparison } from '../core/snapshot.js';
47
46
  import { mergeIgnoresAttr, mergeVetoesAttr } from '../lib/root-attrs.js';
48
47
  import { autosaveActive } from '../lib/autosave-state.js';
49
- import { enableContentEditable } from '../core/admin-contenteditable.js';
50
- import { enableOnClick } from '../core/admin-onclick.js';
51
- import { enableAdminInputs } from '../core/admin-inputs.js';
52
- import { enableAdminResources } from '../core/admin-resources.js';
53
- import { presence } from './presence.js';
54
- import { editingMarker } from './editing-marker.js';
55
- import { hostMeta, hostSupports } from '../core/host-meta.js';
56
- import { EditingSender } from './editing-sender.js';
48
+ import { hostSupports } from '../core/host-meta.js';
49
+ import { PAGE_FORMAT, PAGE_FORMAT_HEADER } from '../core/page-format-wire.js';
57
50
  import { servedDocumentEtag } from '../core/host-attrs.js';
58
51
  import { recordEtag, seedEtag, lastSeenEtag, conditionalSaves, representedEtag, forgetRepresentedEtag } from '../core/etag.js';
59
52
  import { pageMaybeDirty, pauseGate, resumeGate, gateCaptureToken, gateClearIfUnchanged } from '../lib/dirty-gate.js';
60
53
  import { gestureSeen } from '../lib/user-gesture.js';
61
54
  import { SyncStream } from './stream.js';
62
55
  import { streamOpened, streamClosed, streamEvent, LIVE_EVENTS } from '../live/stream-bridge.js';
63
- import { conflicts, beginApply, completeApply, failApply } from './conflicts.js';
64
- import { beginLineageCapture } from './conflict-footprints.js';
56
+ import { conflicts, conflictLedger, loadConflicts } from './conflicts-facade.js';
57
+ import { upgradeReplaces, stopForUpgrade } from './version-guard.js';
65
58
  import { isDerivedAttr, ownsDerivedText, stripDerived, repaintDerived, paintSignatureOfHtml, derivedActive } from '../lib/derived.js';
66
59
 
60
+ // The edit-only half of live sync (save, admin and editing-sender code) is a
61
+ // separate chunk. Edit mode starts it as live-sync evaluates, so its modules
62
+ // stand where the old static imports left them before the first frame or save,
63
+ // and view mode never fetches it.
64
+ let edit = null;
65
+ let editLoad = null;
66
+
67
+ function loadEdit() {
68
+ editLoad ??= new Promise((resolve) => setTimeout(resolve, 0))
69
+ .then(() => import('./live-sync-edit.js'))
70
+ .then((loaded) => (edit = loaded))
71
+ .catch((error) => {
72
+ editLoad = null;
73
+ throw error;
74
+ });
75
+ return editLoad;
76
+ }
77
+
78
+ // The presence store is a first-use download for a reader too, so only the live lane
79
+ // asks for it: a view-mode tab names it nowhere and fetches none of it.
80
+ let presenceStore = null;
81
+ let presenceLoad = null;
82
+
83
+ function loadPresence() {
84
+ presenceLoad ??= import('./presence-store.js')
85
+ .then((mod) => (presenceStore = mod.presence))
86
+ .catch((error) => {
87
+ presenceLoad = null;
88
+ throw error;
89
+ });
90
+ return presenceLoad;
91
+ }
92
+
93
+ // `entering` means an entry is importing this module: the controller starts the
94
+ // lane once the mode is committed, so this evaluation starts nothing and loads
95
+ // nothing. An explicit initial edit, and a reader's saved lane, are untouched.
96
+ const entering = modeState().phase === "entering";
97
+
98
+ if (isEditMode && !entering) loadEdit().catch(() => {});
99
+
100
+ // The conflict ledger is what a merge writes to, so an editor starts its download as
101
+ // this file evaluates rather than at the first merge: nothing on the edit path pays
102
+ // for it twice. A reader starts it with its first merged frame (or with the live lane).
103
+ if (isEditMode) loadConflicts().catch(() => {});
104
+
105
+ // Who else is on the document, and the block somebody else is editing: both are
106
+ // a download of their own. A reader alone on a page draws neither, so they
107
+ // arrive with the first roster frame; a frame that lands while the download is
108
+ // in flight waits in order, and anything read before it is the empty state a
109
+ // bare constructor gives.
110
+ let roster = null;
111
+ let rosterLoad = null;
112
+ let rosterQueue = [];
113
+
114
+ function applyRoster(data) {
115
+ roster.presence.update(data);
116
+ roster.editingMarker.update(data);
117
+ }
118
+
119
+ function loadRoster() {
120
+ rosterLoad ??= Promise.all([import('./presence.js'), import('./editing-marker.js')])
121
+ .then(([presenceMod, markerMod]) => {
122
+ roster = { presence: presenceMod.presence, editingMarker: markerMod.editingMarker };
123
+ const queued = rosterQueue;
124
+ rosterQueue = [];
125
+ for (const data of queued) applyRoster(data);
126
+ return roster;
127
+ })
128
+ .catch((error) => {
129
+ rosterLoad = null;
130
+ throw error;
131
+ });
132
+ return rosterLoad;
133
+ }
134
+
135
+ // A download that failed keeps the queue, so the next frame asks again.
136
+ function takeRosterFrame(data) {
137
+ if (roster) {
138
+ applyRoster(data);
139
+ return;
140
+ }
141
+ rosterQueue.push(data);
142
+ loadRoster().catch(() => {});
143
+ }
144
+
145
+ // The receipt of a lane that has applied no saved file.
146
+ const NO_FILE = Object.freeze({ bytes: null, etag: null, seq: null });
147
+
148
+ // save.js returns this from savePageThrottled outside edit mode; a view-mode
149
+ // tab that somehow asks for a merge save gets the same answer without loading it.
150
+ const notInEditModeResult = () => ({
151
+ msg: 'Not in edit mode',
152
+ msgType: 'skipped',
153
+ code: null,
154
+ etag: null,
155
+ });
156
+
67
157
  // What a live-sync merge never reads or touches on any side: editor chrome,
68
158
  // content kept out of the save or the snapshot, the contents of frozen regions,
69
159
  // and nodes browser extensions inject. The same set the 0.5 morph's sync policy
@@ -92,6 +182,7 @@ function emptyScratchpads(root) {
92
182
  function scratchpadsEmptied(input) {
93
183
  if (input == null) return input;
94
184
  const doc = typeof input === 'string' ? new DOMParser().parseFromString(input, 'text/html') : input;
185
+ canonicalizeReceived(doc);
95
186
  emptyScratchpads(doc);
96
187
  return doc;
97
188
  }
@@ -127,6 +218,7 @@ const authoredAttr = (el) => el.getAttribute('data-id') || el.getAttribute('id')
127
218
  function countAuthored(root) {
128
219
  const counts = new Map();
129
220
  const visit = (el) => {
221
+ if (el.matches(SYNC_IGNORE_SELECTOR)) return;
130
222
  const id = authoredAttr(el);
131
223
  if (id) counts.set(id, (counts.get(id) || 0) + 1);
132
224
  const kids = el.localName === 'template' && el.content ? el.content.children : el.children;
@@ -178,64 +270,24 @@ function readServedStamp(html) {
178
270
  * mid-edit. Root attributes (editmode, savestatus, tokens) need no reversal
179
271
  * here; the merge's attribute veto keeps the live root's own.
180
272
  */
181
- function activateIncomingDoc(rootEl) {
182
- if (!isEditMode || !rootEl) return;
183
- enableContentEditable(rootEl);
184
- enableOnClick(rootEl);
185
- enableAdminInputs(rootEl);
186
- enableAdminResources(rootEl);
273
+ function activateIncomingDoc(rootEl, lane) {
274
+ if (lane !== 'live' || !rootEl || !edit) return;
275
+ edit.enableContentEditable(rootEl);
276
+ edit.enableOnClick(rootEl);
277
+ edit.enableAdminInputs(rootEl);
278
+ edit.enableAdminResources(rootEl);
187
279
  }
188
280
 
189
281
  function pairedBaseline() {
190
282
  const { forComparison, forDirty } = captureForComparisonAndDirty({ flushUndo: false });
191
283
  return [forComparison, forDirty];
192
284
  }
193
- import { savePageThrottled, setLastSavedBaselines, setUnsavedChanges, getLastSavedBytes, conflictResolvedBySync, isSaveConflicted, baselineSettled, schedulePaintHeal } from '../core/save.js';
194
285
 
195
286
  /**
196
- * The two live-sync wires, and the rule for choosing between them.
197
- *
198
- * Spec §10 puts both halves on `/_/sync`. Not every host serves that yet, and the
199
- * ones that do not cannot be upgraded on our schedule: hyperclay-local ships behind
200
- * Apple notarization, while this library reaches every document within one 10-minute
201
- * cache TTL. So the client is the half that has to know both.
202
- *
203
- * The choice is DISCOVERED, never guessed. Spec §5 makes `/_/meta` the only way to
204
- * learn what a host can do, and §10 says a client that has not discovered `sync`
205
- * never opens either half of the route. Probing is also a bad instrument here: an
206
- * EventSource reports a 404, an auth refusal and an offline browser through one
207
- * error path, so a failed connection cannot tell you which address was wrong.
208
- *
209
- * One profile is chosen once and used for BOTH directions for the life of the page.
210
- * Letting send and receive decide independently is exactly the bug this replaces: a
211
- * client that posted to the spec address while streaming from the legacy one.
212
- *
213
- * Both stream addresses carry `client-id`, in the same kebab spelling as the rest of
214
- * the query. It is the tab's own sender id — the one every outbound frame already
215
- * carries — so a host that reads it at admission knows this connection by the value
216
- * its frames arrive under, and can tell two tabs of one cookie-less guest apart. A
217
- * host that does not read it ignores an unknown parameter.
287
+ * The wires live in wire-profile.js: both halves of one sync pick a single profile
288
+ * from `/_/meta`, and the reader's receive-only stream makes the same choice.
218
289
  */
219
- const WIRE_PROFILES = {
220
- spec: {
221
- name: 'spec',
222
- relayPath: '/_/sync',
223
- documentHeader: 'Document-URL',
224
- snapshotKey: 'snapshot',
225
- streamPath: (href, lane, resumeId, clientId) =>
226
- `/_/sync?document-url=${encodeURIComponent(href)}&lane=${lane}&resume-id=${resumeId}&client-id=${encodeURIComponent(clientId)}`,
227
- },
228
- // What every published clayjs speaks, and what hyperclayjs and the inline script in
229
- // every Collection dashboard hardcode. Hosts keep these addresses permanently.
230
- legacy: {
231
- name: 'legacy',
232
- relayPath: '/_/live-sync/save',
233
- documentHeader: 'Page-URL',
234
- snapshotKey: 'html',
235
- streamPath: (href, lane, resumeId, clientId) =>
236
- `/_/live-sync/stream?page-url=${encodeURIComponent(href)}&lane=${lane}&resume-id=${resumeId}&client-id=${encodeURIComponent(clientId)}`,
237
- },
238
- };
290
+ import { WIRE_PROFILES, resolveWireProfile } from './wire-profile.js';
239
291
 
240
292
  class LiveSync {
241
293
  constructor() {
@@ -294,6 +346,7 @@ class LiveSync {
294
346
  this._sharedSync = false;
295
347
  this._liveData = false;
296
348
  this._editing = null;
349
+ this._presenceLease = null;
297
350
  this._ready = null;
298
351
  // Bumped by every start(), so a discovery that resolves after a stop/restart
299
352
  // cannot open a stream for the run that has already been torn down.
@@ -304,8 +357,13 @@ class LiveSync {
304
357
 
305
358
  // Lane on the shared per-file SSE channel. Edit mode: 'live' (owner-gated,
306
359
  // pre-strip peer snapshots + notifications). View mode: 'saved' (receive-
307
- // only post-strip on-disk HTML). Overridable per-instance for tests.
308
- this.lane = isEditMode ? 'live' : 'saved';
360
+ // only post-strip on-disk HTML). This is only the lane a page starts with:
361
+ // start() reads the committed mode, so one instance serves a reader's saved
362
+ // lane and, after an entry, the live lane.
363
+ this._lane = isEditMode ? 'live' : 'saved';
364
+ // A lane chosen by hand (a test, or a live switch handing one over) is never
365
+ // re-derived; otherwise every start() reads the committed mode.
366
+ this._laneChosen = false;
309
367
 
310
368
  // Store handler reference for cleanup
311
369
  this._snapshotHandler = null;
@@ -339,6 +397,15 @@ class LiveSync {
339
397
  this._morphInFlight = false;
340
398
  this._rafHandle = null;
341
399
 
400
+ // The saved file this lane last applied, `{ bytes, etag, seq }`: the receipt a
401
+ // switch is handed, and the only record of it a same-body frame may move. A
402
+ // metadata stamp is not a file, so it never advances this one.
403
+ this._represented = NO_FILE;
404
+ // A switch owns the queue from prepareHandoff() until it commits or resumes:
405
+ // frames keep filling their slots and nothing morphs.
406
+ this._handoff = null;
407
+ this._handoffHeld = false;
408
+
342
409
  // Disk-sourced external changes (htmlclay watcher content) get their own
343
410
  // one-deep slot so a peer frame arriving in the same window can't
344
411
  // displace them. Drained in seq order alongside the peer slot.
@@ -381,6 +448,7 @@ class LiveSync {
381
448
  // retries cannot pile up behind one another.
382
449
  this._servedFetchId = 0;
383
450
  this._startupRetry = null;
451
+ this._engineRetry = null;
384
452
 
385
453
  // Whether each lane is currently holding. A hold is a safe outcome but a
386
454
  // silent one: without an event the page simply stops updating and nothing
@@ -459,11 +527,26 @@ class LiveSync {
459
527
  this._setDiskBase(captureForSaveAndComparison({ emitForSync: false }).forSave, this._ticket());
460
528
  }
461
529
 
530
+ get lane() {
531
+ return this._lane;
532
+ }
533
+
534
+ set lane(value) {
535
+ this._lane = value;
536
+ this._laneChosen = true;
537
+ }
538
+
462
539
  /**
463
540
  * Start the LiveSync system
464
541
  * Can be called after stop() to restart with a new file
542
+ *
543
+ * The lane is read here, from the committed mode, unless the caller names one:
544
+ * the same instance rides a reader's saved lane, becomes the live lane at an
545
+ * entry, and comes back to the saved lane at an exit. `represented` is the
546
+ * `{bytes, etag, seq}` receipt a switch hands over, so a restarted saved lane
547
+ * has one before any frame arrives and a frame repeating it changes nothing.
465
548
  */
466
- start(file = null) {
549
+ start(file = null, { lane, represented } = {}) {
467
550
  // Reset destroyed flag to allow restart after stop()
468
551
  this.isDestroyed = false;
469
552
 
@@ -479,6 +562,10 @@ class LiveSync {
479
562
  return;
480
563
  }
481
564
 
565
+ const committed = modeState().mode === 'edit' ? 'live' : 'saved';
566
+ const wasLive = this.lane === 'live';
567
+ this._lane = lane || (this._laneChosen ? this._lane : committed);
568
+
482
569
  // Reset state for new connection. Resetting lastSeenSeq is a new baseline,
483
570
  // so mint a fresh resume id — this stream must not resume the previous one.
484
571
  this.lastHtml = null;
@@ -495,6 +582,13 @@ class LiveSync {
495
582
  this._applyGen++;
496
583
  this.lastSeenSeq = 0;
497
584
  this._lastExternalSeq = 0;
585
+ this._represented = represented
586
+ ? Object.freeze({ bytes: represented.bytes ?? null, etag: represented.etag ?? null, seq: represented.seq ?? null })
587
+ : NO_FILE;
588
+ // A saved lane a live session just gave back, holding no receipt, is the one case
589
+ // that has to fetch the served file for itself: an initial-edit exit that saved
590
+ // nothing still leaves a reader that can show and hand over a file.
591
+ this._acquireEmptySaved = wasLive && this.lane === 'saved' && this._represented.bytes === null;
498
592
  this._pendingExternal = null;
499
593
  this.resumeId = this.generateResumeId();
500
594
 
@@ -503,13 +597,16 @@ class LiveSync {
503
597
  // below: a base taken before a module built its DOM makes the first dirty merge
504
598
  // see both sides add that DOM, and keep it twice.
505
599
  if (this.lane === 'live') this._seedBases();
600
+ // The live lane merges from its first frame, and every merge there reads the
601
+ // ledger, so the download starts with the lane instead of delaying a frame.
602
+ if (this.lane === 'live') loadConflicts().catch(() => {});
506
603
 
507
604
  console.log(`[LiveSync] Starting for: ${this.currentFile} (lane=${this.lane})`);
508
605
  // One discovery request stands between here and the stream. It is memoized and
509
606
  // bounded, and it does not gate ordinary saves — /_/save is a separate lane that
510
607
  // never moved. Snapshots produced during the window are queued, not dropped.
511
608
  const gen = ++this._startGen;
512
- if (this.lane === 'live' && !baselineSettled()) {
609
+ if (this.lane === 'live' && !(edit ? edit.baselineSettled() : false)) {
513
610
  const applyGen = this._applyGen;
514
611
  const morphTicket = this._morphTicket;
515
612
  const saveEpoch = this._saveEpoch;
@@ -550,7 +647,10 @@ class LiveSync {
550
647
  }
551
648
  // Exposed so a caller (and the tests) can await the point where the stream is
552
649
  // actually open, rather than counting microtasks behind the discovery request.
553
- this._ready = this._resolveProfile().then(() => {
650
+ this._ready = Promise.all([
651
+ this.lane === 'live' ? loadEdit() : null,
652
+ this._resolveProfile(),
653
+ ]).then(() => {
554
654
  if (this.isDestroyed || this._startGen !== gen) return;
555
655
  this.connect();
556
656
  });
@@ -558,12 +658,10 @@ class LiveSync {
558
658
  // snapshot listener would never fire — skip registering it.
559
659
  if (this.lane === 'live') {
560
660
  this.listenForSnapshots();
561
- hostSupports('presence-editing').then((supported) => {
562
- if (!supported || this.isDestroyed || this._startGen !== gen || this._editing) return;
563
- this._editing = new EditingSender({ send: (path) => this._postEditing(path) });
564
- this._editing.start();
565
- this._editing.roster(this._lastRoster);
566
- }, () => {});
661
+ // One presence transport per document, chosen from the host's own answer, and
662
+ // one sender behind it: the generic marker publishes through the store, so a
663
+ // page target and a caret path never talk over each other.
664
+ loadPresence().then((store) => this._startPresence(store, gen), () => {});
567
665
  this._saveSavedHandler = () => {
568
666
  this._saveEpoch++;
569
667
  // The file now holds what this save wrote: the disk lane's new base,
@@ -574,7 +672,7 @@ class LiveSync {
574
672
  const ticket = this._saveTicket || this._ticket();
575
673
  this._saveTicket = 0;
576
674
  if (ticket > this._acceptedSaveTicket) this._acceptedSaveTicket = ticket;
577
- const bytes = getLastSavedBytes();
675
+ const bytes = edit ? edit.getLastSavedBytes() : null;
578
676
  if (bytes != null) this._setDiskBase(bytes, ticket);
579
677
  this._relayCommit();
580
678
  };
@@ -590,7 +688,7 @@ class LiveSync {
590
688
  queueMicrotask(() => {
591
689
  if (this.isDestroyed) return;
592
690
  if (refusedBy !== lastSeenEtag()) return;
593
- conflictResolvedBySync(refusedBy);
691
+ edit?.conflictResolvedBySync(refusedBy);
594
692
  this._saveAfterMerge(pageMaybeDirty(), false);
595
693
  });
596
694
  };
@@ -610,14 +708,24 @@ class LiveSync {
610
708
  streamClosed();
611
709
 
612
710
  // Nothing feeds the roster once the stream is gone, so leaving the stack up
613
- // would show a list of people who may all have left.
614
- presence.clear();
615
- editingMarker.clear();
711
+ // would show a list of people who may all have left. A roster that never
712
+ // loaded has drawn nothing, and its queued frames are dropped with the
713
+ // stream that fed them.
714
+ rosterQueue = [];
715
+ if (roster) {
716
+ roster.presence.clear();
717
+ roster.editingMarker.clear();
718
+ }
616
719
 
617
720
  if (this._editing) {
618
721
  this._editing.stop();
619
722
  this._editing = null;
620
723
  }
724
+ // Only this run's lease: a successor that already attached keeps its roster.
725
+ if (this._presenceLease) {
726
+ this._presenceLease.detach();
727
+ this._presenceLease = null;
728
+ }
621
729
  this._lastRoster = null;
622
730
 
623
731
  if (this._snapshotHandler) {
@@ -654,6 +762,8 @@ class LiveSync {
654
762
  this._pendingIdentityMap = null;
655
763
  this._pendingBy = null;
656
764
  this._pendingExternal = null;
765
+ this._handoff = null;
766
+ this._handoffHeld = false;
657
767
  clearTimeout(this._holdRetryPeer);
658
768
  clearTimeout(this._holdRetryExt);
659
769
  this._holdRetryPeer = null;
@@ -663,6 +773,8 @@ class LiveSync {
663
773
  // fetch, if any, owns the question.
664
774
  clearTimeout(this._startupRetry);
665
775
  this._startupRetry = null;
776
+ clearTimeout(this._engineRetry);
777
+ this._engineRetry = null;
666
778
  this._servedFetchId++;
667
779
  }
668
780
 
@@ -704,23 +816,14 @@ class LiveSync {
704
816
  */
705
817
  _resolveProfile() {
706
818
  if (!this._profilePromise) {
707
- this._profilePromise = hostMeta()
708
- .then((meta) => {
709
- const extensions = meta?.extensions || [];
710
- const hasSync = extensions.includes('sync');
711
- const hasLiveData = extensions.includes('live-data');
712
- const hasPresence = extensions.includes('presence');
713
- this._sharedData = hasSync && hasLiveData && extensions.includes('sync-worker-data') && !hasPresence;
714
- this._sharedSync = hasSync && !hasPresence && (this._sharedData || (extensions.includes('sync-worker') && !hasLiveData));
715
- this._liveData = hasLiveData;
716
- return hasSync ? WIRE_PROFILES.spec : WIRE_PROFILES.legacy;
717
- })
718
- .catch(() => WIRE_PROFILES.legacy)
719
- .then((profile) => {
720
- this._profile = profile;
721
- this._log(`Wire profile: ${profile.name}`);
722
- return profile;
723
- });
819
+ this._profilePromise = resolveWireProfile().then((resolved) => {
820
+ this._sharedData = resolved.sharedData;
821
+ this._sharedSync = resolved.sharedSync;
822
+ this._liveData = resolved.liveData;
823
+ this._profile = resolved.profile;
824
+ this._log(`Wire profile: ${resolved.profile.name}`);
825
+ return resolved.profile;
826
+ });
724
827
  }
725
828
  return this._profilePromise;
726
829
  }
@@ -809,9 +912,21 @@ class LiveSync {
809
912
  startupCheck: Boolean(servedDocumentEtag),
810
913
  });
811
914
 
915
+ // A saved reader that a live session handed back with no bytes has nothing to
916
+ // show and nothing to hand the next entry. Ask the host for the served file once,
917
+ // as a view boot does, so an initial-edit exit that saved nothing still leaves a
918
+ // receipt.
919
+ if (this._acquireEmptySaved) {
920
+ this._acquireEmptySaved = false;
921
+ this._fetchServedDocument(undefined, { repair: true });
922
+ }
923
+
812
924
  this.sse.onopen = () => {
813
925
  console.log('[LiveSync] Connected');
814
926
  if (this._editing) this._editing.reconnected();
927
+ // A reconnect is a new connection: the host issues it a new presence session,
928
+ // and nothing this tab sent under the old one may stand.
929
+ if (presenceStore) presenceStore.reconnect();
815
930
  if (this.onConnect) this.onConnect();
816
931
  };
817
932
 
@@ -886,8 +1001,7 @@ class LiveSync {
886
1001
  } catch {
887
1002
  return;
888
1003
  }
889
- presence.update(data);
890
- editingMarker.update(data);
1004
+ takeRosterFrame(data);
891
1005
  this._lastRoster = data;
892
1006
  if (this._editing) this._editing.roster(data);
893
1007
  });
@@ -957,6 +1071,15 @@ class LiveSync {
957
1071
  return;
958
1072
  }
959
1073
 
1074
+ // A saved frame repeating the file this lane already applied: the receipt moves
1075
+ // onto the new stamp and the page is not touched, because the bytes on screen
1076
+ // ARE these bytes.
1077
+ if (this.lane === 'saved' && html === this._represented.bytes) {
1078
+ this._represented = Object.freeze({ bytes: html, etag: etag || null, seq: seq ?? null });
1079
+ recordEtag(etag);
1080
+ return;
1081
+ }
1082
+
960
1083
  // The common case, and it costs nothing: a peer saved bytes this tab has
961
1084
  // already applied, so the frame's only news is the stamp. Taking it without
962
1085
  // a morph is exactly as safe as taking it after one, because the baseline
@@ -964,8 +1087,8 @@ class LiveSync {
964
1087
  if (etag && html === this.lastHtml) {
965
1088
  this._log(`Taking the stamp from a peer save of content already applied (seq=${seq})`);
966
1089
  recordEtag(etag);
967
- if (isSaveConflicted()) {
968
- conflictResolvedBySync(etag);
1090
+ if (edit && edit.isSaveConflicted()) {
1091
+ edit.conflictResolvedBySync(etag);
969
1092
  this._saveAfterMerge(pageMaybeDirty(), false);
970
1093
  }
971
1094
  return;
@@ -1114,6 +1237,7 @@ class LiveSync {
1114
1237
  headers: {
1115
1238
  'Content-Type': 'application/json',
1116
1239
  [profile.documentHeader]: window.location.href,
1240
+ [PAGE_FORMAT_HEADER]: String(PAGE_FORMAT),
1117
1241
  },
1118
1242
  body: JSON.stringify({
1119
1243
  [profile.snapshotKey]: html,
@@ -1154,13 +1278,45 @@ class LiveSync {
1154
1278
  });
1155
1279
  }
1156
1280
 
1157
- _postEditing(path) {
1281
+ /**
1282
+ * Take this run's presence transport and, behind its own capability, the generic
1283
+ * editing marker that publishes through it. `gen` is the start() this belongs to,
1284
+ * so a discovery that answers after a stop attaches nothing.
1285
+ */
1286
+ _startPresence(store, gen) {
1287
+ if (this.isDestroyed || this._startGen !== gen || this._presenceLease) return null;
1288
+ return store.connect({ send: (body) => this._postPresence(body) })
1289
+ .then((transport) => {
1290
+ if (this.isDestroyed || this._startGen !== gen) {
1291
+ transport?.detach();
1292
+ return null;
1293
+ }
1294
+ this._presenceLease = transport || null;
1295
+ return hostSupports('presence-editing');
1296
+ }, () => false)
1297
+ .then((supported) => {
1298
+ if (!supported || this.isDestroyed || this._startGen !== gen || this._editing) return;
1299
+ return loadEdit().then((mod) => {
1300
+ if (this.isDestroyed || this._startGen !== gen || this._editing) return;
1301
+ this._editing = new mod.EditingSender({});
1302
+ this._editing.start();
1303
+ this._editing.roster(this._lastRoster);
1304
+ });
1305
+ });
1306
+ }
1307
+
1308
+ /**
1309
+ * The relay's half of a transport: one request carrying the body the store built.
1310
+ * A page target arrives in the extended envelope, a legacy path exactly as it
1311
+ * always has, and the host decides by the same connection ownership either way.
1312
+ */
1313
+ _postPresence(body) {
1158
1314
  const url = new URL('/_/presence', window.location.origin);
1159
1315
  url.searchParams.set('client-id', this.clientId);
1160
1316
  return fetch(url.href, {
1161
1317
  method: 'POST',
1162
1318
  headers: { 'Content-Type': 'application/json', 'Document-URL': window.location.href },
1163
- body: JSON.stringify({ editing: path }),
1319
+ body: JSON.stringify(body),
1164
1320
  }).then((response) => response.ok, () => false);
1165
1321
  }
1166
1322
 
@@ -1319,7 +1475,15 @@ class LiveSync {
1319
1475
  // server could not replay, so it keeps its unstamped fallback.
1320
1476
  if (!repair) return;
1321
1477
  } else if (stamp === representedEtag()) {
1322
- // The file this tab was served, and nothing to do about it.
1478
+ // The file this tab was served, and nothing to do about it. The exception is
1479
+ // a saved-lane reader with no bytes: it has nothing to show and nothing to
1480
+ // hand the next entry, and these bytes ARE the file it starts from, so the
1481
+ // receipt moves even though the DOM is already showing them. That is the
1482
+ // saved lane's acquisition, and it is also the startup check of a page booted
1483
+ // in view, which is the same no-bytes reader asking which file it represents.
1484
+ if ((repair || startup) && this.lane === 'saved') {
1485
+ this._represented = Object.freeze({ bytes: html, etag: stamp, seq: seq ?? null });
1486
+ }
1323
1487
  return;
1324
1488
  }
1325
1489
  } else if (typeof seq === 'number' && seq < this._lastExternalSeq) {
@@ -1440,13 +1604,35 @@ class LiveSync {
1440
1604
  this._scheduleNextFrame();
1441
1605
  }
1442
1606
 
1607
+ /**
1608
+ * Hand the saved lane's queue to a switch without stopping it: an apply already in
1609
+ * flight finishes, a frame that arrives meanwhile keeps its slot, and a repair the
1610
+ * host asked for stays owed. The protocol itself is a dynamic target, so a page that
1611
+ * never switches modes never fetches it.
1612
+ *
1613
+ * @param {Object} [options] - `{ signal, deadline }`
1614
+ * @returns {Promise<{represented: Object, reconcile: function(): Promise<void>, resume: function(), commit: function(): boolean}>}
1615
+ */
1616
+ prepareHandoff(options = {}) {
1617
+ if (this._handoff) return Promise.resolve(this._handoff);
1618
+ if (this.isDestroyed) return Promise.reject(new Error('LiveSync: stopped'));
1619
+ this._handoffHeld = true;
1620
+ if (this._rafHandle != null) {
1621
+ this._cancelFrame(this._rafHandle);
1622
+ this._rafHandle = null;
1623
+ }
1624
+ return import('./saved-handoff.js')
1625
+ .then(({ engineHandoff }) => engineHandoff(this, options))
1626
+ .then((handoff) => (this._handoff = handoff));
1627
+ }
1628
+
1443
1629
  /**
1444
1630
  * Schedule the next-frame morph if one isn't already pending. Skipped when
1445
1631
  * a morph is in flight; the morph's post-completion check will reschedule
1446
1632
  * if a newer payload arrived during it.
1447
1633
  */
1448
1634
  _scheduleNextFrame() {
1449
- if (this.isDestroyed) return;
1635
+ if (this.isDestroyed || this._handoffHeld) return;
1450
1636
  if (this._rafHandle != null) return;
1451
1637
  if (this._morphInFlight) return;
1452
1638
  this._rafHandle = this._requestFrame(() => this._runPending());
@@ -1464,6 +1650,20 @@ class LiveSync {
1464
1650
  this._rafHandle = null;
1465
1651
  if (this.isDestroyed) return;
1466
1652
 
1653
+ if (!loadedEngine()) {
1654
+ this._morphInFlight = true;
1655
+ try {
1656
+ await mergeEngine();
1657
+ } catch (error) {
1658
+ this._log('Merge engine failed to load', error);
1659
+ this._engineRetry = setTimeout(() => this._scheduleNextFrame(), 2000);
1660
+ return;
1661
+ } finally {
1662
+ this._morphInFlight = false;
1663
+ }
1664
+ if (this.isDestroyed) return;
1665
+ }
1666
+
1467
1667
  const ext = this._pendingExternal;
1468
1668
  const opts = ext?.fetchOptions || null;
1469
1669
  // A startup check and a stamped repair are both version checks: each one
@@ -1656,11 +1856,17 @@ class LiveSync {
1656
1856
  * @param {object} [lane.extra] - extra mergeDocument options (the disk lane's beforeApply)
1657
1857
  */
1658
1858
  async _mergeIncoming(html, identityMap, { base, baseIdentityMap = null, captureLocal, synthetic, source = 'peer', seq = null, etag = null, extra = {} }) {
1859
+ if (!loadedEngine()) await mergeEngine();
1860
+ // The first merge is what a reader downloads the ledger for: this frame is the
1861
+ // one that may produce a conflict, and nothing before it could have. A ledger
1862
+ // already in hand is read synchronously, so a tab that has merged once begins
1863
+ // its next apply in the caller's own tick, as it always did.
1864
+ const { beginApply, completeApply, failApply, beginLineageCapture } = conflictLedger() || await loadConflicts();
1659
1865
  let lineageCapture;
1660
1866
  const mergeDocument = (options) => {
1661
1867
  lineageCapture = beginLineageCapture(conflicts.list());
1662
1868
  try {
1663
- const pending = HyperMorph.mergeDocument({ ...options, lineage: lineageCapture.lineage });
1869
+ const pending = loadedEngine().HyperMorph.mergeDocument({ ...options, lineage: lineageCapture.lineage });
1664
1870
  lineageCapture.returned();
1665
1871
  return pending;
1666
1872
  } catch (error) {
@@ -1692,7 +1898,8 @@ class LiveSync {
1692
1898
  const syntheticByAuthored = new Map();
1693
1899
  if (isIdMap(identityMap)) {
1694
1900
  remote = new DOMParser().parseFromString(html, 'text/html');
1695
- const imported = HyperMorph.importMap(remote.documentElement, identityMap);
1901
+ canonicalizeReceived(remote);
1902
+ const imported = loadedEngine().HyperMorph.importMap(remote.documentElement, identityMap);
1696
1903
  for (const el of remote.querySelectorAll('[data-id], [id]')) {
1697
1904
  const id = authored(el);
1698
1905
  const synthetic = id && imported.get(el);
@@ -1708,9 +1915,10 @@ class LiveSync {
1708
1915
  opaque: (el) => el.matches(FREEZE_SELECTOR) || (derivedActive() && ownsDerivedText(el)),
1709
1916
  remoteWins: (el) => el.matches(REMOTE_WINS_SELECTOR),
1710
1917
  ignoreAttribute: (el, name, merged) => isRootLocalAttr(el, name, merged) || (derivedActive() && isDerivedAttr(merged || el, name)),
1918
+ attributeMerge,
1711
1919
  protectFocusedValue: true,
1712
1920
  keepLiveOnly: source === 'peer',
1713
- scripts: { mergeTags: mergeTagRecognizers },
1921
+ scripts: { mergeTags: loadedEngine().mergeTagRecognizers },
1714
1922
  hooks: { beforeAttributeUpdated },
1715
1923
  ...extra,
1716
1924
  };
@@ -1835,7 +2043,8 @@ class LiveSync {
1835
2043
  _saveAfterMerge(diverged, typedDuringWait) {
1836
2044
  if (!(diverged || typedDuringWait)) return;
1837
2045
  if (!autosaveActive()) return;
1838
- savePageThrottled();
2046
+ if (edit) edit.savePageThrottled();
2047
+ else notInEditModeResult();
1839
2048
  }
1840
2049
 
1841
2050
  /**
@@ -1849,6 +2058,15 @@ class LiveSync {
1849
2058
  * @returns {Promise<void>}
1850
2059
  */
1851
2060
  async _doApplyUpdate(html, seq, identityMap, etag, by = null) {
2061
+ // Another version of the document is not a change to this one, whichever lane
2062
+ // or slot it arrived on: a peer snapshot merges no more than a saved frame does,
2063
+ // and nothing applies after the first refusal. The guard holds the saves and
2064
+ // asks the notice to offer a reload.
2065
+ if (upgradeReplaces(html)) {
2066
+ await stopForUpgrade();
2067
+ return;
2068
+ }
2069
+ if (this.lane === 'live' && !edit) await loadEdit();
1852
2070
  this._log('applyUpdate - pausing mutations and morphing');
1853
2071
  this.isPaused = true;
1854
2072
 
@@ -1986,7 +2204,7 @@ class LiveSync {
1986
2204
  emitForSync: false,
1987
2205
  flushUndo: false,
1988
2206
  });
1989
- setLastSavedBaselines(forComparison, forDirty);
2207
+ edit.setLastSavedBaselines(forComparison, forDirty);
1990
2208
  this._setDiskBase(forSave, this._ticket());
1991
2209
  }
1992
2210
 
@@ -2006,8 +2224,15 @@ class LiveSync {
2006
2224
  // event rather than on the frame's arrival for one reason: a frame that held
2007
2225
  // returns above without reaching here, so nothing can name an author for a
2008
2226
  // change this tab never took. Null on every frame the host did not stamp.
2227
+ //
2228
+ // A frame on the SAVED lane is a file, not a preview: only the live lane
2229
+ // carries keystrokes nobody saved. Tagging both as `peer` made a view-mode
2230
+ // reader, which drops previews, ignore every save on a page that also names
2231
+ // `sync`. `lane` names the lane the frame came from either way.
2232
+ if (this.lane === 'saved') this._represented = Object.freeze({ bytes: html, etag: etag || null, seq: seq ?? null });
2233
+
2009
2234
  document.dispatchEvent(new CustomEvent('clay:sync-applied', {
2010
- detail: { seq, source: 'peer', by: by || null, html, report, unresolved: this.unresolvedConflicts, conflictIds }
2235
+ detail: { seq, source: this.lane === 'saved' ? 'disk' : 'peer', lane: this.lane, by: by || null, html, report, unresolved: this.unresolvedConflicts, conflictIds }
2011
2236
  }));
2012
2237
  } finally {
2013
2238
  this._log('applyUpdate - morph complete, resuming mutations');
@@ -2026,7 +2251,7 @@ class LiveSync {
2026
2251
  // isPaused is lifted so the convergence save's relay reaches peers.
2027
2252
  // Only where autosave is on: a manual-save page must never auto-write, so
2028
2253
  // there the merge stays local, still dirty, until the person saves.
2029
- if (stamped) conflictResolvedBySync(etag);
2254
+ if (stamped) edit?.conflictResolvedBySync(etag);
2030
2255
  this._saveAfterMerge(diverged, typedDuringWait);
2031
2256
  }
2032
2257
 
@@ -2046,6 +2271,14 @@ class LiveSync {
2046
2271
  * local edits.
2047
2272
  */
2048
2273
  async _doApplyExternal(html, seq, etag = null, by = null, fetchOptions = null) {
2274
+ // Another version of the document is not a change to this one: it does not
2275
+ // merge, and after it no later frame is applied either. The guard holds the
2276
+ // saves and asks the notice to offer a reload.
2277
+ if (upgradeReplaces(html)) {
2278
+ await stopForUpgrade();
2279
+ return;
2280
+ }
2281
+ if (this.lane === 'live' && !edit) await loadEdit();
2049
2282
  const priorDisk = this._diskBase;
2050
2283
  this._log(`applyExternal - external disk change (seq=${seq})`);
2051
2284
  this.isPaused = true;
@@ -2119,7 +2352,7 @@ class LiveSync {
2119
2352
  source: 'disk',
2120
2353
  seq,
2121
2354
  etag,
2122
- extra: { beforeApply: (doc) => activateIncomingDoc(doc.documentElement) },
2355
+ extra: { beforeApply: (doc) => activateIncomingDoc(doc.documentElement, this.lane) },
2123
2356
  });
2124
2357
  diverged = report.localDiverged;
2125
2358
  typedDuringWait = typed;
@@ -2160,8 +2393,8 @@ class LiveSync {
2160
2393
  // beforeunload stays quiet. The dirty re-check matters: typing that
2161
2394
  // arrived during the morph's async wait would otherwise be captured
2162
2395
  // into the baseline and recorded as saved without reaching disk.
2163
- setLastSavedBaselines(...pairedBaseline());
2164
- setUnsavedChanges(false);
2396
+ edit.setLastSavedBaselines(...pairedBaseline());
2397
+ edit.setUnsavedChanges(false);
2165
2398
 
2166
2399
  // Cross-lane baseline: the PEER diff base (lastHtml) would otherwise
2167
2400
  // still describe pre-frame state, and a later dirty peer apply would
@@ -2173,6 +2406,8 @@ class LiveSync {
2173
2406
  this._applyGen++;
2174
2407
  }
2175
2408
 
2409
+ if (this.lane === 'saved') this._represented = Object.freeze({ bytes: html, etag: etag || null, seq: seq ?? null });
2410
+
2176
2411
  document.dispatchEvent(new CustomEvent('clay:sync-applied', {
2177
2412
  detail: {
2178
2413
  seq,
@@ -2199,14 +2434,14 @@ class LiveSync {
2199
2434
  // (our edits + their bytes). The baseline was left pre-external, so the
2200
2435
  // save sees both as changes and writes the merge back. Only where
2201
2436
  // autosave is on, as in the peer lane.
2202
- if (stamped) conflictResolvedBySync(etag);
2437
+ if (stamped) edit?.conflictResolvedBySync(etag);
2203
2438
  this._saveAfterMerge(diverged, typedDuringWait);
2204
2439
  // A disk frame from a writer that does not paint (an agent, the data API)
2205
2440
  // changes records and leaves the painted values as they were. If the page's
2206
2441
  // repainted values differ from them once the timer fires, one tab saves so
2207
2442
  // the bytes on disk heal. A writer that paints is left alone.
2208
2443
  if (priorDisk != null && paintSignatureOfHtml(priorDisk) === paintSignatureOfHtml(html)) {
2209
- schedulePaintHeal(() => (this._diskBase == null ? null : paintSignatureOfHtml(this._diskBase)));
2444
+ if (edit) edit.schedulePaintHeal(() => (this._diskBase == null ? null : paintSignatureOfHtml(this._diskBase)));
2210
2445
  }
2211
2446
  }
2212
2447
 
@@ -2253,13 +2488,22 @@ class LiveSync {
2253
2488
  this.isDestroyed = true;
2254
2489
  console.log('[LiveSync] Stopped');
2255
2490
  }
2491
+
2492
+ /**
2493
+ * The lifecycle participant for this module: the lease a live switch prepares,
2494
+ * enables at the commit and disposes at the exit. Its body is in the edit-only
2495
+ * chunk, so a reader never loads it.
2496
+ */
2497
+ editParticipant() {
2498
+ return loadEdit().then((mod) => mod.syncParticipant(this));
2499
+ }
2256
2500
  }
2257
2501
 
2258
2502
  // Singleton instance
2259
2503
  const liveSync = new LiveSync();
2260
2504
 
2261
2505
  // Auto-initialize when DOM is ready
2262
- if (typeof window !== 'undefined') {
2506
+ if (typeof window !== 'undefined' && !entering) {
2263
2507
  if (document.readyState === 'loading') {
2264
2508
  document.addEventListener('DOMContentLoaded', () => liveSync.start());
2265
2509
  } else {
@@ -2270,5 +2514,6 @@ if (typeof window !== 'undefined') {
2270
2514
  // Export for the clayjs module system. The class itself is exported so
2271
2515
  // tests can create fresh instances without driving the singleton's
2272
2516
  // EventSource/snapshot wiring.
2517
+ const morph = (oldEl, newEl, options) => mergeEngine().then(({ morph }) => morph(oldEl, newEl, options));
2273
2518
  export { liveSync, LiveSync, morph, WIRE_PROFILES, conflicts, SYNC_IGNORE_SELECTOR, REMOTE_WINS_SELECTOR };
2274
2519
  export default liveSync;