@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,535 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* saved-stream.js — the receive-only transport a reader follows saved files with.
|
|
3
|
+
*
|
|
4
|
+
* A reader has no write half. It needs the saved lane's startup GET, its frames and
|
|
5
|
+
* its reconnect repair, and nothing else: no POST, no PATCH, no live relay, no page
|
|
6
|
+
* store to admit anything into, and no write credentials. So this is a transport of
|
|
7
|
+
* its own rather than a mode of live-sync.js, and it imports neither the save lane,
|
|
8
|
+
* the live lane, the snapshot pipeline, the merge engine nor the editor.
|
|
9
|
+
*
|
|
10
|
+
* It owns a REPRESENTED RECEIPT — `{ bytes, etag, seq }` — and moves it only when
|
|
11
|
+
* `onSnapshot` acknowledges a complete apply. `bytes` is one unmodified HTML string
|
|
12
|
+
* from an accepted frame or GET, `etag` belongs to those bytes, and `seq` is the
|
|
13
|
+
* resume floor that apply is known to cover. Transport watermarks never become part
|
|
14
|
+
* of it: a cursor says what the stream will replay, not what this page holds.
|
|
15
|
+
*
|
|
16
|
+
* Branches, spelled out because each one is a way to show a reader a file it does
|
|
17
|
+
* not hold or to hide one it does:
|
|
18
|
+
*
|
|
19
|
+
* - a full frame with a newer sequence validates and applies; a duplicate or an
|
|
20
|
+
* older one is ignored without moving the receipt;
|
|
21
|
+
* - a frame with no sequence (an unstamped legacy host) is processed in arrival
|
|
22
|
+
* order and keeps `seq: null` rather than inventing one;
|
|
23
|
+
* - a cursor with `resync: true`, the startup version check and any content-less
|
|
24
|
+
* external-change or ETag notice refetch the served file with `cache: 'no-store'`;
|
|
25
|
+
* a stamp alone never repairs missing bytes, and a notice never moves the receipt;
|
|
26
|
+
* - a cursor jump without resync is transport bookkeeping only: other lanes consume
|
|
27
|
+
* numbers, so a gap does not prove a content frame is missing;
|
|
28
|
+
* - every host sends a sequence, so that orders the frames: a newer one applies and
|
|
29
|
+
* one the receipt already covers is ignored. A stamp confirms identity only when
|
|
30
|
+
* both sides carry one;
|
|
31
|
+
* - a frame repeating the bytes the article holds moves the receipt and repaints
|
|
32
|
+
* nothing;
|
|
33
|
+
* - after a GET the cursor is a covered replay floor, not an ordering of versions.
|
|
34
|
+
* The fetched body's version is unconfirmed until a frame names it, and a frame
|
|
35
|
+
* naming another stamp refetches rather than overwrite bytes that may be newer. A
|
|
36
|
+
* host that stamps neither side is ordered by sequence;
|
|
37
|
+
* - a body read while frames were still arriving is discarded in favour of one the
|
|
38
|
+
* stream can place, but only twice: a steady writer would otherwise starve the
|
|
39
|
+
* reader of every fetched body;
|
|
40
|
+
* - a bad frame, a refused apply or a failed GET keeps the article and the receipt
|
|
41
|
+
* and reports a recoverable status; it never pretends the file was acknowledged;
|
|
42
|
+
* - stop aborts and invalidates every outstanding continuation, and nothing calls
|
|
43
|
+
* back after it.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import { SyncStream } from './stream.js';
|
|
47
|
+
import { resolveWireProfile } from './wire-profile.js';
|
|
48
|
+
|
|
49
|
+
const LANE = 'saved';
|
|
50
|
+
const MAX_DISCARDS = 2;
|
|
51
|
+
|
|
52
|
+
function mintClientId() {
|
|
53
|
+
return Math.random().toString(36).slice(2, 11) + Date.now().toString(36);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function mintResumeId() {
|
|
57
|
+
try {
|
|
58
|
+
const bytes = new Uint8Array(16);
|
|
59
|
+
(globalThis.crypto || window.crypto).getRandomValues(bytes);
|
|
60
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
61
|
+
} catch {
|
|
62
|
+
return Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The version a fetched served document names on its own root, or null. Read from
|
|
68
|
+
* the body, never from discovery: discovery answers about a later moment than any
|
|
69
|
+
* response. Parsed detached, so this touches the live page nowhere.
|
|
70
|
+
*/
|
|
71
|
+
function readBodyStamp(html) {
|
|
72
|
+
if (typeof html !== 'string' || html === '') return null;
|
|
73
|
+
const parsed = new DOMParser().parseFromString(html, 'text/html');
|
|
74
|
+
return parsed.documentElement?.getAttribute('documentetag') || null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const stampOf = (value) => (typeof value === 'string' && value ? value : null);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The page URL every request in this module resolves against, or null when the page
|
|
81
|
+
* has no host to follow. `file://` and a sandboxed frame have an opaque origin:
|
|
82
|
+
* `window.location.origin` is the string `"null"` there and `new URL` refuses it as
|
|
83
|
+
* a base, and a `file:` page URL names no server either way. Opening a stream on
|
|
84
|
+
* such a page is not a failure to report, it is a request that does not exist.
|
|
85
|
+
*/
|
|
86
|
+
function resolvePageURL(documentURL) {
|
|
87
|
+
const origin = window.location.origin;
|
|
88
|
+
// The string `"null"` is what an opaque origin reports, and `new URL` refuses it
|
|
89
|
+
// as a base before any `file:` page URL is even read.
|
|
90
|
+
if (typeof origin !== 'string' || origin === '' || origin === 'null') return null;
|
|
91
|
+
const url = new URL(documentURL || window.location.href, origin);
|
|
92
|
+
return url.protocol === 'http:' || url.protocol === 'https:' ? url.href : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const seqOf = (value) => (Number.isSafeInteger(value) ? value : null);
|
|
96
|
+
|
|
97
|
+
function normalizeReceipt(represented) {
|
|
98
|
+
if (represented == null) return Object.freeze({ bytes: null, etag: null, seq: null });
|
|
99
|
+
if (typeof represented.bytes !== 'string' || represented.bytes === '') {
|
|
100
|
+
throw new TypeError('createSavedStream: represented.bytes must be a full HTML string');
|
|
101
|
+
}
|
|
102
|
+
return Object.freeze({
|
|
103
|
+
bytes: represented.bytes,
|
|
104
|
+
etag: stampOf(represented.etag),
|
|
105
|
+
seq: seqOf(represented.seq),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Follow saved frames for one reader.
|
|
111
|
+
*
|
|
112
|
+
* `onSnapshot(candidate)` runs synchronously and returns true only after the whole
|
|
113
|
+
* candidate applied, so the receipt moves on acknowledgement and nowhere else. The
|
|
114
|
+
* page URL is resolved against the real origin, never `document.baseURI`: a `<base>`
|
|
115
|
+
* in the authored document must not choose where the reader streams or fetches.
|
|
116
|
+
*
|
|
117
|
+
* @param {Object} options
|
|
118
|
+
* @param {string} [options.documentURL] - The page whose saved file to follow.
|
|
119
|
+
* @param {?string} [options.servedEtag] - The stamp this response was served with.
|
|
120
|
+
* @param {?{bytes: string, etag: ?string, seq: ?number}} [options.represented]
|
|
121
|
+
* @param {function({bytes: string, etag: ?string, seq: ?number}): boolean} options.onSnapshot
|
|
122
|
+
* @param {function(Object)} [options.onStatus]
|
|
123
|
+
* @returns {{start: function(): Object, adopt: function(Object): Object, stop: function(), ready: ?Promise, represented: Object}}
|
|
124
|
+
*/
|
|
125
|
+
export function createSavedStream({ documentURL, servedEtag = null, represented = null, onSnapshot, onStatus } = {}) {
|
|
126
|
+
if (typeof onSnapshot !== 'function') {
|
|
127
|
+
throw new TypeError('createSavedStream: onSnapshot must be a function');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const pageURL = resolvePageURL(documentURL);
|
|
131
|
+
const startupCheck = Boolean(stampOf(servedEtag));
|
|
132
|
+
|
|
133
|
+
let receipt = normalizeReceipt(represented);
|
|
134
|
+
// An opaque origin — `file://`, a sandboxed frame — has no host to stream from, and no
|
|
135
|
+
// URL to resolve one against: there is no stream, and the reader holds the served
|
|
136
|
+
// article alone. Returning an inert handle keeps that a no-stream rather than an
|
|
137
|
+
// exception on the way to one.
|
|
138
|
+
if (pageURL === null) {
|
|
139
|
+
const idle = {
|
|
140
|
+
ready: null,
|
|
141
|
+
get represented() {
|
|
142
|
+
return receipt;
|
|
143
|
+
},
|
|
144
|
+
adopt() {
|
|
145
|
+
return idle;
|
|
146
|
+
},
|
|
147
|
+
start() {
|
|
148
|
+
idle.ready = Promise.resolve(idle);
|
|
149
|
+
return idle;
|
|
150
|
+
},
|
|
151
|
+
stop() {},
|
|
152
|
+
};
|
|
153
|
+
return idle;
|
|
154
|
+
}
|
|
155
|
+
// Whether the receipt's version is known to sit at the stream's position, so a
|
|
156
|
+
// newer frame applies straight from the stream. An accepted GET body leaves this
|
|
157
|
+
// false until a frame names its stamp.
|
|
158
|
+
let matched = true;
|
|
159
|
+
let watermark = receipt.seq ?? 0;
|
|
160
|
+
let arrivals = 0;
|
|
161
|
+
let discards = 0;
|
|
162
|
+
// Where an acknowledged apply goes: the caller's `onSnapshot`, or the reader that
|
|
163
|
+
// adopted this stream at acquisition (`adopt`).
|
|
164
|
+
let sink = onSnapshot;
|
|
165
|
+
let notify = onStatus;
|
|
166
|
+
let generation = 0;
|
|
167
|
+
let stopped = false;
|
|
168
|
+
let stream = null;
|
|
169
|
+
let repairJob = null;
|
|
170
|
+
let pendingRepair = null;
|
|
171
|
+
let repairPaused = false;
|
|
172
|
+
// The roster this reader draws arrives as a named `presence` frame on this same
|
|
173
|
+
// stream — there is no second connection — and the store behind it is a first-use
|
|
174
|
+
// download, so a host that names nobody costs a reader nothing. A reader has no
|
|
175
|
+
// write half, so the lease it takes cannot publish at all: its send refuses, and no
|
|
176
|
+
// request leaves for a target a page tried to set in view mode.
|
|
177
|
+
let presenceStore = null;
|
|
178
|
+
let presenceLease = null;
|
|
179
|
+
let presenceLoad = null;
|
|
180
|
+
const presenceQueue = [];
|
|
181
|
+
|
|
182
|
+
const active = (gen) => !stopped && gen === generation;
|
|
183
|
+
|
|
184
|
+
function report(state, detail = {}) {
|
|
185
|
+
if (stopped || typeof notify !== 'function') return;
|
|
186
|
+
notify({ state, ...detail });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function leavePresence() {
|
|
190
|
+
presenceQueue.length = 0;
|
|
191
|
+
if (presenceLease) {
|
|
192
|
+
presenceLease.detach();
|
|
193
|
+
presenceLease = null;
|
|
194
|
+
}
|
|
195
|
+
presenceStore = null;
|
|
196
|
+
presenceLoad = null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// A frame that lands while the download is in flight waits in order, and a
|
|
200
|
+
// download that failed keeps the queue so the next frame asks again.
|
|
201
|
+
function takePresence(data) {
|
|
202
|
+
if (presenceStore) {
|
|
203
|
+
presenceStore.receive(data);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
presenceQueue.push(data);
|
|
207
|
+
if (presenceLoad) return;
|
|
208
|
+
presenceLoad = import('./presence-store.js')
|
|
209
|
+
.then(({ presence }) => {
|
|
210
|
+
presenceStore = presence;
|
|
211
|
+
presenceLease = presence.attachTransport({ send: () => Promise.resolve(false) });
|
|
212
|
+
for (const frame of presenceQueue.splice(0)) presence.receive(frame);
|
|
213
|
+
return presence;
|
|
214
|
+
})
|
|
215
|
+
.catch(() => {
|
|
216
|
+
presenceLoad = null;
|
|
217
|
+
return null;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function closeStream() {
|
|
222
|
+
leavePresence();
|
|
223
|
+
if (!stream) return;
|
|
224
|
+
const closing = stream;
|
|
225
|
+
stream = null;
|
|
226
|
+
closing.onopen = null;
|
|
227
|
+
closing.onerror = null;
|
|
228
|
+
closing.onmessage = null;
|
|
229
|
+
closing.close();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function abortRepair() {
|
|
233
|
+
if (!repairJob) return;
|
|
234
|
+
const job = repairJob;
|
|
235
|
+
repairJob = null;
|
|
236
|
+
job.controller.abort();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Record one accepted body. The only writer of `receipt`, and it writes only when
|
|
241
|
+
* the candidate applied completely.
|
|
242
|
+
*/
|
|
243
|
+
function accept(candidate) {
|
|
244
|
+
if (stopped) return false;
|
|
245
|
+
const next = {
|
|
246
|
+
bytes: candidate.bytes,
|
|
247
|
+
etag: stampOf(candidate.etag),
|
|
248
|
+
seq: seqOf(candidate.seq),
|
|
249
|
+
};
|
|
250
|
+
if (typeof next.bytes !== 'string' || next.bytes === '') {
|
|
251
|
+
report('recoverable', { reason: 'invalid-bytes', seq: next.seq });
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
if (next.bytes === receipt.bytes && next.etag === receipt.etag && next.seq === receipt.seq) {
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
let applied = false;
|
|
258
|
+
try {
|
|
259
|
+
applied = sink(Object.freeze(next)) === true;
|
|
260
|
+
} catch {
|
|
261
|
+
applied = false;
|
|
262
|
+
}
|
|
263
|
+
if (!applied) {
|
|
264
|
+
report('recoverable', { reason: 'refused', seq: next.seq, etag: next.etag });
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
receipt = Object.freeze(next);
|
|
268
|
+
report('applied', { seq: receipt.seq, etag: receipt.etag });
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function latestSeq(reason, kind) {
|
|
273
|
+
if (kind === 'startup') return null;
|
|
274
|
+
const candidate = Math.max(reason.seq ?? 0, watermark);
|
|
275
|
+
return candidate > 0 ? candidate : null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function deferRepair(reason, why) {
|
|
279
|
+
pendingRepair = reason;
|
|
280
|
+
repairPaused = true;
|
|
281
|
+
report('recoverable', { reason: why, seq: reason.seq ?? null });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function requestRepair(reason) {
|
|
285
|
+
if (stopped) return;
|
|
286
|
+
pendingRepair = { kind: reason.kind, seq: seqOf(reason.seq) };
|
|
287
|
+
repairPaused = false;
|
|
288
|
+
runRepair();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function runRepair() {
|
|
292
|
+
if (stopped || repairJob || repairPaused || !pendingRepair) return;
|
|
293
|
+
const gen = generation;
|
|
294
|
+
const reason = pendingRepair;
|
|
295
|
+
pendingRepair = null;
|
|
296
|
+
const controller = new AbortController();
|
|
297
|
+
repairJob = { controller };
|
|
298
|
+
const arrivalsAtStart = arrivals;
|
|
299
|
+
const watermarkAtStart = watermark;
|
|
300
|
+
|
|
301
|
+
const settle = (body) => {
|
|
302
|
+
if (repairJob && repairJob.controller === controller) repairJob = null;
|
|
303
|
+
if (!active(gen) || controller.signal.aborted) return;
|
|
304
|
+
if (body == null) {
|
|
305
|
+
deferRepair(reason, 'repair-failed');
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (arrivals !== arrivalsAtStart || watermark !== watermarkAtStart) {
|
|
309
|
+
// Something arrived while this body was read, so it may describe a page
|
|
310
|
+
// this reader has already left. A reason queued by the arrival is newer
|
|
311
|
+
// and owns the next attempt; otherwise ask again from the latest watermark.
|
|
312
|
+
discards += 1;
|
|
313
|
+
if (discards <= MAX_DISCARDS) {
|
|
314
|
+
pendingRepair = pendingRepair ?? { kind: reason.kind, seq: latestSeq(reason, reason.kind) };
|
|
315
|
+
runRepair();
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
// Twice is as long as a reader waits: under a steady writer every body is
|
|
319
|
+
// read while a frame lands, so the third is applied, not dropped.
|
|
320
|
+
acceptBody(body.text, { kind: reason.kind, seq: latestSeq(reason, reason.kind) }, body.etag);
|
|
321
|
+
runRepair();
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
acceptBody(body.text, reason, body.etag);
|
|
325
|
+
runRepair();
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
fetch(pageURL, { cache: 'no-store', signal: controller.signal })
|
|
329
|
+
.then((response) =>
|
|
330
|
+
response.ok
|
|
331
|
+
? response.text().then((text) => ({ text, etag: stampOf(response.headers?.get?.('etag')) }))
|
|
332
|
+
: null
|
|
333
|
+
)
|
|
334
|
+
.then(settle)
|
|
335
|
+
.catch(() => settle(null));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function acceptBody(html, reason, responseEtag) {
|
|
339
|
+
// The response's own stamp names the body when the host sends one, otherwise the
|
|
340
|
+
// served root attribute does. A discovery stamp is never attached, and identical
|
|
341
|
+
// bytes already named by a stamp keep that name.
|
|
342
|
+
const bodyStamp = stampOf(responseEtag) ?? readBodyStamp(html);
|
|
343
|
+
const etag = bodyStamp ?? (html === receipt.bytes ? receipt.etag : null);
|
|
344
|
+
const seq = reason.kind === 'startup' ? receipt.seq : reason.seq ?? receipt.seq;
|
|
345
|
+
if (!accept({ bytes: html, etag, seq })) return;
|
|
346
|
+
// This body is the baseline now, but its version stands at the stream's position
|
|
347
|
+
// only once a frame names it: until then another stamp refetches.
|
|
348
|
+
matched = false;
|
|
349
|
+
discards = 0;
|
|
350
|
+
repairPaused = false;
|
|
351
|
+
report('current', { seq: receipt.seq, etag: receipt.etag });
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// A frame repeating the bytes on screen: the receipt moves, nothing repaints.
|
|
355
|
+
function advance(etag, seq) {
|
|
356
|
+
const next = {
|
|
357
|
+
bytes: receipt.bytes,
|
|
358
|
+
etag: etag ?? receipt.etag,
|
|
359
|
+
seq: seq ?? receipt.seq,
|
|
360
|
+
};
|
|
361
|
+
if (next.etag === receipt.etag && next.seq === receipt.seq) return;
|
|
362
|
+
receipt = Object.freeze(next);
|
|
363
|
+
report('current', { seq: receipt.seq, etag: receipt.etag });
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function onFrame(bytes, rawEtag, seq) {
|
|
367
|
+
const etag = stampOf(rawEtag);
|
|
368
|
+
// An older sequence is a version this reader has already left: it never rewinds.
|
|
369
|
+
if (seq !== null && receipt.seq !== null && seq <= receipt.seq) return;
|
|
370
|
+
// A frame naming the represented stamp places it at the stream's position, so
|
|
371
|
+
// the frames after it apply straight from the stream again.
|
|
372
|
+
if (!matched && etag !== null && receipt.etag !== null) {
|
|
373
|
+
if (etag !== receipt.etag) {
|
|
374
|
+
requestRepair({ kind: 'unmatched', seq });
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
matched = true;
|
|
378
|
+
}
|
|
379
|
+
if (bytes === receipt.bytes) {
|
|
380
|
+
advance(etag, seq);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (accept({ bytes, etag, seq })) matched = true;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function onNotification(data, seq) {
|
|
387
|
+
const info = data.data;
|
|
388
|
+
if (info && info.kind === 'external-change') {
|
|
389
|
+
if (typeof info.html === 'string') onFrame(info.html, info.etag, seq);
|
|
390
|
+
else requestRepair({ kind: 'notice', seq });
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (typeof data.msg === 'string' && data.msg.endsWith('changed on disk outside this tab')) {
|
|
394
|
+
requestRepair({ kind: 'notice', seq });
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function onMessage(raw, gen) {
|
|
399
|
+
if (!active(gen)) return;
|
|
400
|
+
arrivals++;
|
|
401
|
+
let data;
|
|
402
|
+
try {
|
|
403
|
+
data = JSON.parse(raw);
|
|
404
|
+
} catch {
|
|
405
|
+
report('recoverable', { reason: 'bad-json' });
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
409
|
+
report('recoverable', { reason: 'bad-frame' });
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const seq = seqOf(data.seq);
|
|
413
|
+
if (seq !== null && seq > watermark) watermark = seq;
|
|
414
|
+
if (data.type === 'notification') {
|
|
415
|
+
onNotification(data, seq);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (data.error) {
|
|
419
|
+
report('recoverable', { reason: 'server-error', seq });
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (typeof data.html !== 'string') {
|
|
423
|
+
// A stamp with no document proves nothing about these bytes: refetch instead.
|
|
424
|
+
if (stampOf(data.etag)) requestRepair({ kind: 'notice', seq });
|
|
425
|
+
else report('recoverable', { reason: 'bad-frame', seq });
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
onFrame(data.html, data.etag, seq);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function onCursor(raw, gen) {
|
|
432
|
+
if (!active(gen)) return;
|
|
433
|
+
arrivals++;
|
|
434
|
+
let data;
|
|
435
|
+
try {
|
|
436
|
+
data = JSON.parse(raw);
|
|
437
|
+
} catch {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (!data || typeof data !== 'object') return;
|
|
441
|
+
const seq = seqOf(data.seq);
|
|
442
|
+
if (seq !== null && seq > watermark) watermark = seq;
|
|
443
|
+
if (data.resync === true) {
|
|
444
|
+
requestRepair({ kind: 'resync', seq });
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (data.startup === true) requestRepair({ kind: 'startup', seq: null });
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function onOpen(gen) {
|
|
451
|
+
if (!active(gen)) return;
|
|
452
|
+
report('open', { seq: receipt.seq, etag: receipt.etag });
|
|
453
|
+
if (repairPaused) {
|
|
454
|
+
repairPaused = false;
|
|
455
|
+
runRepair();
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
// No served stamp means no provenance to check, so nothing here asks the
|
|
459
|
+
// version question. A reader with no bytes at all has to ask for them, or a
|
|
460
|
+
// quiet file would leave it with nothing to show.
|
|
461
|
+
if (receipt.bytes === null && !startupCheck) requestRepair({ kind: 'startup', seq: null });
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const handle = {
|
|
465
|
+
ready: null,
|
|
466
|
+
get represented() {
|
|
467
|
+
return receipt;
|
|
468
|
+
},
|
|
469
|
+
// Take this stream over: a reader starting from an acquisition body adopts the
|
|
470
|
+
// stream that fetched it, so one connection carries the file from then on.
|
|
471
|
+
adopt({ onSnapshot: nextSink, onStatus: nextNotify } = {}) {
|
|
472
|
+
if (typeof nextSink === 'function') sink = nextSink;
|
|
473
|
+
if (typeof nextNotify === 'function') notify = nextNotify;
|
|
474
|
+
return handle;
|
|
475
|
+
},
|
|
476
|
+
start() {
|
|
477
|
+
stopped = false;
|
|
478
|
+
const gen = ++generation;
|
|
479
|
+
closeStream();
|
|
480
|
+
abortRepair();
|
|
481
|
+
pendingRepair = null;
|
|
482
|
+
repairPaused = false;
|
|
483
|
+
arrivals = 0;
|
|
484
|
+
discards = 0;
|
|
485
|
+
watermark = receipt.seq ?? 0;
|
|
486
|
+
handle.ready = resolveWireProfile().then((resolved) => {
|
|
487
|
+
if (!active(gen)) return handle;
|
|
488
|
+
const resumeId = mintResumeId();
|
|
489
|
+
const clientId = mintClientId();
|
|
490
|
+
const path = resolved.profile.streamPath(pageURL, LANE, resumeId, clientId);
|
|
491
|
+
// Resolved against the real origin: a <base href> would otherwise point
|
|
492
|
+
// the reader's stream at an origin the authored document chose.
|
|
493
|
+
const opened = new SyncStream(new URL(path, window.location.origin).href, {
|
|
494
|
+
shared: resolved.sharedSync,
|
|
495
|
+
sharedData: resolved.sharedData,
|
|
496
|
+
documentURL: pageURL,
|
|
497
|
+
lane: LANE,
|
|
498
|
+
clientId,
|
|
499
|
+
startupCheck,
|
|
500
|
+
});
|
|
501
|
+
stream = opened;
|
|
502
|
+
opened.onopen = () => onOpen(gen);
|
|
503
|
+
opened.onerror = () => { if (active(gen)) report('reconnecting'); };
|
|
504
|
+
opened.onmessage = (event) => onMessage(event.data, gen);
|
|
505
|
+
opened.addEventListener('cursor', (event) => onCursor(event.data, gen));
|
|
506
|
+
// Named, so it never reaches `onmessage` and never looks like the saved file
|
|
507
|
+
// it is not: who is present and the file a reader holds are separate
|
|
508
|
+
// questions, and a roster frame moves no receipt and no resume cursor.
|
|
509
|
+
opened.addEventListener('presence', (event) => {
|
|
510
|
+
if (!active(gen)) return;
|
|
511
|
+
let data;
|
|
512
|
+
try {
|
|
513
|
+
data = JSON.parse(event.data);
|
|
514
|
+
} catch {
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (data && typeof data === 'object' && !Array.isArray(data)) takePresence(data);
|
|
518
|
+
});
|
|
519
|
+
return handle;
|
|
520
|
+
});
|
|
521
|
+
return handle;
|
|
522
|
+
},
|
|
523
|
+
stop() {
|
|
524
|
+
if (stopped) return;
|
|
525
|
+
stopped = true;
|
|
526
|
+
generation++;
|
|
527
|
+
closeStream();
|
|
528
|
+
abortRepair();
|
|
529
|
+
pendingRepair = null;
|
|
530
|
+
repairPaused = false;
|
|
531
|
+
},
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
return handle;
|
|
535
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Two versions of one document never merge. A copy of a starter template carries
|
|
2
|
+
// `<meta name="hyper-version">`, and an upgrade replaces layout, code and data
|
|
3
|
+
// together; morphing that frame into a tab still showing the previous version would
|
|
4
|
+
// interleave two templates, so the frame is refused whole and the tab is told to
|
|
5
|
+
// reload instead.
|
|
6
|
+
//
|
|
7
|
+
// Only a page that states its own version can make this judgement, and only a frame
|
|
8
|
+
// that states one can disagree: a document without the meta behaves exactly as it did
|
|
9
|
+
// before, which is every page that is not a template copy.
|
|
10
|
+
//
|
|
11
|
+
// Holding the saves is not optional once the frame is refused. This tab's DOM is now
|
|
12
|
+
// a version the host no longer stores, and its next save would write that version
|
|
13
|
+
// back over the upgrade with no refusal to stop it.
|
|
14
|
+
import { isEditMode } from "../core/is-edit-mode.js";
|
|
15
|
+
|
|
16
|
+
export const UPGRADED_MESSAGE = "This document was upgraded.";
|
|
17
|
+
|
|
18
|
+
let stopped = false;
|
|
19
|
+
|
|
20
|
+
function metaOf(root, name) {
|
|
21
|
+
const el = root.querySelector(`meta[name="${name}"]`);
|
|
22
|
+
const value = el ? el.getAttribute("content") : null;
|
|
23
|
+
return value && value.trim() ? value.trim() : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** This page's own version, or null when it does not state one. */
|
|
27
|
+
export function pageVersion(doc = document) {
|
|
28
|
+
return metaOf(doc, "hyper-version");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The version a saved frame carries, or null when it carries none. */
|
|
32
|
+
export function frameVersion(html) {
|
|
33
|
+
if (typeof html !== "string" || !html.includes("hyper-version")) return null;
|
|
34
|
+
return metaOf(new DOMParser().parseFromString(html, "text/html"), "hyper-version");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// One version is not another. A page that states none can make no such judgement.
|
|
38
|
+
function replacesMine(theirs) {
|
|
39
|
+
const mine = pageVersion();
|
|
40
|
+
return Boolean(mine) && Boolean(theirs) && theirs !== mine;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Whether this frame must not be applied: it names a version other than this page's,
|
|
45
|
+
* or an earlier frame already said so and nothing this tab holds is trustworthy since.
|
|
46
|
+
*/
|
|
47
|
+
export function upgradeReplaces(html) {
|
|
48
|
+
if (stopped) return true;
|
|
49
|
+
return replacesMine(frameVersion(html));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The same judgement for an incoming document somebody else already parsed: the
|
|
54
|
+
* reader holds the file's own tree and reads its head from there.
|
|
55
|
+
*/
|
|
56
|
+
export function upgradeReplacesDocument(other) {
|
|
57
|
+
if (stopped) return true;
|
|
58
|
+
if (!other || typeof other.querySelector !== "function") return false;
|
|
59
|
+
return replacesMine(metaOf(other, "hyper-version"));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Refuse this document from here on: every save is held, every later frame is
|
|
64
|
+
* dropped, and the notice system is asked to show the reload prompt.
|
|
65
|
+
*
|
|
66
|
+
* @returns {Promise<void>} settles once the hold is in place
|
|
67
|
+
*/
|
|
68
|
+
export function stopForUpgrade() {
|
|
69
|
+
if (stopped) return Promise.resolve();
|
|
70
|
+
stopped = true;
|
|
71
|
+
// The save lane is edit mode's, and a reader must not download it for a hold it
|
|
72
|
+
// has no saves to apply. Already loaded wherever this can matter, so this is the
|
|
73
|
+
// module's own copy rather than a fetch.
|
|
74
|
+
const hold = isEditMode
|
|
75
|
+
? import("../core/save.js").then((save) => save.holdAllSaves(UPGRADED_MESSAGE), () => {})
|
|
76
|
+
: Promise.resolve();
|
|
77
|
+
// Where the shell (core/conflict-notice.js) is loaded, it is already listening and
|
|
78
|
+
// fetches the notice for this event. A reader has no shell, so the notice is fetched
|
|
79
|
+
// here and handed the event it would otherwise never see.
|
|
80
|
+
const event = new CustomEvent("clay:document-upgraded", { detail: { message: UPGRADED_MESSAGE } });
|
|
81
|
+
const drawn = isEditMode
|
|
82
|
+
? Promise.resolve()
|
|
83
|
+
: import("../core/conflict-notice-ui.js").then((ui) => ui.start(event), () => {});
|
|
84
|
+
document.dispatchEvent(event);
|
|
85
|
+
return Promise.all([hold, drawn]).then(() => {});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const _test = {
|
|
89
|
+
reset() {
|
|
90
|
+
stopped = false;
|
|
91
|
+
},
|
|
92
|
+
};
|