@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.
- package/README.md +4 -5
- package/THIRD-PARTY-NOTICES.md +16 -0
- package/compat/highlight-11.9.0.min.js +1213 -0
- package/compat/highlight-LICENSE +29 -0
- package/compat/journal-assets.lock.json +22 -0
- package/compat/overtype-2.6.1.min.js +116 -0
- package/compat/overtype-LICENSE +21 -0
- package/dist/all.js +90 -0
- package/dist/clay-dom.js +84 -0
- package/dist/clay-events.js +103 -0
- package/dist/clay-internals.js +85 -0
- package/dist/clay-options.js +93 -0
- package/dist/clay-router.js +98 -0
- package/dist/clay-ui.js +95 -0
- package/dist/clay-utils.js +84 -0
- package/dist/clay.js +146 -0
- package/dist/clay.standalone.js +84291 -57611
- package/dist/m/attrs/add-from-T5LJXFFI.js +1 -0
- package/dist/m/attrs/onaftersave-OKSWEJQF.js +1 -0
- package/dist/m/attrs/refetch-on-save-ZUE7PSCI.js +1 -0
- package/dist/m/attrs/save-freeze-MYNVXREN.js +1 -0
- package/dist/m/c-2AZDZWCK.js +1 -0
- package/dist/m/c-2SPVQSHC.js +1 -0
- package/dist/m/c-2YYDMLNZ.js +1 -0
- package/dist/m/c-336SGU5W.js +1 -0
- package/dist/m/c-33JJ6CBX.js +2 -0
- package/dist/m/c-3GJ4SDE2.js +1 -0
- package/dist/m/c-3H6BZDNJ.js +6 -0
- package/dist/m/c-3OWDMNJR.js +2 -0
- package/dist/m/c-3RUU4TPX.js +91 -0
- package/dist/m/c-3XIRD4NU.js +1 -0
- package/dist/m/c-46XYM55L.js +1 -0
- package/dist/m/c-4DFEJ2PO.js +3 -0
- package/dist/m/c-4JZDZFF6.js +1 -0
- package/dist/m/c-4L3BTFRK.js +1 -0
- package/dist/m/c-4Q2XTQ2N.js +2 -0
- package/dist/m/c-4WKFKEKF.js +1 -0
- package/dist/m/c-56QRZTZH.js +1 -0
- package/dist/m/c-5J4E7O4Y.js +4 -0
- package/dist/m/c-5LKDWQTL.js +19 -0
- package/dist/m/c-5QWQ7A5L.js +1 -0
- package/dist/m/c-5RJFTLSF.js +1 -0
- package/dist/m/c-5V3MOVJT.js +1 -0
- package/dist/m/c-5VYOWDTI.js +1 -0
- package/dist/m/c-5YIHJ3TJ.js +1 -0
- package/dist/m/c-6AZOGAPE.js +1 -0
- package/dist/m/c-6CRUUXOX.js +5 -0
- package/dist/m/c-6HMB24EV.js +1 -0
- package/dist/m/c-6KEVEQ7L.js +1 -0
- package/dist/m/c-6NC5I2TV.js +1 -0
- package/dist/m/c-74MGYWZF.js +1 -0
- package/dist/m/c-7DJDKPGL.js +1 -0
- package/dist/m/c-7OLLJPWP.js +1 -0
- package/dist/m/c-7SWMYKAI.js +1 -0
- package/dist/m/c-A3E3NIMC.js +1 -0
- package/dist/m/c-A7LS5UZW.js +1 -0
- package/dist/m/c-AMSNPOCX.js +1 -0
- package/dist/m/c-AQLFEATB.js +1 -0
- package/dist/m/c-B3JM5ZXC.js +1 -0
- package/dist/m/c-BBVGTQG7.js +1 -0
- package/dist/m/c-BPF2NSPV.js +1 -0
- package/dist/m/c-BQ6LOBEE.js +1 -0
- package/dist/m/c-BTDI4Y23.js +1 -0
- package/dist/m/c-BWFVSBDF.js +1 -0
- package/dist/m/c-BYTKL6UX.js +1 -0
- package/dist/m/c-CFHW5AMY.js +6 -0
- package/dist/m/c-CJ3A7ZKE.js +1 -0
- package/dist/m/c-CQHPGKX2.js +1 -0
- package/dist/m/c-CYWLPPUS.js +1 -0
- package/dist/m/c-CZUSOB7V.js +1 -0
- package/dist/m/c-DCFSEJZG.js +1 -0
- package/dist/m/c-DCXOXBF3.js +1 -0
- package/dist/m/c-DFS4XZJV.js +2 -0
- package/dist/m/c-DNENWY4O.js +1 -0
- package/dist/m/c-DNZ5IFM6.js +1 -0
- package/dist/m/c-E4LIR5X5.js +1 -0
- package/dist/m/c-EJTGYSLN.js +1 -0
- package/dist/m/c-EVCQ3BKA.js +1 -0
- package/dist/m/c-EZU56NQV.js +34 -0
- package/dist/m/c-FDNHHZY5.js +1 -0
- package/dist/m/c-FILLTJA7.js +1 -0
- package/dist/m/c-FRLHSY7E.js +1 -0
- package/dist/m/c-FROJUZMV.js +1 -0
- package/dist/m/c-G2JPKCTP.js +1 -0
- package/dist/m/c-GB6TU7WV.js +1 -0
- package/dist/m/c-GFJJEBTU.js +1 -0
- package/dist/m/c-GJAEGGXY.js +1 -0
- package/dist/m/c-GOWBDM6J.js +1 -0
- package/dist/m/c-GQ6F6IDK.js +1 -0
- package/dist/m/c-GSFIGKOP.js +1 -0
- package/dist/m/c-GY422N3Q.js +1 -0
- package/dist/m/c-H4LG442Q.js +1 -0
- package/dist/m/c-HA6INT6J.js +1 -0
- package/dist/m/c-HAWFNBYG.js +1 -0
- package/dist/m/c-HDDEEL77.js +1 -0
- package/dist/m/c-HFKUVACH.js +1 -0
- package/dist/m/c-HHDFCNHV.js +1 -0
- package/dist/m/c-HPURYVZS.js +2 -0
- package/dist/m/c-I2ZATQCD.js +2 -0
- package/dist/m/c-IEM4NTCS.js +1 -0
- package/dist/m/c-IJUK2GPW.js +1 -0
- package/dist/m/c-IW6GGOU2.js +1 -0
- package/dist/m/c-JAX2667S.js +6 -0
- package/dist/m/c-JDCX26UP.js +25 -0
- package/dist/m/c-JRKY44DK.js +1 -0
- package/dist/m/c-JU3NK25H.js +1 -0
- package/dist/m/c-KAU6E43D.js +1 -0
- package/dist/m/c-KBS3UV6B.js +1 -0
- package/dist/m/c-KGVGROZT.js +1 -0
- package/dist/m/c-KI7M4AY3.js +1 -0
- package/dist/m/c-KIPQJX4B.js +1 -0
- package/dist/m/c-KQ6TYJAP.js +135 -0
- package/dist/m/c-KU3ZFADW.js +1 -0
- package/dist/m/c-L4WQSC27.js +1 -0
- package/dist/m/c-LFPHCXY3.js +1 -0
- package/dist/m/c-LRPUO2MX.js +1 -0
- package/dist/m/c-LWLBHLVC.js +3 -0
- package/dist/m/c-LZJPNR2W.js +1 -0
- package/dist/m/c-M2L47S2X.js +7 -0
- package/dist/m/c-MFP2ZWD4.js +1 -0
- package/dist/m/c-MKSQ4Y57.js +1 -0
- package/dist/m/c-MTQHWQC3.js +1 -0
- package/dist/m/c-N326R2JD.js +1 -0
- package/dist/m/c-N7JPUPGC.js +1 -0
- package/dist/m/c-NRPJEVJS.js +1 -0
- package/dist/m/c-NUFFJVVJ.js +1 -0
- package/dist/m/c-NUHP2XDB.js +1 -0
- package/dist/m/c-NUHSPT5P.js +1 -0
- package/dist/m/c-O72HVYSA.js +1 -0
- package/dist/m/c-OCSZ4QGN.js +1 -0
- package/dist/m/c-OIBUTDXI.js +1 -0
- package/dist/m/c-OIDRZKAF.js +1 -0
- package/dist/m/c-OW5QIGG3.js +1 -0
- package/dist/m/c-P7KNDG34.js +1 -0
- package/dist/m/c-PGRCK73F.js +1 -0
- package/dist/m/c-PQ5LK6QE.js +993 -0
- package/dist/m/c-PS5ARVWR.js +9 -0
- package/dist/m/c-PZ4XEPUP.js +2 -0
- package/dist/m/c-Q622NS2Y.js +1 -0
- package/dist/m/c-Q7NQD65V.js +1 -0
- package/dist/m/c-QH63LQOS.js +1 -0
- package/dist/m/c-QHIWXHRO.js +1 -0
- package/dist/m/c-QHWJ5ZIJ.js +1 -0
- package/dist/m/c-QICP2YR5.js +1 -0
- package/dist/m/c-QJ4OM3G6.js +1 -0
- package/dist/m/c-QNKREOL6.js +1 -0
- package/dist/m/c-R3L7ZJGB.js +2 -0
- package/dist/m/c-R7IWVINQ.js +1 -0
- package/dist/m/c-RM7J7BV2.js +1 -0
- package/dist/m/c-RSMOODDU.js +1 -0
- package/dist/m/c-RVMWGXEJ.js +1 -0
- package/dist/m/c-RYUXUUBX.js +1 -0
- package/dist/m/c-S4ZEH46B.js +9 -0
- package/dist/m/c-S7H2DRCB.js +1 -0
- package/dist/m/c-S7LK4KUE.js +1 -0
- package/dist/m/c-SDAXDM2D.js +1 -0
- package/dist/m/c-SGIA4IEQ.js +188 -0
- package/dist/m/c-SMBZG5X4.js +1 -0
- package/dist/m/c-SMGU7RT3.js +235 -0
- package/dist/m/c-SPIORRYS.js +1 -0
- package/dist/m/c-SWLRT66R.js +1 -0
- package/dist/m/c-SZGYW5HT.js +1 -0
- package/dist/m/c-TCCA5BZN.js +2 -0
- package/dist/m/c-TCJTVAFN.js +1 -0
- package/dist/m/c-TJ6WOJL3.js +67 -0
- package/dist/m/c-TQEMVJCS.js +1 -0
- package/dist/m/c-TRS6TM6O.js +1 -0
- package/dist/m/c-TYLXX4NF.js +21 -0
- package/dist/m/c-U62OB4BT.js +1 -0
- package/dist/m/c-U6YWUAR3.js +8 -0
- package/dist/m/c-UDCSU5G5.js +2 -0
- package/dist/m/c-UDUUJPFZ.js +1 -0
- package/dist/m/c-UGHH4MB3.js +1 -0
- package/dist/m/c-UL2YUY52.js +1 -0
- package/dist/m/c-VAK7SG37.js +1 -0
- package/dist/m/c-VDSS4K3Z.js +1 -0
- package/dist/m/c-VE6ZMYMC.js +1 -0
- package/dist/m/c-VG5XIN4T.js +1 -0
- package/dist/m/c-VHM5XQHK.js +1 -0
- package/dist/m/c-VHNSBD2M.js +1 -0
- package/dist/m/c-VI5RMSVY.js +1 -0
- package/dist/m/c-VKEDWYDF.js +1 -0
- package/dist/m/c-VRZN56XY.js +1 -0
- package/dist/m/c-W5MCHEGA.js +1 -0
- package/dist/m/c-WDC5GVKU.js +1 -0
- package/dist/m/c-WN5AY3OZ.js +1 -0
- package/dist/m/c-WNYBLIOZ.js +1 -0
- package/dist/m/c-WRBKEJD4.js +1 -0
- package/dist/m/c-WWXIDH4O.js +28 -0
- package/dist/m/c-WYFDLITL.js +1 -0
- package/dist/m/c-XCNTK76U.js +1 -0
- package/dist/m/c-XMS6DPBR.js +1 -0
- package/dist/m/c-XN6MBKI6.js +67 -0
- package/dist/m/c-XPELB6E5.js +1 -0
- package/dist/m/c-XQXZAO7N.js +1 -0
- package/dist/m/c-Y5HBBIQ4.js +1 -0
- package/dist/m/c-ZE46QDIH.js +1 -0
- package/dist/m/c-ZUCOKOHT.js +1 -0
- package/dist/m/c-ZVPVXTJF.js +4 -0
- package/dist/m/c-ZVRKXKM7.js +1 -0
- package/dist/m/c-ZWIOJGMT.js +1 -0
- package/dist/m/core/admin-attrs-QHOKPJ7W.js +1 -0
- package/dist/m/core/autosave-HRYGUWV6.js +1 -0
- package/dist/m/core/conflict-notice-6MROJN2L.js +1 -0
- package/dist/m/core/draft-mode-WSQAT67S.js +1 -0
- package/dist/m/core/host-permission-FNXVEFNE.js +1 -0
- package/dist/m/core/is-edit-mode-KTWA3O5N.js +1 -0
- package/dist/m/core/persist-JFYRHMRJ.js +3 -0
- package/dist/m/core/recovery-FLTCC2OC.js +1 -0
- package/dist/m/core/save-HQZ4S667.js +1 -0
- package/dist/m/core/save-core-KGQWFUNH.js +1 -0
- package/dist/m/core/snapshot-EGQZNT45.js +1 -0
- package/dist/m/core/unsaved-warning-IDJLD66H.js +1 -0
- package/dist/m/dom/all-APS5ZSKP.js +1 -0
- package/dist/m/dom/dom-helpers-L7OSR6S4.js +1 -0
- package/dist/m/events/index-RN2JNGBV.js +1 -0
- package/dist/m/internals/index-PVXGPUDA.js +1 -0
- package/dist/m/lib/cache-bust-MUMFXZK6.js +1 -0
- package/dist/m/lib/region-policy-SB2AWVG5.js +1 -0
- package/dist/m/loader-S4I7GKXR.js +1 -0
- package/dist/m/manifest-4C419D67.js +1 -0
- package/dist/m/manifest-edit-B077F288.js +1 -0
- package/dist/m/manifest.json +3988 -0
- package/dist/m/options/options-EJMS3K24.js +1 -0
- package/dist/m/page/editor-NNJA4OKD.js +202 -0
- package/dist/m/plugins/ai-edit-QN4PMMNL.js +6 -0
- package/dist/m/plugins/bind-text-NX46N5UV.js +1 -0
- package/dist/m/plugins/cms-HP3LSTPN.js +2293 -0
- package/dist/m/plugins/demo-RM5IR6AO.js +1 -0
- package/dist/m/plugins/derive-UAAA4V4S.js +1 -0
- package/dist/m/plugins/indicator-URDBBOQO.js +1 -0
- package/dist/m/plugins/live-HFWD4OIH.js +1 -0
- package/dist/m/plugins/menu-3HIEQSI6.js +138 -0
- package/dist/m/plugins/message-LQ6EDH4V.js +1 -0
- package/dist/m/plugins/page-JOKB6XPR.js +2 -0
- package/dist/m/plugins/page-view-NXOJHNW6.js +1 -0
- package/dist/m/plugins/people-SGPJKANK.js +1 -0
- package/dist/m/plugins/quickcrop-QNY3PKLD.js +26 -0
- package/dist/m/plugins/quickcrop-QNY3PKLD.js.LEGAL.txt +7 -0
- package/dist/m/plugins/richclay-NRQDSWX6.js +517 -0
- package/dist/m/plugins/richclay-NRQDSWX6.js.LEGAL.txt +1 -0
- package/dist/m/plugins/sortable-GWT4PWDV.js +1 -0
- package/dist/m/plugins/source-4ZRSAIYM.js +1 -0
- package/dist/m/plugins/undo-VDA5P7FH.js +1 -0
- package/dist/m/plugins/unsent-HJFLQJ2O.js +1 -0
- package/dist/m/plugins/upgrade-W7FG5VFG.js +1 -0
- package/dist/m/plugins/upload-WLYKK6TL.js +1 -0
- package/dist/m/plugins/visitor-drafts-LIDY7NH6.js +21 -0
- package/dist/m/plugins/visitor-drafts-owner-ZXCKBZE7.js +1 -0
- package/dist/m/plugins/wire-4N6GYAF6.js +1 -0
- package/dist/m/reader-core-DXYCURFF.js +68 -0
- package/dist/m/route/route-KOCZSEGN.js +1 -0
- package/dist/m/sync/live-sync-J7VMYTKS.js +1 -0
- package/dist/m/ui/index-3JLVI27O.js +1 -0
- package/dist/m/utils/index-4TJCVLT7.js +1 -0
- package/dist/m/vendor/Sortable.vendor-A755ITLN.js +3 -0
- package/dist/m/vendor/Sortable.vendor-A755ITLN.js.LEGAL.txt +1 -0
- package/dist/m/vendor/sap-core.vendor-A7BMUYZT.js +6 -0
- package/dist/offline-manifest.json +4034 -0
- package/dist/page/themes/light-1.css +430 -0
- package/dist/server/core.cjs +23748 -22855
- package/dist/server/offline-runtime.cjs +4954 -0
- package/dist/server/offline-runtime.mjs +4923 -0
- package/dist/server/rooms.mjs +9 -0
- package/docs/guide.md +1078 -669
- package/entries/all.js +2 -0
- package/entries/clay-data.js +1 -1
- package/entries/clay-dom.js +2 -0
- package/entries/clay-events.js +2 -0
- package/entries/clay-internals.js +2 -0
- package/entries/clay-options.js +2 -0
- package/entries/clay-router.js +2 -0
- package/entries/clay-ui.js +2 -0
- package/entries/clay-utils.js +2 -0
- package/entries/clay.js +2 -0
- package/entries/sap.js +6 -6
- package/fonts/ibm-plex-mono-LICENSE +93 -0
- package/fonts/ibm-plex-mono-latin-300-normal.woff2 +0 -0
- package/fonts/ibm-plex-mono-latin-400-italic.woff2 +0 -0
- package/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- package/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- package/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
- package/fonts/ibm-plex-sans-LICENSE +93 -0
- package/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
- package/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
- package/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
- package/fonts/ibm-plex.css +71 -0
- package/fonts/inter-LICENSE +93 -0
- package/fonts/inter-cyrillic-ext-wght-normal.woff2 +0 -0
- package/fonts/inter-cyrillic-wght-normal.woff2 +0 -0
- package/fonts/inter-greek-ext-wght-normal.woff2 +0 -0
- package/fonts/inter-greek-wght-normal.woff2 +0 -0
- package/fonts/inter-latin-ext-wght-normal.woff2 +0 -0
- package/fonts/inter-latin-wght-normal.woff2 +0 -0
- package/fonts/inter-vietnamese-wght-normal.woff2 +0 -0
- package/fonts/inter.css +62 -0
- package/fonts/newsreader-LICENSE +93 -0
- package/fonts/newsreader-latin-ext-opsz-normal.woff2 +0 -0
- package/fonts/newsreader-latin-opsz-normal.woff2 +0 -0
- package/fonts/newsreader-vietnamese-opsz-normal.woff2 +0 -0
- package/fonts/newsreader.css +26 -0
- package/fonts/work-sans-LICENSE +93 -0
- package/fonts/work-sans-latin-wght-normal.woff2 +0 -0
- package/fonts/work-sans.css +8 -0
- package/package.json +20 -3
- package/packed-contract.json +1 -0
- package/src/attrs/add-from.js +27 -4
- package/src/attrs/save-freeze.js +7 -52
- package/src/core/admin-attrs.js +119 -12
- package/src/core/admin-contenteditable.js +12 -10
- package/src/core/admin-inputs.js +14 -15
- package/src/core/admin-onclick.js +2 -0
- package/src/core/autosave.js +74 -21
- package/src/core/boot-phase.js +2 -1
- package/src/core/conflict-download.js +19 -23
- package/src/core/conflict-notice-ui.js +1173 -0
- package/src/core/conflict-notice.js +140 -697
- package/src/core/download.js +32 -0
- package/src/core/edit-mode.js +147 -14
- package/src/core/etag.js +30 -12
- package/src/core/host-meta.js +90 -8
- package/src/core/host-permission.js +29 -0
- package/src/core/is-edit-mode.js +145 -12
- package/src/core/mode-key.js +125 -0
- package/src/core/mode-lifecycle.js +544 -0
- package/src/core/mode-receipt.js +30 -0
- package/src/core/mode-refusal.js +13 -0
- package/src/core/mode-resources.js +52 -0
- package/src/core/mode-support.js +82 -0
- package/src/core/mode-switch.js +259 -0
- package/src/core/mode-transition.js +112 -0
- package/src/core/page-data-write.js +305 -0
- package/src/core/page-data.js +26 -227
- package/src/core/page-format-wire.js +4 -0
- package/src/core/page-format.js +58 -0
- package/src/core/persist.js +37 -9
- package/src/core/preserve-authored.js +114 -0
- package/src/core/recovery.js +772 -0
- package/src/core/runtime-url.js +105 -0
- package/src/core/save-core.js +113 -12
- package/src/core/save-replace.js +72 -0
- package/src/core/save-size-notice.js +134 -0
- package/src/core/save-size.js +140 -0
- package/src/core/save.js +806 -119
- package/src/core/snapshot.js +159 -11
- package/src/core/source-map.js +157 -9
- package/src/core/unsaved-warning.js +14 -4
- package/src/core/upgrade-run.js +408 -0
- package/src/core/version.js +18 -0
- package/src/lib/assets.js +30 -0
- package/src/lib/autosave-debug-gate.js +50 -0
- package/src/lib/autosave-scheduler.js +140 -0
- package/src/lib/boot-imports.js +8 -0
- package/src/lib/derived.js +3 -1
- package/src/lib/dirty-gate.js +15 -0
- package/src/lib/doc-storage.js +33 -0
- package/src/lib/effect.js +9 -4
- package/src/lib/mutation.js +4 -2
- package/src/lib/offline-runtime.js +226 -0
- package/src/lib/preload.js +48 -0
- package/src/lib/root-attrs.js +1 -1
- package/src/lib/trash.js +18 -0
- package/src/lib/undo/capture-move.js +152 -0
- package/src/live/local-channel.js +61 -0
- package/src/live/local-lane.js +60 -9
- package/src/live/server-lane.js +6 -3
- package/src/loader-logic.js +76 -15
- package/src/loader.js +218 -30
- package/src/page/adopt-boot.js +76 -22
- package/src/page/adopt.js +25 -4
- package/src/page/awareness.js +230 -0
- package/src/page/blocks.js +113 -6
- package/src/page/blocksel.js +20 -4
- package/src/page/board-face.js +13 -0
- package/src/page/board-panel.js +5 -17
- package/src/page/board-style.js +2 -13
- package/src/page/board-view.js +1 -11
- package/src/page/board-views.js +3 -0
- package/src/page/board.js +2 -2
- package/src/page/boot-pack.js +26 -0
- package/src/page/boot.js +237 -0
- package/src/page/chart-tables.js +11 -0
- package/src/page/chart.js +6 -10
- package/src/page/checklist.js +1 -1
- package/src/page/checkpoint-proof.js +289 -0
- package/src/page/chrome.js +431 -63
- package/src/page/csv-import.js +17 -2
- package/src/page/document-title-dom.js +52 -0
- package/src/page/document-title-runtime.js +90 -0
- package/src/page/document-title.js +12 -0
- package/src/page/drag.js +48 -13
- package/src/page/drop.js +2 -1
- package/src/page/editor.js +399 -90
- package/src/page/emoji-data.js +162 -0
- package/src/page/feature-loader.js +72 -0
- package/src/page/features/board.js +21 -0
- package/src/page/features/chart.js +11 -0
- package/src/page/features/code.js +13 -0
- package/src/page/features/dates.js +180 -0
- package/src/page/features/emoji.js +187 -0
- package/src/page/features/media.js +290 -0
- package/src/page/features/mentions.js +188 -0
- package/src/page/features/registry.js +28 -0
- package/src/page/features/table.js +383 -0
- package/src/page/{tool-view.js → features/tool.js} +33 -2
- package/src/page/format-codec.js +215 -0
- package/src/page/format-command.js +9 -0
- package/src/page/format-gate.js +179 -0
- package/src/page/format-host.js +54 -0
- package/src/page/format-level.js +157 -0
- package/src/page/format-native-commands.js +142 -0
- package/src/page/format-native.js +42 -0
- package/src/page/highlight-slot.js +23 -0
- package/src/page/highlight.js +30 -16
- package/src/page/history-intent.js +78 -0
- package/src/page/inline.js +45 -28
- package/src/page/items.js +67 -14
- package/src/page/link.js +1 -0
- package/src/page/markdown.js +112 -3
- package/src/page/media-views.js +3 -0
- package/src/page/media.js +33 -245
- package/src/page/mention-picker.js +99 -0
- package/src/page/merge.js +1 -1
- package/src/page/outline-cache.js +11 -0
- package/src/page/placeholder.js +27 -7
- package/src/page/projection.js +240 -51
- package/src/page/reader-patch.js +306 -0
- package/src/page/recover.js +1 -1
- package/src/page/rich/page-tool.js +1 -4
- package/src/page/runtime-owner.js +27 -0
- package/src/page/save-projection.js +46 -4
- package/src/page/save-states.js +1 -1
- package/src/page/schema.js +47 -0
- package/src/page/table-rect-key.js +3 -0
- package/src/page/table-select.js +4 -3
- package/src/page/table-style.js +1 -1
- package/src/page/table-views.js +3 -0
- package/src/page/table.js +8 -293
- package/src/page/themes/content.css +25 -1
- package/src/page/themes/light-1.css +163 -2
- package/src/page/themes/light-1.source.css +269 -0
- package/src/page/tool-capability.js +3 -0
- package/src/page/tool-views.js +3 -0
- package/src/page/view-hooks.js +32 -0
- package/src/page/view.js +390 -107
- package/src/page/ybind/awareness.js +172 -0
- package/src/page/ybind/binding.js +40 -5
- package/src/page/ybind/bridge.js +50 -47
- package/src/page/ybind/decode.js +52 -19
- package/src/page/ybind/doc.js +27 -2
- package/src/page/ybind/group.js +12 -3
- package/src/page/ybind/index.js +2 -0
- package/src/page/ybind/lineage.js +71 -0
- package/src/page/ybind/marks.js +26 -1
- package/src/page/ybind/migrate.js +48 -13
- package/src/page/ybind/moves.js +32 -21
- package/src/page/ybind/positions.js +50 -0
- package/src/page/ybind/provider.js +97 -24
- package/src/page/ybind/recovery-proof.js +191 -0
- package/src/page/ybind/recovery-replay.js +488 -0
- package/src/page/ybind/tokens.js +15 -6
- package/src/page/ybind/walk.js +10 -3
- package/src/page/ybind/writer.js +48 -4
- package/src/plugins/ai-edit.js +167 -32
- package/src/plugins/bind-text.js +218 -0
- package/src/plugins/cms.js +93 -0
- package/src/plugins/indicator.js +57 -5
- package/src/plugins/live.js +319 -35
- package/src/plugins/menu/claygoo.svg.js +1 -0
- package/src/plugins/menu/seo-head.js +323 -0
- package/src/plugins/menu/seo.js +849 -0
- package/src/plugins/menu.js +425 -0
- package/src/plugins/page-view.js +131 -0
- package/src/plugins/page.js +1131 -270
- package/src/plugins/people.js +158 -31
- package/src/plugins/richclay.js +119 -9
- package/src/plugins/sortable.js +370 -60
- package/src/plugins/source.js +189 -366
- package/src/plugins/undo.js +289 -19
- package/src/plugins/unsent.js +132 -0
- package/src/plugins/upgrade-run.js +1 -0
- package/src/plugins/upgrade.js +210 -0
- package/src/plugins/upload-task.js +571 -0
- package/src/plugins/upload-ui.js +265 -0
- package/src/plugins/upload.js +34 -153
- package/src/plugins/visitor-drafts/inert.js +32 -0
- package/src/plugins/visitor-drafts/intro.js +2 -1
- package/src/plugins/visitor-drafts/owner-chip.js +3 -0
- package/src/plugins/visitor-drafts/prompt.js +2 -2
- package/src/plugins/visitor-drafts-owner.js +85 -0
- package/src/plugins/visitor-drafts.js +31 -42
- package/src/plugins/wire.js +82 -1
- package/src/reader-core.js +26 -0
- package/src/recovery/conflicts.js +287 -0
- package/src/recovery/identity.js +154 -0
- package/src/recovery/store.js +792 -0
- package/src/recovery/undo.js +257 -0
- package/src/server/checkpoint/index.js +68 -4
- package/src/server/offline-runtime/index.js +214 -0
- package/src/server/rooms/index.js +2 -1
- package/src/server/rooms/live-data-rooms.js +2 -2
- package/src/server/rooms/page-admission.js +33 -0
- package/src/server/rooms/page-lineage.js +56 -0
- package/src/server/rooms/page-rebase.js +11 -5
- package/src/server/rooms/page-state.js +2 -0
- package/src/server/rooms/page-write-admission.js +9 -3
- package/src/source/session.js +705 -0
- package/src/sync/conflict-revert.js +5 -4
- package/src/sync/conflicts-edit.js +119 -0
- package/src/sync/conflicts-facade.js +49 -0
- package/src/sync/conflicts.js +99 -1
- package/src/sync/editing-marker.js +37 -6
- package/src/sync/editing-sender.js +8 -1
- package/src/sync/live-sync-edit.js +63 -0
- package/src/sync/live-sync.js +364 -119
- package/src/sync/local-presence.js +187 -0
- package/src/sync/merge-declarations.js +10 -0
- package/src/sync/merge-engine.js +12 -0
- package/src/sync/presence-store.js +412 -0
- package/src/sync/presence-target.js +243 -0
- package/src/sync/presence.js +72 -41
- package/src/sync/saved-handoff.js +200 -0
- package/src/sync/saved-stream.js +544 -0
- package/src/sync/version-guard.js +92 -0
- package/src/sync/wire-profile.js +81 -0
- package/src/ui/bevel-controls.js +2 -8
- package/src/ui/page-scheme.js +7 -0
- package/src/ui/skins/cms.js +1 -1
- package/src/ui/theme-config.js +150 -0
- package/src/ui/theme.js +6 -138
- package/src/ui/toast-lazy.js +11 -0
- package/src/vendor/LICENSES-optional.md +253 -0
- package/src/vendor/highlight.vendor.js +3 -0
- package/src/vendor/hljs-bash.vendor.js +2 -0
- package/src/vendor/hljs-css.vendor.js +2 -0
- package/src/vendor/hljs-javascript.vendor.js +2 -0
- package/src/vendor/hljs-json.vendor.js +2 -0
- package/src/vendor/hljs-python.vendor.js +2 -0
- package/src/vendor/hljs-xml.vendor.js +2 -0
- package/src/vendor/hyper-html-api-data.vendor.js +1 -1
- package/src/vendor/hyper-html-api-upgrade.vendor.js +5 -0
- package/src/vendor/hyper-morph-identity.vendor.js +2 -0
- package/src/vendor/hyper-morph-json.vendor.js +3 -0
- package/src/vendor/hyper-morph.vendor.js +6 -6
- package/src/vendor/hyper-undo.vendor.js +1 -1
- package/src/vendor/hypercms.vendor.js +169 -84
- package/src/vendor/marked.vendor.js +68 -0
- package/src/vendor/overtype.vendor.js +994 -0
- package/src/vendor/richclay.vendor.js +17 -17
- package/src/vendor/sap-core.vendor.js +5 -5
- package/src/vendor/sap-ownership.vendor.js +11 -7
package/src/sync/live-sync.js
CHANGED
|
@@ -35,35 +35,125 @@
|
|
|
35
35
|
* INTEGRATES WITH: snapshot.js (receives snapshot-ready events)
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
|
-
import {
|
|
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 {
|
|
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 {
|
|
50
|
-
import {
|
|
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,
|
|
64
|
-
import {
|
|
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 (!
|
|
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
|
|
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
|
-
|
|
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).
|
|
308
|
-
|
|
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 =
|
|
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
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
|
|
615
|
-
|
|
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 =
|
|
708
|
-
.
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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;
|