@panphora/clayjs 1.15.2 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +83090 -58706
- 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-NWPPXOCZ.js +1 -0
- package/dist/m/attrs/save-freeze-FUP4Q4X3.js +1 -0
- package/dist/m/c-2AZDZWCK.js +1 -0
- package/dist/m/c-2JL6E75N.js +1 -0
- package/dist/m/c-2M73252O.js +1 -0
- package/dist/m/c-2SPVQSHC.js +1 -0
- package/dist/m/c-33HUYEDI.js +1 -0
- package/dist/m/c-33JJ6CBX.js +2 -0
- package/dist/m/c-35NBGMZB.js +1 -0
- package/dist/m/c-3GJ4SDE2.js +1 -0
- package/dist/m/c-3IQRECYS.js +1 -0
- package/dist/m/c-3OWDMNJR.js +2 -0
- package/dist/m/c-46XYM55L.js +1 -0
- package/dist/m/c-4B6H4G5V.js +1 -0
- package/dist/m/c-4DRMS4QG.js +9 -0
- package/dist/m/c-4FFHZM32.js +1 -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-4YSCRICO.js +1 -0
- package/dist/m/c-54RCDCBU.js +1 -0
- package/dist/m/c-5M4YEKCX.js +4 -0
- package/dist/m/c-5R4PTRG2.js +1 -0
- package/dist/m/c-5V3MOVJT.js +1 -0
- package/dist/m/c-5VYOWDTI.js +1 -0
- package/dist/m/c-5Y6TNX5W.js +34 -0
- package/dist/m/c-5YIHJ3TJ.js +1 -0
- package/dist/m/c-66O32MV3.js +28 -0
- package/dist/m/c-6HMB24EV.js +1 -0
- package/dist/m/c-7DJDKPGL.js +1 -0
- package/dist/m/c-7O6AFR65.js +1 -0
- package/dist/m/c-7SWMYKAI.js +1 -0
- package/dist/m/c-7VGZ7RU5.js +1 -0
- package/dist/m/c-A7LS5UZW.js +1 -0
- package/dist/m/c-ANQTJ3B5.js +2 -0
- package/dist/m/c-AQLFEATB.js +1 -0
- package/dist/m/c-AYVOREBE.js +1 -0
- package/dist/m/c-B2ZXPOS3.js +3 -0
- package/dist/m/c-BBVGTQG7.js +1 -0
- package/dist/m/c-BMIZKMPV.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-CJZSLMVQ.js +1 -0
- package/dist/m/c-CYWLPPUS.js +1 -0
- package/dist/m/c-CZUSOB7V.js +1 -0
- package/dist/m/c-DBE6ORSI.js +1 -0
- package/dist/m/c-DCFSEJZG.js +1 -0
- package/dist/m/c-DNZ5IFM6.js +1 -0
- package/dist/m/c-EDHUR4MX.js +1 -0
- package/dist/m/c-EJTGYSLN.js +1 -0
- package/dist/m/c-EY6UOM54.js +1 -0
- package/dist/m/c-F3CKJTJS.js +1 -0
- package/dist/m/c-F7Y6FLPY.js +1 -0
- package/dist/m/c-FA4DZVTC.js +1 -0
- package/dist/m/c-FILLTJA7.js +1 -0
- package/dist/m/c-G2BUZITR.js +1 -0
- package/dist/m/c-GB3JTV6E.js +1 -0
- package/dist/m/c-GBGWXTX4.js +1 -0
- package/dist/m/c-GHBCCVG4.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-H4LG442Q.js +1 -0
- package/dist/m/c-HA6INT6J.js +1 -0
- package/dist/m/c-HPURYVZS.js +2 -0
- package/dist/m/c-HRDCD3WN.js +67 -0
- package/dist/m/c-HT33EQRN.js +1 -0
- package/dist/m/c-HVYXIS72.js +1 -0
- package/dist/m/c-IB52R237.js +1 -0
- package/dist/m/c-IJUK2GPW.js +1 -0
- package/dist/m/c-IKWNE4R4.js +1 -0
- package/dist/m/c-IU3IYEU6.js +1 -0
- package/dist/m/c-IZ2EBRAJ.js +1 -0
- package/dist/m/c-J7CA54IP.js +1 -0
- package/dist/m/c-JAX2667S.js +6 -0
- package/dist/m/c-KI7M4AY3.js +1 -0
- package/dist/m/c-KIPQJX4B.js +1 -0
- package/dist/m/c-KKWLDOPM.js +1 -0
- package/dist/m/c-KR4UW4KW.js +1 -0
- package/dist/m/c-KY3VJGUA.js +8 -0
- package/dist/m/c-L4WQSC27.js +1 -0
- package/dist/m/c-LFPHCXY3.js +1 -0
- package/dist/m/c-LR4CGEPT.js +91 -0
- package/dist/m/c-LRPUO2MX.js +1 -0
- package/dist/m/c-LTGMXXKO.js +1 -0
- package/dist/m/c-LXENEBV7.js +1 -0
- package/dist/m/c-LZJPNR2W.js +1 -0
- package/dist/m/c-MMUIUUZG.js +2 -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-NMJRXKPC.js +8 -0
- package/dist/m/c-NRPJEVJS.js +1 -0
- package/dist/m/c-NUHP2XDB.js +1 -0
- package/dist/m/c-NZMNXVF6.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-OYTFP5QN.js +1 -0
- package/dist/m/c-P7KNDG34.js +1 -0
- package/dist/m/c-PPOSCAQD.js +1 -0
- package/dist/m/c-PQ5LK6QE.js +993 -0
- package/dist/m/c-PZ4XEPUP.js +2 -0
- package/dist/m/c-QHIWXHRO.js +1 -0
- package/dist/m/c-QHWJ5ZIJ.js +1 -0
- package/dist/m/c-QIX5BJYN.js +1 -0
- package/dist/m/c-QNKREOL6.js +1 -0
- package/dist/m/c-QQHEX6HA.js +1 -0
- package/dist/m/c-R7RMANJI.js +9 -0
- package/dist/m/c-RI4NT75Y.js +1 -0
- package/dist/m/c-RM7J7BV2.js +1 -0
- package/dist/m/c-RUJKSUQW.js +1 -0
- package/dist/m/c-RVMWGXEJ.js +1 -0
- package/dist/m/c-S7H2DRCB.js +1 -0
- package/dist/m/c-S7LK4KUE.js +1 -0
- package/dist/m/c-SGCCC4FW.js +25 -0
- package/dist/m/c-SGIA4IEQ.js +188 -0
- package/dist/m/c-SWRJFCNN.js +1 -0
- package/dist/m/c-T4VO4OCB.js +1 -0
- package/dist/m/c-TCCA5BZN.js +2 -0
- package/dist/m/c-TQEMVJCS.js +1 -0
- package/dist/m/c-TQZ2JYHD.js +1 -0
- package/dist/m/c-TR6RMDQ7.js +3 -0
- package/dist/m/c-TRS6TM6O.js +1 -0
- package/dist/m/c-UGHH4MB3.js +1 -0
- package/dist/m/c-UKHTLWUW.js +1 -0
- package/dist/m/c-UL2YUY52.js +1 -0
- package/dist/m/c-UNN7LWCT.js +1 -0
- package/dist/m/c-UXBMRDZB.js +235 -0
- package/dist/m/c-V7PZZRG7.js +1 -0
- package/dist/m/c-VAK7SG37.js +1 -0
- package/dist/m/c-VDSS4K3Z.js +1 -0
- package/dist/m/c-VFX45IZQ.js +1 -0
- package/dist/m/c-VHNSBD2M.js +1 -0
- package/dist/m/c-VKEDWYDF.js +1 -0
- package/dist/m/c-VL52ADDR.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-WWDBYXLK.js +1 -0
- package/dist/m/c-WYFDLITL.js +1 -0
- package/dist/m/c-XN6MBKI6.js +67 -0
- package/dist/m/c-XNNW2CWK.js +1 -0
- package/dist/m/c-XPELB6E5.js +1 -0
- package/dist/m/c-XQXZAO7N.js +1 -0
- package/dist/m/c-Y6USGTS5.js +5 -0
- package/dist/m/c-YDH7Q3NY.js +1 -0
- package/dist/m/c-YG2NJYS6.js +135 -0
- package/dist/m/c-Z36J63PP.js +1 -0
- package/dist/m/c-Z4VRC7GU.js +7 -0
- package/dist/m/c-Z6K52OLP.js +19 -0
- package/dist/m/c-ZDY2MDAA.js +21 -0
- package/dist/m/c-ZE46QDIH.js +1 -0
- package/dist/m/c-ZUCOKOHT.js +1 -0
- package/dist/m/core/admin-attrs-VGNLAUDK.js +1 -0
- package/dist/m/core/autosave-E4PEFPZ6.js +1 -0
- package/dist/m/core/conflict-notice-VQ6Y64PH.js +1 -0
- package/dist/m/core/draft-mode-WSQAT67S.js +1 -0
- package/dist/m/core/host-permission-XN3K2IW3.js +1 -0
- package/dist/m/core/is-edit-mode-JXHDVGUL.js +1 -0
- package/dist/m/core/persist-HKHA66IY.js +3 -0
- package/dist/m/core/recovery-TBBSICE2.js +1 -0
- package/dist/m/core/save-WUCHTQBE.js +1 -0
- package/dist/m/core/save-core-FJVHT5M3.js +1 -0
- package/dist/m/core/snapshot-F3SWDZMM.js +1 -0
- package/dist/m/core/unsaved-warning-STKBHE6G.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-23DPTUT4.js +1 -0
- package/dist/m/internals/index-WOVAP3RU.js +1 -0
- package/dist/m/lib/cache-bust-CGWT6ZJI.js +1 -0
- package/dist/m/lib/region-policy-SB2AWVG5.js +1 -0
- package/dist/m/loader-HXX6YNH5.js +1 -0
- package/dist/m/manifest-F22DEDAC.js +1 -0
- package/dist/m/manifest-edit-C3C7376B.js +1 -0
- package/dist/m/manifest.json +3491 -0
- package/dist/m/options/options-2LH76JQ3.js +1 -0
- package/dist/m/page/editor-XUTB2HB2.js +189 -0
- package/dist/m/plugins/ai-edit-ZQ4CGWIP.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-ZIBFAA3R.js +1 -0
- package/dist/m/plugins/derive-UAAA4V4S.js +1 -0
- package/dist/m/plugins/indicator-PV4YW2ID.js +1 -0
- package/dist/m/plugins/live-NUTTXSBS.js +1 -0
- package/dist/m/plugins/menu-HCKO3DSP.js +138 -0
- package/dist/m/plugins/message-LQ6EDH4V.js +1 -0
- package/dist/m/plugins/page-KSUH7G6J.js +2 -0
- package/dist/m/plugins/page-view-W73OW7WI.js +1 -0
- package/dist/m/plugins/people-AJJ32YCG.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-RC3L2F6X.js +517 -0
- package/dist/m/plugins/richclay-RC3L2F6X.js.LEGAL.txt +1 -0
- package/dist/m/plugins/sortable-YRMQX3ZU.js +1 -0
- package/dist/m/plugins/source-KHK5UUSK.js +1 -0
- package/dist/m/plugins/undo-OF2L3CXU.js +1 -0
- package/dist/m/plugins/unsent-HJFLQJ2O.js +1 -0
- package/dist/m/plugins/upgrade-ZNYODP5Y.js +1 -0
- package/dist/m/plugins/upload-XSGNILUY.js +1 -0
- package/dist/m/plugins/visitor-drafts-XK5GW55Q.js +21 -0
- package/dist/m/plugins/visitor-drafts-owner-KSSH75EV.js +1 -0
- package/dist/m/plugins/wire-ZWVWKF76.js +1 -0
- package/dist/m/reader-core-ILWLXVMP.js +68 -0
- package/dist/m/route/route-KOCZSEGN.js +1 -0
- package/dist/m/sync/live-sync-4AUVXVJU.js +1 -0
- package/dist/m/ui/index-EVHKMKBL.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 +3800 -0
- package/dist/page/themes/light-1.css +406 -0
- package/dist/server/core.cjs +23662 -22860
- 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/conflict-download.js +22 -0
- package/src/core/conflict-notice-ui.js +1173 -0
- package/src/core/conflict-notice.js +140 -697
- package/src/core/edit-mode.js +147 -14
- package/src/core/etag.js +30 -12
- package/src/core/host-meta.js +72 -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 +109 -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 +805 -118
- package/src/core/snapshot.js +121 -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 +141 -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 +79 -5
- 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 +17 -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/checklist.js +1 -1
- package/src/page/chrome.js +411 -61
- package/src/page/csv-import.js +17 -2
- package/src/page/document-title-dom.js +40 -0
- package/src/page/document-title-runtime.js +76 -0
- package/src/page/document-title.js +9 -0
- package/src/page/drag.js +48 -13
- package/src/page/drop.js +2 -1
- package/src/page/editor.js +330 -86
- package/src/page/feature-loader.js +72 -0
- package/src/page/features/board.js +12 -0
- package/src/page/features/code.js +13 -0
- package/src/page/features/media.js +290 -0
- package/src/page/features/registry.js +23 -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-gate.js +179 -0
- package/src/page/format-level.js +157 -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 +18 -25
- package/src/page/items.js +52 -11
- package/src/page/link.js +1 -0
- package/src/page/markdown.js +86 -1
- package/src/page/media-views.js +3 -0
- package/src/page/media.js +33 -245
- package/src/page/outline-cache.js +11 -0
- package/src/page/placeholder.js +27 -7
- package/src/page/projection.js +196 -50
- 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 +7 -4
- package/src/page/schema.js +14 -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/light-1.css +139 -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.js +390 -107
- package/src/page/ybind/awareness.js +172 -0
- package/src/page/ybind/binding.js +9 -1
- 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 +25 -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 +84 -22
- package/src/page/ybind/recovery-proof.js +191 -0
- package/src/page/ybind/recovery-replay.js +130 -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 +282 -34
- 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 +1022 -275
- 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/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 +535 -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
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* local-presence.js — `clay.presence` over the same-browser channel.
|
|
3
|
+
*
|
|
4
|
+
* A host without a presence endpoint advertises a broadcast transport, and this is
|
|
5
|
+
* the transport the presence store leases for it. Tabs of one document in one
|
|
6
|
+
* browser exchange sessions over the document's BroadcastChannel, so two readers
|
|
7
|
+
* see each other with no room, no writer lock, no data state and no durable lane
|
|
8
|
+
* loaded at all. The channel is the local lane's, shared and reference-counted by
|
|
9
|
+
* `local-channel.js`; the frames are `presence-v1`, which the lane never reads and
|
|
10
|
+
* an older client already ignores as a type it does not handle.
|
|
11
|
+
*
|
|
12
|
+
* A session is a fresh random identifier in this tab, not an account: the roster
|
|
13
|
+
* says this browser, "This computer", with one session per tab, and only this
|
|
14
|
+
* tab's session is `you`. Nothing here advances the durable room's sequence, takes
|
|
15
|
+
* its lock or enters its mirror.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { EXPIRE_MS, RENEW_MS, validateTarget } from './presence-target.js';
|
|
19
|
+
import { PRESENCE_TYPE, localChannel } from '../live/local-channel.js';
|
|
20
|
+
import { presence } from './presence-store.js';
|
|
21
|
+
|
|
22
|
+
export const LOCAL_LABEL = 'This computer';
|
|
23
|
+
|
|
24
|
+
// A cap on the sessions one tab will hold, so a peer that keeps inventing ids
|
|
25
|
+
// cannot grow the roster without bound.
|
|
26
|
+
export const MAX_SESSIONS = 64;
|
|
27
|
+
|
|
28
|
+
const MAX_LABEL = 64;
|
|
29
|
+
const SESSION_ID = /^[\x20-\x7e]{1,128}$/;
|
|
30
|
+
|
|
31
|
+
function randomId() {
|
|
32
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') return crypto.randomUUID();
|
|
33
|
+
return Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readLabel(value) {
|
|
37
|
+
if (typeof value !== 'string') return LOCAL_LABEL;
|
|
38
|
+
const trimmed = value.trim().slice(0, MAX_LABEL);
|
|
39
|
+
return trimmed || LOCAL_LABEL;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// A session frame carries the editing value the store already validated; anything
|
|
43
|
+
// else a peer sends is read as no target rather than repaired into one.
|
|
44
|
+
function readEditing(value) {
|
|
45
|
+
const result = validateTarget(value === undefined ? null : value);
|
|
46
|
+
return result.ok ? result.target : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The person-level editing value: this tab when it is as new as any other
|
|
51
|
+
* session, else the newest session that has something to show. Page targets are
|
|
52
|
+
* the store's to drop (they live only in sessions), so this hands over whatever
|
|
53
|
+
* the newest session publishes.
|
|
54
|
+
*/
|
|
55
|
+
function personEditing(own, others) {
|
|
56
|
+
let best = null;
|
|
57
|
+
let n = -1;
|
|
58
|
+
for (const session of others) {
|
|
59
|
+
if (session.editing === null || session.n <= n) continue;
|
|
60
|
+
best = session.editing;
|
|
61
|
+
n = session.n;
|
|
62
|
+
}
|
|
63
|
+
return own.editing !== null && own.n >= n ? own.editing : best;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The broadcast presence transport for one tab. `start` joins the document's
|
|
68
|
+
* channel and attaches to the store, which leases the transport; `stop` says
|
|
69
|
+
* goodbye, leaves the channel and lets the store clear its roster.
|
|
70
|
+
*
|
|
71
|
+
* @param {object} [options]
|
|
72
|
+
* @param {string} [options.scope] - the canonical document scope, shared with local
|
|
73
|
+
* collaboration. Documents that only share a filename have different scopes.
|
|
74
|
+
* @param {string} [options.key] - the channel name, when it is not the scope's
|
|
75
|
+
* @param {object} [options.store] - the presence store to feed and to send for
|
|
76
|
+
* @param {object} [options.channel] - the document's channel, when the local lane
|
|
77
|
+
* in this tab already owns one
|
|
78
|
+
* @param {string} [options.label] - the honest local name this browser shows
|
|
79
|
+
*/
|
|
80
|
+
export function localPresence({ scope = null, key = null, store = presence, channel: shared = null, label = LOCAL_LABEL } = {}) {
|
|
81
|
+
const name = readLabel(label);
|
|
82
|
+
const channelKey = key ?? 'clay-live:' + (scope ?? (typeof window !== 'undefined' ? window.location.pathname : '/'));
|
|
83
|
+
const personId = 'local:' + (scope ?? channelKey);
|
|
84
|
+
const sessionId = randomId();
|
|
85
|
+
|
|
86
|
+
// session id -> { id, n, editing, seenAt }: what each peer tab last said.
|
|
87
|
+
const peers = new Map();
|
|
88
|
+
let target = null;
|
|
89
|
+
let n = 0;
|
|
90
|
+
let channel = null;
|
|
91
|
+
let unsubscribe = null;
|
|
92
|
+
let lease = null;
|
|
93
|
+
let heartbeat = null;
|
|
94
|
+
let running = false;
|
|
95
|
+
|
|
96
|
+
function post(message) {
|
|
97
|
+
if (!channel) return;
|
|
98
|
+
channel.post({ type: PRESENCE_TYPE, from: sessionId, ...message });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// One person, one session per tab: this tab's own, then every peer's, in id
|
|
102
|
+
// order so two tabs holding the same people publish the same roster.
|
|
103
|
+
function publish() {
|
|
104
|
+
const own = { id: sessionId, you: true, n, editing: target };
|
|
105
|
+
const others = [];
|
|
106
|
+
for (const peer of peers.values()) others.push({ id: peer.id, you: false, n: peer.n, editing: peer.editing });
|
|
107
|
+
const sessions = [own, ...others].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
108
|
+
store.receive({
|
|
109
|
+
people: [{ id: personId, name, you: true, canEdit: true, editing: personEditing(own, others), sessions }],
|
|
110
|
+
anonymous: 0,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function onMessage(message) {
|
|
115
|
+
if (!message || message.from === sessionId || typeof message.from !== 'string') return;
|
|
116
|
+
if (!SESSION_ID.test(message.from)) return;
|
|
117
|
+
const known = peers.get(message.from);
|
|
118
|
+
if (message.op === 'bye') {
|
|
119
|
+
if (known) peers.delete(message.from);
|
|
120
|
+
publish();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (message.op !== 'hello' && message.op !== 'update' && message.op !== 'beat') return;
|
|
124
|
+
if (!known && peers.size >= MAX_SESSIONS) return;
|
|
125
|
+
// A beat only refreshes a session; it cannot change where the peer is.
|
|
126
|
+
const editing = message.op === 'beat' ? (known ? known.editing : null) : readEditing(message.editing);
|
|
127
|
+
const said = Number.isSafeInteger(message.n) && message.n >= 0 ? message.n : 0;
|
|
128
|
+
if (known && said < known.n) return;
|
|
129
|
+
peers.set(message.from, { id: message.from, n: said, editing, seenAt: Date.now() });
|
|
130
|
+
// A tab that has just arrived is answered, so it learns the tabs already here.
|
|
131
|
+
if (message.op === 'hello' && !known) post({ op: 'hello', n, editing: target, label: name });
|
|
132
|
+
publish();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function tick() {
|
|
136
|
+
if (!running) return;
|
|
137
|
+
post({ op: 'beat', n });
|
|
138
|
+
const at = Date.now();
|
|
139
|
+
let dropped = false;
|
|
140
|
+
for (const [id, peer] of peers) {
|
|
141
|
+
if (at - peer.seenAt <= EXPIRE_MS) continue;
|
|
142
|
+
peers.delete(id);
|
|
143
|
+
dropped = true;
|
|
144
|
+
}
|
|
145
|
+
if (dropped) publish();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// The store's side of the lease: its target, as the body it built for a host.
|
|
149
|
+
function send(body) {
|
|
150
|
+
const editing = body && typeof body === 'object' && 'editing' in body ? body.editing : null;
|
|
151
|
+
target = readEditing(editing);
|
|
152
|
+
n += 1;
|
|
153
|
+
post({ op: 'update', n, editing: target });
|
|
154
|
+
publish();
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
sessionId,
|
|
160
|
+
/** Join the channel and take the store's targets and roster. Returns the lease. */
|
|
161
|
+
start() {
|
|
162
|
+
if (running) return lease;
|
|
163
|
+
running = true;
|
|
164
|
+
channel = shared ?? localChannel(channelKey);
|
|
165
|
+
unsubscribe = channel.subscribe('presence', onMessage);
|
|
166
|
+
lease = store.attachTransport({ send });
|
|
167
|
+
post({ op: 'hello', n, editing: target, label: name });
|
|
168
|
+
publish();
|
|
169
|
+
heartbeat = setInterval(tick, RENEW_MS);
|
|
170
|
+
return lease;
|
|
171
|
+
},
|
|
172
|
+
/** Say goodbye, leave the channel and give the store its roster back. */
|
|
173
|
+
stop() {
|
|
174
|
+
if (!running) return;
|
|
175
|
+
running = false;
|
|
176
|
+
if (heartbeat !== null) clearInterval(heartbeat);
|
|
177
|
+
heartbeat = null;
|
|
178
|
+
post({ op: 'bye' });
|
|
179
|
+
peers.clear();
|
|
180
|
+
unsubscribe?.();
|
|
181
|
+
unsubscribe = null;
|
|
182
|
+
channel = null;
|
|
183
|
+
lease?.detach();
|
|
184
|
+
lease = null;
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const KINDS = { "merge-tokens": "tokens", "merge-max": "max" };
|
|
2
|
+
|
|
3
|
+
export function attributeMerge(el, name) {
|
|
4
|
+
if (!name.startsWith("data-") || name === "data-id" || !el?.closest) return null;
|
|
5
|
+
for (const [attr, kind] of Object.entries(KINDS)) {
|
|
6
|
+
const owner = el.closest(`[${attr}]`);
|
|
7
|
+
if (owner && owner.getAttribute(attr).split(/\s+/).includes(name)) return kind;
|
|
8
|
+
}
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
let engine = null;
|
|
2
|
+
let loading = null;
|
|
3
|
+
|
|
4
|
+
export const loadedEngine = () => engine;
|
|
5
|
+
|
|
6
|
+
export function mergeEngine() {
|
|
7
|
+
if (engine) return Promise.resolve(engine);
|
|
8
|
+
loading ??= Promise.all([import("../vendor/hyper-morph.vendor.js"), import("./merge-tags.js")])
|
|
9
|
+
.then(([vendor, tags]) => (engine = { HyperMorph: vendor.HyperMorph, morph: vendor.morph, mergeTagRecognizers: tags.mergeTagRecognizers }))
|
|
10
|
+
.catch((error) => { loading = null; throw error; });
|
|
11
|
+
return loading;
|
|
12
|
+
}
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* presence-store.js — `clay.presence`: the roster and this tab's target.
|
|
3
|
+
*
|
|
4
|
+
* One store serves a Document's page targets and Kanban's record targets. It holds
|
|
5
|
+
* what the host said, publishes an immutable snapshot, and turns `setTarget` calls
|
|
6
|
+
* into the bounded requests the host accepts. The chrome that draws the roster
|
|
7
|
+
* subscribes here rather than owning the state, so a reader that draws nothing and
|
|
8
|
+
* an editor that draws carets agree about who is present.
|
|
9
|
+
*
|
|
10
|
+
* Everything here is synchronous except the transport's own promise. Detaching a
|
|
11
|
+
* lease that a newer lease replaced cannot clear the newer owner's roster, and a
|
|
12
|
+
* snapshot never contains the private session token, connection ids or socket data.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { hostMeta } from '../core/host-meta.js';
|
|
16
|
+
import {
|
|
17
|
+
COALESCE_MS,
|
|
18
|
+
RENEW_MS,
|
|
19
|
+
PRESENCE_VERSION,
|
|
20
|
+
normalizeEditing,
|
|
21
|
+
normalizeSelfSession,
|
|
22
|
+
normalizeSessions,
|
|
23
|
+
requestBody,
|
|
24
|
+
targetKey,
|
|
25
|
+
validateTarget,
|
|
26
|
+
} from './presence-target.js';
|
|
27
|
+
|
|
28
|
+
const emptySnapshot = () => Object.freeze({
|
|
29
|
+
supported: false,
|
|
30
|
+
recordAwareness: false,
|
|
31
|
+
pageAwareness: false,
|
|
32
|
+
people: Object.freeze([]),
|
|
33
|
+
anonymous: 0,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export class PresenceStore {
|
|
37
|
+
constructor() {
|
|
38
|
+
this.supported = false;
|
|
39
|
+
this.recordAwareness = false;
|
|
40
|
+
this.pageAwareness = false;
|
|
41
|
+
this.people = [];
|
|
42
|
+
this.anonymous = 0;
|
|
43
|
+
this.snapshot = emptySnapshot();
|
|
44
|
+
this.listeners = new Set();
|
|
45
|
+
// The `n` each public session last reported, so a frame that overtook a newer
|
|
46
|
+
// one for the same session cannot move a caret back.
|
|
47
|
+
this.sessions = new Map();
|
|
48
|
+
// The private connection session: {id, token}. Consumed by the sender, never
|
|
49
|
+
// published.
|
|
50
|
+
this.session = null;
|
|
51
|
+
this.target = null;
|
|
52
|
+
this.heldKey = '';
|
|
53
|
+
this.owner = null;
|
|
54
|
+
// Whether the target this tab holds belongs to an explicit owner (a page) or to
|
|
55
|
+
// the generic editing path. One of them speaks at a time.
|
|
56
|
+
this.explicit = false;
|
|
57
|
+
this.connecting = null;
|
|
58
|
+
this.n = 0;
|
|
59
|
+
this.sentKey = null;
|
|
60
|
+
this.timer = null;
|
|
61
|
+
this.timerKind = null;
|
|
62
|
+
this.inflight = false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The current immutable snapshot. The same object until something changes, so a
|
|
67
|
+
* subscriber can compare by identity.
|
|
68
|
+
*/
|
|
69
|
+
getSnapshot() {
|
|
70
|
+
return this.snapshot;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Emits the current snapshot immediately, then every change. Includes the
|
|
75
|
+
* unsupported state, so a consumer never has to wait for a frame to ask.
|
|
76
|
+
*
|
|
77
|
+
* @param {(snapshot: Object) => void} listener
|
|
78
|
+
* @returns {() => void}
|
|
79
|
+
*/
|
|
80
|
+
subscribe(listener) {
|
|
81
|
+
if (typeof listener !== 'function') throw new TypeError('clay.presence.subscribe needs a listener function');
|
|
82
|
+
this.listeners.add(listener);
|
|
83
|
+
listener(this.snapshot);
|
|
84
|
+
return () => {
|
|
85
|
+
this.listeners.delete(listener);
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** What the host advertised, asked of it once by the chrome. */
|
|
90
|
+
setCapabilities({ supported = false, recordAwareness = false, pageAwareness = false } = {}) {
|
|
91
|
+
const nextSupported = supported === true;
|
|
92
|
+
const nextRecord = recordAwareness === true;
|
|
93
|
+
const nextPage = pageAwareness === true;
|
|
94
|
+
if (nextSupported === this.supported && nextRecord === this.recordAwareness && nextPage === this.pageAwareness) return this.snapshot;
|
|
95
|
+
this.supported = nextSupported;
|
|
96
|
+
this.recordAwareness = nextRecord;
|
|
97
|
+
this.pageAwareness = nextPage;
|
|
98
|
+
return this.emit();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Take one roster frame. Held and published whether or not the host announced
|
|
103
|
+
* presence: the chrome decides whether to draw, the snapshot says who is there.
|
|
104
|
+
*
|
|
105
|
+
* @param {Object} frame
|
|
106
|
+
* @returns {Object} the snapshot this frame produced
|
|
107
|
+
*/
|
|
108
|
+
receive(frame) {
|
|
109
|
+
const data = frame && typeof frame === 'object' ? frame : {};
|
|
110
|
+
const list = Array.isArray(data.people) ? data.people : [];
|
|
111
|
+
const previous = this.sessions;
|
|
112
|
+
const sessions = new Map();
|
|
113
|
+
const people = [];
|
|
114
|
+
|
|
115
|
+
for (const person of list) {
|
|
116
|
+
if (!person || typeof person.id !== 'string' || !person.id) continue;
|
|
117
|
+
const name = typeof person.name === 'string' && person.name.trim() ? person.name.trim() : 'Someone';
|
|
118
|
+
const theirs = normalizeSessions(person.sessions, previous);
|
|
119
|
+
for (const session of theirs) sessions.set(session.id, session);
|
|
120
|
+
const entry = {
|
|
121
|
+
id: person.id,
|
|
122
|
+
name,
|
|
123
|
+
you: person.you === true,
|
|
124
|
+
editing: normalizeEditing(person.editing),
|
|
125
|
+
sessions: theirs,
|
|
126
|
+
};
|
|
127
|
+
if (typeof person.canEdit === 'boolean') entry.canEdit = person.canEdit;
|
|
128
|
+
people.push(entry);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.people = people;
|
|
132
|
+
this.sessions = sessions;
|
|
133
|
+
const count = data.anonymous;
|
|
134
|
+
this.anonymous = typeof count === 'number' && Number.isFinite(count) && count > 0 ? Math.floor(count) : 0;
|
|
135
|
+
|
|
136
|
+
// The connection the frame came over issues this. Page coordinates cannot be
|
|
137
|
+
// sent without it, so a target held while it was missing goes now.
|
|
138
|
+
const self = normalizeSelfSession(data.selfSession);
|
|
139
|
+
const wasSessionless = !this.session || !this.session.token;
|
|
140
|
+
if (self) this.session = self;
|
|
141
|
+
|
|
142
|
+
const snapshot = this.emit();
|
|
143
|
+
// The first frame of a connection carries the token, and it upgrades whatever
|
|
144
|
+
// this tab already holds: a record target sent in the single-field shape goes
|
|
145
|
+
// again in the extended envelope, and a page target that had no body goes now.
|
|
146
|
+
if (self && wasSessionless && this.owner && this.target !== null) {
|
|
147
|
+
this.sentKey = null;
|
|
148
|
+
this.flush();
|
|
149
|
+
}
|
|
150
|
+
return snapshot;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The stream went away: nobody is known to be present and the token is spent. */
|
|
154
|
+
clear() {
|
|
155
|
+
this.people = [];
|
|
156
|
+
this.anonymous = 0;
|
|
157
|
+
this.sessions = new Map();
|
|
158
|
+
this.session = null;
|
|
159
|
+
this.cancel();
|
|
160
|
+
this.sentKey = null;
|
|
161
|
+
return this.emit();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Publish this tab's target. Advisory and synchronous: a target that is not the
|
|
166
|
+
* one already held is coalesced to one request every 250 ms, and a clear bypasses
|
|
167
|
+
* the wait. Returns false for a target this client will not publish.
|
|
168
|
+
*
|
|
169
|
+
* A target set here belongs to an explicit owner — the page — and that owner
|
|
170
|
+
* keeps the tab's one session until it releases it: the generic editing path is
|
|
171
|
+
* silenced meanwhile, so a caret marker can never overwrite a page selection. A
|
|
172
|
+
* clear keeps the ownership, so `setTarget(null)` publishes nobody anywhere
|
|
173
|
+
* rather than falling back to a positional marker.
|
|
174
|
+
*
|
|
175
|
+
* @param {null|string|{kind: string}} target
|
|
176
|
+
* @returns {boolean}
|
|
177
|
+
*/
|
|
178
|
+
setTarget(target) {
|
|
179
|
+
const result = validateTarget(target === undefined ? null : target);
|
|
180
|
+
if (!result.ok) return false;
|
|
181
|
+
this.explicit = true;
|
|
182
|
+
return this.hold(result.target);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Publish the generic positional marker of the editing path. Ignored while an
|
|
187
|
+
* explicit owner holds the tab's target, and returns false when it is: the page
|
|
188
|
+
* speaks for this tab until its teardown releases the target.
|
|
189
|
+
*
|
|
190
|
+
* @param {null|string} value
|
|
191
|
+
* @returns {boolean}
|
|
192
|
+
*/
|
|
193
|
+
setGeneric(value) {
|
|
194
|
+
if (this.explicit) return false;
|
|
195
|
+
const result = validateTarget(value === undefined ? null : value);
|
|
196
|
+
if (!result.ok) return false;
|
|
197
|
+
return this.hold(result.target);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The owner release, for a page's teardown: the explicit target is gone with the
|
|
202
|
+
* thing that set it, so this clears it and lets the generic editing path speak
|
|
203
|
+
* again. Internal to the store's owners — page code clears one selection with
|
|
204
|
+
* `setTarget(null)` and gives the whole target up with this.
|
|
205
|
+
*
|
|
206
|
+
* @returns {boolean}
|
|
207
|
+
*/
|
|
208
|
+
releaseTarget() {
|
|
209
|
+
if (!this.explicit) return false;
|
|
210
|
+
this.explicit = false;
|
|
211
|
+
return this.hold(null);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Reconcile the target this tab holds with a target that arrived from its owner,
|
|
216
|
+
* without touching who owns it.
|
|
217
|
+
*/
|
|
218
|
+
hold(target) {
|
|
219
|
+
const key = targetKey(target);
|
|
220
|
+
if (key === this.heldKey) {
|
|
221
|
+
// The target did not move, but a stream that went away took its request with
|
|
222
|
+
// it: a target sent before a reconnect is sent again.
|
|
223
|
+
if (this.owner && this.sentKey !== key) this.schedule(COALESCE_MS);
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
this.target = target;
|
|
227
|
+
this.heldKey = key;
|
|
228
|
+
if (target === null) {
|
|
229
|
+
this.cancel();
|
|
230
|
+
this.flush();
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
this.schedule(COALESCE_MS);
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Take ownership of the stream's targets and roster. The returned lease is how an
|
|
239
|
+
* owner gives them back; a lease a newer one replaced detaches nothing.
|
|
240
|
+
*
|
|
241
|
+
* @param {{send: (body: Object) => any}} transport
|
|
242
|
+
* @returns {{active: boolean, detach: () => void}}
|
|
243
|
+
*/
|
|
244
|
+
attachTransport(transport) {
|
|
245
|
+
if (!transport || typeof transport.send !== 'function') {
|
|
246
|
+
throw new TypeError('clay.presence.attachTransport needs a transport with a send function');
|
|
247
|
+
}
|
|
248
|
+
const store = this;
|
|
249
|
+
const lease = {
|
|
250
|
+
send: transport.send,
|
|
251
|
+
get active() {
|
|
252
|
+
return store.owner === lease;
|
|
253
|
+
},
|
|
254
|
+
detach() {
|
|
255
|
+
if (store.owner !== lease) return;
|
|
256
|
+
store.owner = null;
|
|
257
|
+
store.session = null;
|
|
258
|
+
store.n = 0;
|
|
259
|
+
store.sentKey = null;
|
|
260
|
+
store.cancel();
|
|
261
|
+
store.target = null;
|
|
262
|
+
store.heldKey = '';
|
|
263
|
+
store.explicit = false;
|
|
264
|
+
store.clear();
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
this.owner = lease;
|
|
268
|
+
this.session = null;
|
|
269
|
+
this.n = 0;
|
|
270
|
+
this.sentKey = null;
|
|
271
|
+
this.cancel();
|
|
272
|
+
// A fresh connection starts from nothing, so the target this tab holds is sent
|
|
273
|
+
// again rather than assumed delivered.
|
|
274
|
+
if (this.target !== null) this.flush();
|
|
275
|
+
return lease;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Attach this document's one presence transport, chosen from what the host said
|
|
280
|
+
* about itself and never from receiving an event: the relay it accepts targets
|
|
281
|
+
* on, or the same-browser channel its awareness block names. Exactly one — a
|
|
282
|
+
* broadcast host is never probed at the relay, and a relay host never opens a
|
|
283
|
+
* channel — and a host that names neither gets no transport at all.
|
|
284
|
+
*
|
|
285
|
+
* @param {{send?: ?function(Object): any}} [options] - the relay's send, when
|
|
286
|
+
* this tab may publish at all. A reader passes none.
|
|
287
|
+
* @returns {Promise<?{active: boolean, detach: function(): void}>}
|
|
288
|
+
*/
|
|
289
|
+
connect({ send = null } = {}) {
|
|
290
|
+
if (this.connecting) return this.connecting;
|
|
291
|
+
this.connecting = hostMeta()
|
|
292
|
+
.then((meta) => {
|
|
293
|
+
const extensions = Array.isArray(meta?.extensions) ? meta.extensions : [];
|
|
294
|
+
const awareness = meta?.document && typeof meta.document === 'object' ? meta.document.awareness : null;
|
|
295
|
+
if (
|
|
296
|
+
awareness && awareness.transport === 'broadcast' &&
|
|
297
|
+
(extensions.includes('page-awareness-v1') || extensions.includes('record-awareness-v1'))
|
|
298
|
+
) {
|
|
299
|
+
const scope = typeof awareness.scope === 'string' && awareness.scope ? awareness.scope : null;
|
|
300
|
+
return import('./local-presence.js')
|
|
301
|
+
.then(({ localPresence }) => localPresence({ scope, store: this }).start() ?? null);
|
|
302
|
+
}
|
|
303
|
+
if (typeof send === 'function' && extensions.includes('presence-editing')) {
|
|
304
|
+
return this.attachTransport({ send });
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
})
|
|
308
|
+
.finally(() => {
|
|
309
|
+
this.connecting = null;
|
|
310
|
+
});
|
|
311
|
+
return this.connecting;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* The stream reconnected. The host issues a session per connection, so the token
|
|
316
|
+
* this tab held, the order it was sending in and the target it believed delivered
|
|
317
|
+
* are all spent: a page target waits for the new session to name itself, and the
|
|
318
|
+
* generic marker goes again on its own schedule.
|
|
319
|
+
*/
|
|
320
|
+
reconnect() {
|
|
321
|
+
this.session = null;
|
|
322
|
+
this.n = 0;
|
|
323
|
+
this.sentKey = null;
|
|
324
|
+
this.cancel();
|
|
325
|
+
if (!this.owner || this.target === null) return;
|
|
326
|
+
this.flush();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
emit() {
|
|
330
|
+
const people = this.people.map((person) => Object.freeze({
|
|
331
|
+
...person,
|
|
332
|
+
sessions: Object.freeze(person.sessions),
|
|
333
|
+
}));
|
|
334
|
+
this.snapshot = Object.freeze({
|
|
335
|
+
supported: this.supported,
|
|
336
|
+
recordAwareness: this.recordAwareness,
|
|
337
|
+
pageAwareness: this.pageAwareness,
|
|
338
|
+
people: Object.freeze(people),
|
|
339
|
+
anonymous: this.anonymous,
|
|
340
|
+
});
|
|
341
|
+
for (const listener of [...this.listeners]) listener(this.snapshot);
|
|
342
|
+
return this.snapshot;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* One timer. A pending coalescing window keeps its deadline when somebody keeps
|
|
347
|
+
* moving — otherwise a moving caret would never send — while a renewal or a retry
|
|
348
|
+
* is exactly what a new target replaces.
|
|
349
|
+
*/
|
|
350
|
+
schedule(wait, kind = 'coalesce') {
|
|
351
|
+
if (this.timer !== null) {
|
|
352
|
+
if (this.timerKind === 'coalesce') return;
|
|
353
|
+
clearTimeout(this.timer);
|
|
354
|
+
}
|
|
355
|
+
this.timerKind = kind;
|
|
356
|
+
this.timer = setTimeout(() => {
|
|
357
|
+
this.timer = null;
|
|
358
|
+
this.timerKind = null;
|
|
359
|
+
this.flush();
|
|
360
|
+
}, wait);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
cancel() {
|
|
364
|
+
if (this.timer === null) return;
|
|
365
|
+
clearTimeout(this.timer);
|
|
366
|
+
this.timer = null;
|
|
367
|
+
this.timerKind = null;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
renew() {
|
|
371
|
+
if (this.timer !== null || !this.owner || this.target === null) return;
|
|
372
|
+
this.schedule(RENEW_MS, 'renew');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
flush() {
|
|
376
|
+
if (this.inflight) return;
|
|
377
|
+
const owner = this.owner;
|
|
378
|
+
if (!owner) return;
|
|
379
|
+
const n = this.n + 1;
|
|
380
|
+
const body = requestBody({ target: this.target, session: this.session, n });
|
|
381
|
+
if (!body) return;
|
|
382
|
+
this.cancel();
|
|
383
|
+
this.sentKey = this.heldKey;
|
|
384
|
+
this.inflight = true;
|
|
385
|
+
if (body.v === PRESENCE_VERSION) this.n = n;
|
|
386
|
+
|
|
387
|
+
const settle = (ok) => {
|
|
388
|
+
this.inflight = false;
|
|
389
|
+
if (this.owner !== owner) return;
|
|
390
|
+
if (this.heldKey !== this.sentKey) {
|
|
391
|
+
// A newer target arrived while this one was in flight: the latest wins.
|
|
392
|
+
this.flush();
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (ok) this.renew();
|
|
396
|
+
else this.schedule(RENEW_MS, 'renew');
|
|
397
|
+
};
|
|
398
|
+
Promise.resolve(owner.send(body)).then((ok) => settle(Boolean(ok)), () => settle(false));
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export const presence = new PresenceStore();
|
|
403
|
+
|
|
404
|
+
export default presence;
|
|
405
|
+
|
|
406
|
+
// The facade belongs on `clay` the moment it exists. Two entries load this
|
|
407
|
+
// module — the roster chrome a reader gets with its first frame, and the editing
|
|
408
|
+
// sender an editor starts with — and both find the same store here.
|
|
409
|
+
if (typeof window !== 'undefined') {
|
|
410
|
+
window.clay = window.clay || {};
|
|
411
|
+
if (!window.clay.presence) window.clay.presence = presence;
|
|
412
|
+
}
|