@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
|
@@ -5,15 +5,35 @@ import { createStore, safeStorage } from "./visitor-drafts/store.js";
|
|
|
5
5
|
import { createUiMemory } from "./visitor-drafts/ui-memory.js";
|
|
6
6
|
import { rememberSource, downloadUrl } from "./visitor-drafts/source.js";
|
|
7
7
|
import { mountPrompt } from "./visitor-drafts/prompt.js";
|
|
8
|
-
import {
|
|
9
|
-
import { mountOwnerChip } from "./visitor-drafts/owner-chip.js";
|
|
10
|
-
|
|
11
|
-
const DOWNLOAD_URL = "/_/download?as=htmlclay";
|
|
8
|
+
import { DOWNLOAD_URL, backToDraft, editOriginal, inertVisitorDrafts } from "./visitor-drafts/inert.js";
|
|
12
9
|
|
|
13
10
|
let store = null;
|
|
14
11
|
let restoredDetail = null;
|
|
15
12
|
let downloadLink = DOWNLOAD_URL;
|
|
16
13
|
let source = null;
|
|
14
|
+
let ready = Promise.resolve();
|
|
15
|
+
|
|
16
|
+
// The chip is a convenience: a failed import leaves the draft working without it.
|
|
17
|
+
function mountChip() {
|
|
18
|
+
return import("./visitor-drafts/owner-chip.js")
|
|
19
|
+
.then((m) => m.mountOwnerChip())
|
|
20
|
+
.catch((error) => {
|
|
21
|
+
console.warn("visitor-drafts: the owner chip did not load", error);
|
|
22
|
+
return null;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Most visitors never see the intro (no template, a draft already kept, or dismissed),
|
|
27
|
+
// so it is fetched only for a page that has one; a failed import costs only the intro.
|
|
28
|
+
function mountIntroLate(options) {
|
|
29
|
+
if (!options.template) return Promise.resolve(null);
|
|
30
|
+
return import("./visitor-drafts/intro.js")
|
|
31
|
+
.then((m) => m.mountIntro(options))
|
|
32
|
+
.catch((error) => {
|
|
33
|
+
console.warn("visitor-drafts: the intro did not load", error);
|
|
34
|
+
return null;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
17
37
|
|
|
18
38
|
const slot = draftSlot();
|
|
19
39
|
const rulesTag = document.querySelector('script[data-rules-name~="api"]');
|
|
@@ -30,11 +50,11 @@ if (isDraftMode && slot && rulesTag) {
|
|
|
30
50
|
const appName = document.querySelector('meta[name="application-name"]')?.content?.trim() || document.title.split(/:|\s\|\s/)[0].trim() || "This app";
|
|
31
51
|
const template = document.querySelector("template[visitor-intro]");
|
|
32
52
|
const start = () => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
53
|
+
ready = mountPrompt({ memory, appName, link: downloadLink }).then((prompt) => mountIntroLate({ memory, template, appName, hadDraft: result.hadDraft }).then(() => prompt)).then((prompt) => {
|
|
54
|
+
document.addEventListener("clay:drafts-store-failed", () => prompt.showError());
|
|
55
|
+
document.addEventListener("clay:drafts-saved", () => prompt.clearError());
|
|
56
|
+
return (isOwner ? mountChip() : Promise.resolve(null)).then(() => prompt);
|
|
57
|
+
});
|
|
38
58
|
};
|
|
39
59
|
document.readyState === "loading" ? document.addEventListener("DOMContentLoaded", start, { once: true }) : start();
|
|
40
60
|
setDraftFlush(() => store.flush());
|
|
@@ -48,41 +68,10 @@ if (isDraftMode && slot && rulesTag) {
|
|
|
48
68
|
if (!rulesTag) missing.push('a <script data-rules-name~="api"> rules tag');
|
|
49
69
|
console.warn(`visitor-drafts: this page keeps no draft (missing ${missing.join(" and ")})`);
|
|
50
70
|
} else if (isOwner && isEditMode && !hasSaveToken()) {
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export const ready = Promise.resolve();
|
|
55
|
-
|
|
56
|
-
function editOriginal() {
|
|
57
|
-
const url = new URL(window.location.href);
|
|
58
|
-
url.searchParams.set("editmode", "true");
|
|
59
|
-
window.location.href = url.toString();
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function backToDraft() {
|
|
64
|
-
const url = new URL(window.location.href);
|
|
65
|
-
url.searchParams.delete("editmode");
|
|
66
|
-
window.location.href = url.toString();
|
|
67
|
-
return true;
|
|
71
|
+
ready = mountChip();
|
|
68
72
|
}
|
|
69
73
|
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
active: false,
|
|
73
|
-
slot: null,
|
|
74
|
-
key: null,
|
|
75
|
-
hasDraft: () => false,
|
|
76
|
-
restored: () => null,
|
|
77
|
-
saveNow: () => false,
|
|
78
|
-
reset: () => false,
|
|
79
|
-
onRestore: () => false,
|
|
80
|
-
editOriginal,
|
|
81
|
-
backToDraft,
|
|
82
|
-
downloadUrl: () => DOWNLOAD_URL,
|
|
83
|
-
source: null,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
74
|
+
export { ready };
|
|
86
75
|
|
|
87
76
|
export const visitorDrafts = store ? {
|
|
88
77
|
active: true,
|
package/src/plugins/wire.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isEditMode } from "../core/is-edit-mode.js";
|
|
1
|
+
import { isEditMode, modeState } from "../core/is-edit-mode.js";
|
|
2
2
|
import { saveToken } from "../core/host-attrs.js";
|
|
3
3
|
import { hostMeta, hostSupports } from "../core/host-meta.js";
|
|
4
4
|
|
|
@@ -475,6 +475,80 @@ function releaseSaving(rec) {
|
|
|
475
475
|
saveLane?.resumeAutosave();
|
|
476
476
|
}
|
|
477
477
|
|
|
478
|
+
// --- write admission -------------------------------------------------------
|
|
479
|
+
|
|
480
|
+
// A request that asks for the page's own bytes is an edit-session operation: it flushes
|
|
481
|
+
// the page and holds the save lane around the POST, so it may only start while the
|
|
482
|
+
// session it was sent in still admits writes. The lease below closes that admission
|
|
483
|
+
// while an exit drains and leaves an active save hold — this request's own operation —
|
|
484
|
+
// pending until it settles, so an exit cannot report a clean view over an agent that is
|
|
485
|
+
// still writing the file. A read-only request (`document: "none"`, which is every
|
|
486
|
+
// helper and message workflow) is not gated: it writes nothing here.
|
|
487
|
+
let session = null;
|
|
488
|
+
|
|
489
|
+
const admits = () => !session || session.admitted;
|
|
490
|
+
|
|
491
|
+
const fault = (message, code, pending) => Object.assign(new Error(`clay.wire: ${message}`), { code, pending });
|
|
492
|
+
|
|
493
|
+
// The generation is what makes an await safe to come back from. A helper's identity
|
|
494
|
+
// round trip, the save lane's import and the flush all outlive a switch that started
|
|
495
|
+
// while they were in flight; the record is sent for the session it began in, and a
|
|
496
|
+
// stale one ends as an error rather than handing an agent bytes from a session that
|
|
497
|
+
// has gone.
|
|
498
|
+
function admitsRecord(rec) {
|
|
499
|
+
if (rec.document !== "edit") return true;
|
|
500
|
+
if (rec.generation === modeState().generation && admits()) return true;
|
|
501
|
+
finish(rec, "error", "the page changed mode before this request went out", {
|
|
502
|
+
errorCode: "stale-document",
|
|
503
|
+
errorSource: "client",
|
|
504
|
+
});
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* The wire as a mode lease.
|
|
510
|
+
*
|
|
511
|
+
* Nothing to construct: the channel is lazy and belongs to the page. `quiesce` closes
|
|
512
|
+
* admission for edit requests and refuses the exit while one is still open, because
|
|
513
|
+
* that request's save hold is work the barrier cannot flush around — the hold stands
|
|
514
|
+
* until the request itself settles (a terminal frame, a cancel, or its deadline).
|
|
515
|
+
* `resume` reopens admission over the same records. `dispose` (after the barrier) only
|
|
516
|
+
* forgets the session: with none open the page is back to its unnegotiated rule.
|
|
517
|
+
*
|
|
518
|
+
* `initial: true` adopts the boot's rule instead of starting one: an explicit edit boot
|
|
519
|
+
* negotiated no session, so the adopted lease is admitted from the start and enable has
|
|
520
|
+
* nothing left to open.
|
|
521
|
+
*/
|
|
522
|
+
export function wireParticipant() {
|
|
523
|
+
return {
|
|
524
|
+
id: "clay:wire",
|
|
525
|
+
order: 900,
|
|
526
|
+
prepare(context = {}) {
|
|
527
|
+
const lease = { admitted: context.initial === true };
|
|
528
|
+
session = lease;
|
|
529
|
+
return {
|
|
530
|
+
enable() {
|
|
531
|
+
if (session === lease) lease.admitted = true;
|
|
532
|
+
},
|
|
533
|
+
quiesce() {
|
|
534
|
+
if (session !== lease) return;
|
|
535
|
+
lease.admitted = false;
|
|
536
|
+
const pending = [...records.values()]
|
|
537
|
+
.filter((rec) => rec.document === "edit" && (rec.holdsSave || OPEN_STATES.has(rec.state)))
|
|
538
|
+
.map((rec) => rec.id);
|
|
539
|
+
if (pending.length) throw fault("a request on this page has not settled", "pending-input", pending);
|
|
540
|
+
},
|
|
541
|
+
resume() {
|
|
542
|
+
if (session === lease) lease.admitted = true;
|
|
543
|
+
},
|
|
544
|
+
dispose() {
|
|
545
|
+
if (session === lease) session = null;
|
|
546
|
+
},
|
|
547
|
+
};
|
|
548
|
+
},
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
478
552
|
function once(eventName, timeout) {
|
|
479
553
|
return new Promise((resolve) => {
|
|
480
554
|
let done = false;
|
|
@@ -616,6 +690,7 @@ function send(payload, opts = {}) {
|
|
|
616
690
|
landingTimer: null,
|
|
617
691
|
abort: typeof AbortController === "function" ? new AbortController() : null,
|
|
618
692
|
holdsSave: false,
|
|
693
|
+
generation: modeState().generation,
|
|
619
694
|
settle: null,
|
|
620
695
|
signal: opts.signal && typeof opts.signal.addEventListener === "function" ? opts.signal : null,
|
|
621
696
|
signalHandler: null,
|
|
@@ -649,12 +724,16 @@ function send(payload, opts = {}) {
|
|
|
649
724
|
}
|
|
650
725
|
}
|
|
651
726
|
|
|
727
|
+
if (!admitsRecord(rec)) return;
|
|
728
|
+
|
|
652
729
|
if (rec.document === "edit") {
|
|
653
730
|
await holdSaving(rec);
|
|
654
731
|
if (rec.state !== "sent") return;
|
|
732
|
+
if (!admitsRecord(rec)) return;
|
|
655
733
|
|
|
656
734
|
await flushSave();
|
|
657
735
|
if (rec.state !== "sent") return;
|
|
736
|
+
if (!admitsRecord(rec)) return;
|
|
658
737
|
}
|
|
659
738
|
|
|
660
739
|
// Subscribe before posting. A handler can acknowledge in single-digit
|
|
@@ -804,6 +883,8 @@ async function helpers() {
|
|
|
804
883
|
.map(({ name, state }) => ({ name, state }));
|
|
805
884
|
}
|
|
806
885
|
|
|
886
|
+
export { wireParticipant as modeParticipant };
|
|
887
|
+
|
|
807
888
|
export const wire = { send, cancel, get, list, isBusy, on, helpers };
|
|
808
889
|
|
|
809
890
|
export default wire;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// The always-wave core as one module graph for the built release. The chunk build
|
|
2
|
+
// makes this an entry and rewrites the loader's always-wave thunks to read the
|
|
3
|
+
// namespace each path names here, so a page fetches one file instead of six while
|
|
4
|
+
// the loader still activates the core before any plugin, in this order.
|
|
5
|
+
//
|
|
6
|
+
// The import list is CORE_WAVES.always (src/loader-logic.js), written out because
|
|
7
|
+
// import statements cannot be computed. tests/unit/build/reader-core-order.test.js
|
|
8
|
+
// runs the built graph and the source graph and compares their evaluation order, so
|
|
9
|
+
// a module added to that wave without a line here fails a test instead of quietly
|
|
10
|
+
// moving to the end. Source mode never imports this file.
|
|
11
|
+
import * as mutation from "./lib/mutation.js";
|
|
12
|
+
import * as preserveAuthored from "./core/preserve-authored.js";
|
|
13
|
+
import * as editMode from "./core/edit-mode.js";
|
|
14
|
+
import * as modeKey from "./core/mode-key.js";
|
|
15
|
+
import * as staleHostNotice from "./core/stale-host-notice.js";
|
|
16
|
+
import * as pageData from "./core/page-data.js";
|
|
17
|
+
|
|
18
|
+
// Keyed by the logical path the loader and the manifest use.
|
|
19
|
+
export const core = {
|
|
20
|
+
"lib/mutation.js": mutation,
|
|
21
|
+
"core/preserve-authored.js": preserveAuthored,
|
|
22
|
+
"core/edit-mode.js": editMode,
|
|
23
|
+
"core/mode-key.js": modeKey,
|
|
24
|
+
"core/stale-host-notice.js": staleHostNotice,
|
|
25
|
+
"core/page-data.js": pageData,
|
|
26
|
+
};
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* conflicts.js — the durable half of the conflict ledger.
|
|
3
|
+
*
|
|
4
|
+
* The ledger (sync/conflicts.js) keeps what an incoming frame won over this tab's
|
|
5
|
+
* editing, and it keeps it in memory: the root the merge read as "mine", the nodes
|
|
6
|
+
* the report pointed at, the conflict records themselves. A reload takes every one
|
|
7
|
+
* of those away, and replaying the paths afterwards would point at a DOM that is
|
|
8
|
+
* not the one they were recorded against. So the ledger hands this module the
|
|
9
|
+
* material it cannot rebuild — the pre-apply root as bytes, and the conflicts as
|
|
10
|
+
* plain fields — and this module stores them in the shared recovery store.
|
|
11
|
+
*
|
|
12
|
+
* One record per apply: the root is stored once, and the conflicts reference it.
|
|
13
|
+
* The word "durable" means one completed IndexedDB transaction, and nothing more:
|
|
14
|
+
* a stored record is not a save, and the page never renders from it.
|
|
15
|
+
*
|
|
16
|
+
* What reaches IndexedDB is strings, numbers and booleans. A DOM node, a function,
|
|
17
|
+
* a Map or anything else the structured clone algorithm would refuse is dropped
|
|
18
|
+
* from the record and named in `dropped`, because one uncloneable field must not
|
|
19
|
+
* cost somebody the whole copy. The store's own refusal (quota, unknown schema,
|
|
20
|
+
* an unreachable database) is reported as a code and never removes the live
|
|
21
|
+
* conflict, which is still the person's copy in this tab.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { storage } from "../lib/doc-storage.js";
|
|
25
|
+
|
|
26
|
+
export const CONFLICTS_CHANNEL = "clay.conflicts";
|
|
27
|
+
export const CONFLICTS_VERSION = 1;
|
|
28
|
+
export const CONFLICTS_KIND = "conflict";
|
|
29
|
+
|
|
30
|
+
// A cheap read for the notice's shell, which must not open the database on every
|
|
31
|
+
// edit boot. It says only that this document has stored conflicts to review.
|
|
32
|
+
const HINT_KEY = "conflicts";
|
|
33
|
+
|
|
34
|
+
const CONTEXT_FIELDS = ["kind", "id", "label", "field"];
|
|
35
|
+
|
|
36
|
+
function recoveryService() {
|
|
37
|
+
const clay = globalThis.window?.clay ?? globalThis.clay;
|
|
38
|
+
return clay?.recovery ?? null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let registered = null;
|
|
42
|
+
|
|
43
|
+
function channel() {
|
|
44
|
+
const service = recoveryService();
|
|
45
|
+
if (!service || typeof service.register !== "function") return null;
|
|
46
|
+
if (registered?.service === service) return registered.channel;
|
|
47
|
+
try {
|
|
48
|
+
registered = {
|
|
49
|
+
service,
|
|
50
|
+
channel: service.register({ channel: CONFLICTS_CHANNEL, version: CONFLICTS_VERSION }),
|
|
51
|
+
};
|
|
52
|
+
} catch (_) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return registered.channel;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* How many conflicts this document's store is holding for review, without opening
|
|
60
|
+
* the database. Zero means the notice's shell stays asleep.
|
|
61
|
+
*
|
|
62
|
+
* @returns {number}
|
|
63
|
+
*/
|
|
64
|
+
export function pendingConflicts() {
|
|
65
|
+
const stored = storage.local.get(HINT_KEY);
|
|
66
|
+
return Number.isFinite(stored?.count) && stored.count > 0 ? stored.count : 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function noteHint(count) {
|
|
70
|
+
if (count > 0) storage.local.set(HINT_KEY, { count, at: Date.now() });
|
|
71
|
+
else storage.local.remove(HINT_KEY);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const DROPPED = Symbol("dropped");
|
|
75
|
+
|
|
76
|
+
function isPlainObject(value) {
|
|
77
|
+
if (!value || typeof value !== "object") return false;
|
|
78
|
+
const proto = Object.getPrototypeOf(value);
|
|
79
|
+
return proto === Object.prototype || proto === null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function copyField(value, dropped, field) {
|
|
83
|
+
const type = typeof value;
|
|
84
|
+
if (value === null || type === "string" || type === "number" || type === "boolean") return value;
|
|
85
|
+
if (type === "undefined") return DROPPED;
|
|
86
|
+
if (type !== "object") {
|
|
87
|
+
dropped.push(field);
|
|
88
|
+
return DROPPED;
|
|
89
|
+
}
|
|
90
|
+
if (Array.isArray(value)) {
|
|
91
|
+
return value.map((item, at) => {
|
|
92
|
+
const kept = copyField(item, dropped, `${field}[${at}]`);
|
|
93
|
+
return kept === DROPPED ? null : kept;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (!isPlainObject(value)) {
|
|
97
|
+
dropped.push(field);
|
|
98
|
+
return DROPPED;
|
|
99
|
+
}
|
|
100
|
+
const out = {};
|
|
101
|
+
for (const [key, item] of Object.entries(value)) {
|
|
102
|
+
const kept = copyField(item, dropped, field ? `${field}.${key}` : key);
|
|
103
|
+
if (kept !== DROPPED) out[key] = kept;
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* A copy that survives the trip into IndexedDB: plain objects, arrays, strings,
|
|
110
|
+
* numbers and booleans, with everything else dropped and named.
|
|
111
|
+
*
|
|
112
|
+
* @param {*} value
|
|
113
|
+
* @param {string[]} [dropped] - collects the fields that could not be stored
|
|
114
|
+
* @param {string} [field]
|
|
115
|
+
* @returns {*}
|
|
116
|
+
*/
|
|
117
|
+
export function plainCopy(value, dropped = [], field = "") {
|
|
118
|
+
const kept = copyField(value, dropped, field);
|
|
119
|
+
return kept === DROPPED ? null : kept;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The part of a conflict's context that can be stored: its identity, its label and
|
|
124
|
+
* its field. The resolver's `open` is a function bound to this page's tree, so it
|
|
125
|
+
* lives on the live record and never here.
|
|
126
|
+
*
|
|
127
|
+
* @param {*} context
|
|
128
|
+
* @returns {?Object}
|
|
129
|
+
*/
|
|
130
|
+
export function storedContext(context) {
|
|
131
|
+
if (!context || typeof context !== "object") return null;
|
|
132
|
+
const out = {};
|
|
133
|
+
for (const field of CONTEXT_FIELDS) {
|
|
134
|
+
const value = context[field];
|
|
135
|
+
if (typeof value === "string") {
|
|
136
|
+
if (value) out[field] = value;
|
|
137
|
+
} else if (typeof value === "number" && Number.isFinite(value)) {
|
|
138
|
+
out[field] = String(value);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return Object.keys(out).length ? out : null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Copy one apply and its conflicts into the recovery store, before the ledger
|
|
146
|
+
* releases any of it.
|
|
147
|
+
*
|
|
148
|
+
* Returns synchronously with the staged ids: the copy is already in memory and the
|
|
149
|
+
* write is under way. It is a recovery copy, not a durable receipt.
|
|
150
|
+
*
|
|
151
|
+
* @param {Object} spec
|
|
152
|
+
* @param {Object} spec.apply - the ledger's apply, without its DOM roots
|
|
153
|
+
* @param {?string} spec.root - the pre-apply root as saved bytes
|
|
154
|
+
* @param {Object[]} spec.conflicts - plain conflict entries
|
|
155
|
+
* @returns {{id: *, revision: number}[]}
|
|
156
|
+
*/
|
|
157
|
+
export function recordConflicts({ apply = {}, root = null, conflicts = [] } = {}) {
|
|
158
|
+
const entries = [];
|
|
159
|
+
for (const conflict of conflicts) {
|
|
160
|
+
const dropped = [];
|
|
161
|
+
const entry = plainCopy(conflict, dropped, "");
|
|
162
|
+
if (!entry || typeof entry !== "object" || !entry.id) continue;
|
|
163
|
+
if (dropped.length) entry.dropped = dropped;
|
|
164
|
+
entries.push(entry);
|
|
165
|
+
}
|
|
166
|
+
if (!entries.length) return [];
|
|
167
|
+
const payload = {
|
|
168
|
+
apply: plainCopy(apply, [], "") ?? {},
|
|
169
|
+
root: typeof root === "string" ? root : null,
|
|
170
|
+
savedAt: Date.now(),
|
|
171
|
+
conflicts: entries,
|
|
172
|
+
};
|
|
173
|
+
try {
|
|
174
|
+
const ch = channel();
|
|
175
|
+
if (!ch) return [];
|
|
176
|
+
const staged = ch.stage({ key: String(apply.id ?? ""), kind: CONFLICTS_KIND, payload });
|
|
177
|
+
ch.flush().catch(() => {});
|
|
178
|
+
noteHint(pendingConflicts() + entries.length);
|
|
179
|
+
return [{ id: staged.id, revision: staged.revision }];
|
|
180
|
+
} catch (err) {
|
|
181
|
+
// The live conflict is the copy that matters right now: nothing is removed here.
|
|
182
|
+
console.error("clayjs: the conflict could not be stored for a later reload; it is still here", err);
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const textOf = (value) => (typeof value === "string" ? value : value == null ? "" : String(value));
|
|
188
|
+
|
|
189
|
+
function usableConflict(conflict) {
|
|
190
|
+
if (!conflict || typeof conflict !== "object") return null;
|
|
191
|
+
const id = textOf(conflict.id);
|
|
192
|
+
if (!id) return null;
|
|
193
|
+
const context = storedContext(conflict.context);
|
|
194
|
+
return {
|
|
195
|
+
id,
|
|
196
|
+
kind: textOf(conflict.kind),
|
|
197
|
+
key: textOf(conflict.key),
|
|
198
|
+
name: textOf(conflict.name),
|
|
199
|
+
sentence: textOf(conflict.sentence),
|
|
200
|
+
yours: textOf(conflict.yours),
|
|
201
|
+
now: textOf(conflict.now),
|
|
202
|
+
context,
|
|
203
|
+
dropped: Array.isArray(conflict.dropped) ? conflict.dropped.map(textOf) : [],
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function usableRecord(record) {
|
|
208
|
+
const payload = record?.payload;
|
|
209
|
+
if (!payload || typeof payload !== "object") return null;
|
|
210
|
+
const conflicts = Array.isArray(payload.conflicts) ? payload.conflicts.map(usableConflict).filter(Boolean) : [];
|
|
211
|
+
if (!conflicts.length) return null;
|
|
212
|
+
const apply = payload.apply && typeof payload.apply === "object" ? payload.apply : {};
|
|
213
|
+
return {
|
|
214
|
+
id: record.id,
|
|
215
|
+
revision: record.revision,
|
|
216
|
+
supported: record.supported !== false,
|
|
217
|
+
savedAt: Number.isFinite(payload.savedAt) ? payload.savedAt : null,
|
|
218
|
+
apply,
|
|
219
|
+
root: typeof payload.root === "string" ? payload.root : null,
|
|
220
|
+
conflicts,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Every stored conflict for this document, from every writer: a record does not
|
|
226
|
+
* belong to the tab that made it, because the tab that made it may be gone.
|
|
227
|
+
*
|
|
228
|
+
* @returns {Promise<{ok: boolean, code?: string, records: Object[], status?: Object}>}
|
|
229
|
+
*/
|
|
230
|
+
export async function listConflicts() {
|
|
231
|
+
const ch = channel();
|
|
232
|
+
if (!ch) return { ok: true, records: [] };
|
|
233
|
+
let listed;
|
|
234
|
+
try {
|
|
235
|
+
listed = await ch.list();
|
|
236
|
+
} catch (_) {
|
|
237
|
+
return { ok: false, code: "error", records: [] };
|
|
238
|
+
}
|
|
239
|
+
if (!listed.ok) return { ok: false, code: listed.code, records: [] };
|
|
240
|
+
const records = listed.records.map(usableRecord).filter(Boolean);
|
|
241
|
+
noteHint(records.reduce((sum, record) => sum + record.conflicts.length, 0));
|
|
242
|
+
return { ok: true, records, status: listed.status };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Acknowledge conflicts, by the id the ledger gave them. This is the only way a
|
|
247
|
+
* stored conflict leaves: a person said so, or a live conflict was accepted,
|
|
248
|
+
* reverted or reconciled and its own record has already gone.
|
|
249
|
+
*
|
|
250
|
+
* Only the named conflicts go. A record that still holds another conflict keeps
|
|
251
|
+
* its root, so the surviving text is never lost because a neighbour was dismissed;
|
|
252
|
+
* the remainder is written under a new revision before the old one is deleted, so
|
|
253
|
+
* a failed write leaves the original exactly as it was.
|
|
254
|
+
*
|
|
255
|
+
* @param {string[]} ids
|
|
256
|
+
* @returns {Promise<{ok: boolean, code?: string, removed?: number}>}
|
|
257
|
+
*/
|
|
258
|
+
export async function dismissConflicts(ids) {
|
|
259
|
+
const wanted = new Set((Array.isArray(ids) ? ids : [ids]).filter((id) => id != null).map(String));
|
|
260
|
+
if (!wanted.size) return { ok: true, removed: 0 };
|
|
261
|
+
const ch = channel();
|
|
262
|
+
if (!ch) return { ok: true, removed: 0 };
|
|
263
|
+
const listed = await listConflicts();
|
|
264
|
+
if (!listed.ok) return { ok: false, code: listed.code };
|
|
265
|
+
let removed = 0;
|
|
266
|
+
for (const record of listed.records) {
|
|
267
|
+
const remaining = record.conflicts.filter((entry) => !wanted.has(entry.id));
|
|
268
|
+
if (remaining.length === record.conflicts.length) continue;
|
|
269
|
+
removed += record.conflicts.length - remaining.length;
|
|
270
|
+
if (!remaining.length) {
|
|
271
|
+
await ch.dismiss(record.id, { revision: record.revision });
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
ch.stage({
|
|
275
|
+
key: String(record.id),
|
|
276
|
+
kind: CONFLICTS_KIND,
|
|
277
|
+
payload: { apply: record.apply, root: record.root, savedAt: record.savedAt, conflicts: remaining },
|
|
278
|
+
});
|
|
279
|
+
const written = await ch.flush();
|
|
280
|
+
if (!written.ok) continue;
|
|
281
|
+
await ch.dismiss(record.id, { revision: record.revision });
|
|
282
|
+
}
|
|
283
|
+
await listConflicts();
|
|
284
|
+
// The notice may be holding a record this just changed.
|
|
285
|
+
globalThis.document?.dispatchEvent(new CustomEvent("clay:conflict-store-changed"));
|
|
286
|
+
return { ok: true, removed };
|
|
287
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* identity.js — which recovery records belong to this document, and who is writing.
|
|
3
|
+
*
|
|
4
|
+
* Two tabs on one file must not overwrite each other's unsaved writing, and two
|
|
5
|
+
* documents on one origin (every file on a hyperclay.com subdomain) must not share
|
|
6
|
+
* a record at all. So a record is filed under a four-part scope plus a writer, and
|
|
7
|
+
* this module is the one place that decides both.
|
|
8
|
+
*
|
|
9
|
+
* The document half is the host's own file identity: `documentid`, or the pre-rename
|
|
10
|
+
* `htmlclayid` a document saved before that rename still carries on disk. It is read
|
|
11
|
+
* from HOST_IDENTITY_ATTRS, never from the save-token list, because a name in that
|
|
12
|
+
* list becomes a credential in the save URL. The root is read ONCE, here: a morph or
|
|
13
|
+
* a stream restart can rewrite the root afterwards, and a re-read would file this
|
|
14
|
+
* tab's writing under a document it is not looking at. `check()` reads the root
|
|
15
|
+
* again only to report that mismatch, and never to change the scope.
|
|
16
|
+
*
|
|
17
|
+
* Without a host identity the exact pathname is the identity: no query, no hash, and
|
|
18
|
+
* no decoding of distinct paths into one. A draft is always path-scoped, because a
|
|
19
|
+
* visitor's draft never reaches the host's file and this browser is the only place
|
|
20
|
+
* it lives. Nothing here makes a request; a draft must not ask the host anything.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { HOST_IDENTITY_ATTRS } from "../lib/root-attrs.js";
|
|
24
|
+
|
|
25
|
+
export const PATH_KIND = "path";
|
|
26
|
+
export const EDIT_MODE = "edit";
|
|
27
|
+
export const DRAFT_MODE = "draft";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A fresh writer id, once per page load.
|
|
31
|
+
*
|
|
32
|
+
* Never from sessionStorage: a duplicated tab inherits that storage, and two tabs
|
|
33
|
+
* holding one writer id would overwrite each other's recovery for the same key.
|
|
34
|
+
*
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
export function newWriterId() {
|
|
38
|
+
const minted = globalThis.crypto?.randomUUID?.();
|
|
39
|
+
if (minted) return minted;
|
|
40
|
+
return `wr-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The durable file identity this response carries, or null.
|
|
45
|
+
*
|
|
46
|
+
* Host-injected and stripped before saving, so it names a file and not a revision.
|
|
47
|
+
* A response carrying neither spelling is a downloaded copy, a local file, or a
|
|
48
|
+
* host that mints no identity, and all three fall back to the pathname.
|
|
49
|
+
*
|
|
50
|
+
* @param {?Element} [root] - the document's root element
|
|
51
|
+
* @returns {?{kind: string, identity: string}}
|
|
52
|
+
*/
|
|
53
|
+
export function hostIdentityOf(root = globalThis.document?.documentElement) {
|
|
54
|
+
if (!root || typeof root.getAttribute !== "function") return null;
|
|
55
|
+
for (const attr of HOST_IDENTITY_ATTRS) {
|
|
56
|
+
const value = root.getAttribute(attr);
|
|
57
|
+
if (value) return { kind: attr, identity: value };
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The pathname as an identity: no query, no hash, nothing decoded.
|
|
64
|
+
*
|
|
65
|
+
* Two URLs that differ only in how they were opened are one document. A caller
|
|
66
|
+
* that hands over a whole URL string still gets the path, because filing one
|
|
67
|
+
* document's writing under two identities is how recovery goes missing.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} pathname
|
|
70
|
+
* @returns {{kind: string, identity: string}}
|
|
71
|
+
*/
|
|
72
|
+
export function pathIdentityOf(pathname) {
|
|
73
|
+
const identity = String(pathname || "/").split(/[?#]/)[0];
|
|
74
|
+
return { kind: PATH_KIND, identity: identity || "/" };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* This page's recovery identity: its scope, its writer, and a check on the root.
|
|
79
|
+
*
|
|
80
|
+
* @param {Object} [options]
|
|
81
|
+
* @param {string} [options.mode] - "edit" or "draft"
|
|
82
|
+
* @param {?Element} [options.root]
|
|
83
|
+
* @param {?Location} [options.location]
|
|
84
|
+
* @param {string} [options.pathname]
|
|
85
|
+
* @param {string} [options.origin]
|
|
86
|
+
* @param {string} [options.writerId]
|
|
87
|
+
* @returns {Object}
|
|
88
|
+
*/
|
|
89
|
+
/**
|
|
90
|
+
* Whether two authorities are the same document, seen by the same account.
|
|
91
|
+
*
|
|
92
|
+
* `principalId` is the account, never a token, and a changed login is another principal:
|
|
93
|
+
* a journal is replayed into the authority and principal it was written under or not at
|
|
94
|
+
* all. A missing field is never equal to a missing field.
|
|
95
|
+
*
|
|
96
|
+
* @param {?Object} a
|
|
97
|
+
* @param {?Object} b
|
|
98
|
+
* @returns {boolean}
|
|
99
|
+
*/
|
|
100
|
+
export function sameAuthority(a, b) {
|
|
101
|
+
if (!a || !b) return false;
|
|
102
|
+
if (!a.host || !a.documentId || !a.principalId) return false;
|
|
103
|
+
return a.host === b.host && a.documentId === b.documentId && a.principalId === b.principalId;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function createIdentity({
|
|
107
|
+
mode = EDIT_MODE,
|
|
108
|
+
root = globalThis.document?.documentElement,
|
|
109
|
+
location: where = globalThis.window?.location,
|
|
110
|
+
pathname,
|
|
111
|
+
origin,
|
|
112
|
+
writerId,
|
|
113
|
+
} = {}) {
|
|
114
|
+
const scopeMode = mode === DRAFT_MODE ? DRAFT_MODE : EDIT_MODE;
|
|
115
|
+
const path = pathname ?? where?.pathname ?? "/";
|
|
116
|
+
const from = origin ?? where?.origin ?? "";
|
|
117
|
+
const pathIdentity = pathIdentityOf(path);
|
|
118
|
+
const hostIdentity = scopeMode === DRAFT_MODE ? null : hostIdentityOf(root);
|
|
119
|
+
const captured = hostIdentity ?? pathIdentity;
|
|
120
|
+
const scope = [from, captured.kind, captured.identity, scopeMode];
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
origin: from,
|
|
124
|
+
pathname: path,
|
|
125
|
+
mode: scopeMode,
|
|
126
|
+
scope,
|
|
127
|
+
writerId: writerId || newWriterId(),
|
|
128
|
+
identityKind: captured.kind,
|
|
129
|
+
identity: captured.identity,
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Is the root still naming the document this page filed its records under?
|
|
133
|
+
*
|
|
134
|
+
* A mismatch is reported, never applied: records written under one identity are
|
|
135
|
+
* not moved into another document's scope by a later root rewrite. A host
|
|
136
|
+
* identity that appears after this load is a later binding, and the records stay
|
|
137
|
+
* where they are until a confirmed move offers both scopes for review.
|
|
138
|
+
*
|
|
139
|
+
* @param {?Element} [next]
|
|
140
|
+
* @returns {{ok: boolean, code: ?string}}
|
|
141
|
+
*/
|
|
142
|
+
check(next = globalThis.document?.documentElement) {
|
|
143
|
+
const found = hostIdentityOf(next);
|
|
144
|
+
if (!hostIdentity) {
|
|
145
|
+
return found ? { ok: true, code: null, bound: found, scope } : { ok: true, code: null };
|
|
146
|
+
}
|
|
147
|
+
if (!found) return { ok: false, code: "identity-missing", captured: hostIdentity, scope };
|
|
148
|
+
if (found.kind !== hostIdentity.kind || found.identity !== hostIdentity.identity) {
|
|
149
|
+
return { ok: false, code: "identity-mismatch", captured: hostIdentity, found, scope };
|
|
150
|
+
}
|
|
151
|
+
return { ok: true, code: null };
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|