@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,571 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* upload-task.js — one upload with an identity of its own: state, progress, cancel, retry.
|
|
3
|
+
*
|
|
4
|
+
* `upload()` answers once, at the end, which is all a caller that embeds on failure
|
|
5
|
+
* has ever needed. An editor that shows a row per picture needs more than that: which
|
|
6
|
+
* upload a progress callback belongs to, whether it can still be cancelled, and a
|
|
7
|
+
* retry that is a new attempt of the same thing rather than a second copy of it.
|
|
8
|
+
*
|
|
9
|
+
* const task = clay.upload.start(file, { requiredAccess: "document-or-local" });
|
|
10
|
+
* const off = task.subscribe(({ state, percent }) => draw(state, percent));
|
|
11
|
+
* await task.result; // { ok, msg, msgType, code, uploads }
|
|
12
|
+
* task.cancel();
|
|
13
|
+
* await task.retry(); // a new attempt, same file, same id
|
|
14
|
+
* task.dispose();
|
|
15
|
+
*
|
|
16
|
+
* States are `uploading`, `succeeded`, `failed` and `cancelled`. The id is stable for
|
|
17
|
+
* the life of the task; `attempt` counts up only on an explicit retry, so a callback
|
|
18
|
+
* arriving from an attempt that has already ended can be recognized and ignored.
|
|
19
|
+
*
|
|
20
|
+
* The transport is the same XMLHttpRequest the single-shot path always used, for the
|
|
21
|
+
* same reason: fetch cannot report upload progress. What changed is that one terminal
|
|
22
|
+
* function now settles an attempt for every signal — response, timeout, cancel, a
|
|
23
|
+
* throw while starting — and only the caller that wins emits and resolves. A timeout
|
|
24
|
+
* therefore stays a timeout, and a load that arrives after an abort cannot announce a
|
|
25
|
+
* second ending. `upload()` never rejects for an ordinary transport failure, and
|
|
26
|
+
* neither does a task; programmer misuse (no file, an unknown `requiredAccess`)
|
|
27
|
+
* throws before anything starts instead.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { hostMeta, hostMetaOutcome } from "../core/host-meta.js";
|
|
31
|
+
import { saveToken } from "../core/host-attrs.js";
|
|
32
|
+
import { registerUnsavedState } from "../lib/unsaved-state.js";
|
|
33
|
+
|
|
34
|
+
const UPLOAD_PATH = "/_/upload";
|
|
35
|
+
const UPLOAD_TIMEOUT_MS = 120000;
|
|
36
|
+
|
|
37
|
+
// What each `requiredAccess` name accepts from the host, both in `document.upload.access`
|
|
38
|
+
// before the request and on every upload the host returns afterwards. A host that
|
|
39
|
+
// reports `document` binds each new URL to that document's live read policy; a local
|
|
40
|
+
// host reports `local`, meaning its own file-serving rules rather than a hosted sharing
|
|
41
|
+
// ACL. Anything else — a missing field, `public`, a value this library has never heard
|
|
42
|
+
// of — is insufficient, and the caller that asked for privacy is told so instead of
|
|
43
|
+
// being handed a URL it might paste somewhere permanent.
|
|
44
|
+
const REQUIRED_ACCESS = {
|
|
45
|
+
document: ["document"],
|
|
46
|
+
local: ["local"],
|
|
47
|
+
"document-or-local": ["document", "local"]
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Retry is a new attempt of the same bytes, which only makes sense when asking again
|
|
51
|
+
// could plausibly answer differently. A policy refusal (too large, wrong type, not
|
|
52
|
+
// allowed here) is the host's decision about this file, so the row explains it and
|
|
53
|
+
// leaves retry out; `network`, `timeout`, `error` and `bad-response` are the host
|
|
54
|
+
// having no answer or a broken one, and a cancel is a person who may want to go on.
|
|
55
|
+
export const RETRYABLE_CODES = new Set(["network", "timeout", "error", "bad-response", "aborted"]);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Whether this task's current ending permits a retry.
|
|
59
|
+
*
|
|
60
|
+
* The one table `task.retry()` and the row's Retry button both obey, so the button can
|
|
61
|
+
* never offer an attempt the task would refuse.
|
|
62
|
+
*
|
|
63
|
+
* @param {Object} snapshot - a task snapshot
|
|
64
|
+
* @returns {boolean}
|
|
65
|
+
*/
|
|
66
|
+
export function canRetry(snapshot) {
|
|
67
|
+
if (!snapshot) return false;
|
|
68
|
+
if (snapshot.state === "cancelled") return true;
|
|
69
|
+
if (snapshot.state !== "failed") return false;
|
|
70
|
+
return RETRYABLE_CODES.has(snapshot.result?.code ?? null);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// The tasks this page has started and nobody has disposed. An upload outlives the edit
|
|
74
|
+
// session that started it: the selected file and a stored URL are the person's work, and
|
|
75
|
+
// a teardown that cancelled the request or forgot the result would throw away bytes nobody
|
|
76
|
+
// has inserted yet. Only an upload still on the wire counts as pending work — its fate is
|
|
77
|
+
// unknown, so it is asked to finish before an exit may call the page clean. A result the
|
|
78
|
+
// host already accepted is not pending: inserting it is the caller's next move, not a
|
|
79
|
+
// write anyone is waiting on.
|
|
80
|
+
const tasks = new Set();
|
|
81
|
+
|
|
82
|
+
const uploading = () => [...tasks].some((task) => task.state === "uploading");
|
|
83
|
+
|
|
84
|
+
async function flushUploads() {
|
|
85
|
+
const inFlight = [...tasks].filter((task) => task.state === "uploading").map((task) => task.result);
|
|
86
|
+
if (!inFlight.length) return { ok: true };
|
|
87
|
+
await Promise.allSettled(inFlight);
|
|
88
|
+
return { ok: !uploading() };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const pending = registerUnsavedState({ id: "upload", isPending: uploading, flush: flushUploads });
|
|
92
|
+
|
|
93
|
+
// New attempts are admitted while no edit session is leaving. The exit closes admission
|
|
94
|
+
// before its barrier, so an upload started after that would be bytes nobody waits for.
|
|
95
|
+
// With no session running (a page that never uses the lifecycle) admission is open, which
|
|
96
|
+
// is what every caller of this module has always had.
|
|
97
|
+
let session = null;
|
|
98
|
+
|
|
99
|
+
const admitsAttempts = () => session?.admitted !== false;
|
|
100
|
+
|
|
101
|
+
let taskCount = 0;
|
|
102
|
+
|
|
103
|
+
function nextId() {
|
|
104
|
+
taskCount += 1;
|
|
105
|
+
return `upload-${taskCount}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function result(ok, msg, msgType, code, uploads = []) {
|
|
109
|
+
return { ok, msg, msgType, code, uploads };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function emit(name, detail) {
|
|
113
|
+
if (typeof document === "undefined") return;
|
|
114
|
+
document.dispatchEvent(new CustomEvent(`clay:upload-${name}`, { detail }));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Read a `requiredAccess` name into the access values it accepts.
|
|
119
|
+
*
|
|
120
|
+
* Throws for a name that is not in the table: this is a caller's typo, not a host's
|
|
121
|
+
* answer, and silently treating it as "no requirement" would hand back exactly the
|
|
122
|
+
* URL the caller was trying to avoid.
|
|
123
|
+
*
|
|
124
|
+
* @param {?string} name
|
|
125
|
+
* @returns {?string[]}
|
|
126
|
+
*/
|
|
127
|
+
export function checkRequiredAccess(name) {
|
|
128
|
+
if (name == null) return null;
|
|
129
|
+
const wanted = REQUIRED_ACCESS[name];
|
|
130
|
+
if (!wanted) {
|
|
131
|
+
throw new TypeError(`Unknown requiredAccess: ${String(name)}. Use one of ${Object.keys(REQUIRED_ACCESS).join(", ")}.`);
|
|
132
|
+
}
|
|
133
|
+
return wanted;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Store one file on the host, once, as a single attempt.
|
|
138
|
+
*
|
|
139
|
+
* The whole pipeline behind both entry points: what the host says it supports, what
|
|
140
|
+
* this document is allowed to do, the announced size cap, the request, and — when the
|
|
141
|
+
* caller named a `requiredAccess` — the access the host reported on each returned
|
|
142
|
+
* upload. Every exit is the one result shape, and none of them rejects.
|
|
143
|
+
*
|
|
144
|
+
* @param {File|Blob} file
|
|
145
|
+
* @param {Object} [options]
|
|
146
|
+
* @param {function} [options.onProgress] - ({loaded, total, percent})
|
|
147
|
+
* @param {AbortSignal} [options.signal]
|
|
148
|
+
* @param {?string} [options.requiredAccess] - "document", "local" or "document-or-local"
|
|
149
|
+
* @param {string} [options.taskId] - used on the global events only
|
|
150
|
+
* @param {number} [options.attempt] - used on the global events only
|
|
151
|
+
* @returns {Promise<{ok: boolean, msg: string, msgType: string, code: ?string, uploads: Array}>}
|
|
152
|
+
*/
|
|
153
|
+
export async function runUpload(file, { onProgress, signal, requiredAccess, taskId = nextId(), attempt = 1 } = {}) {
|
|
154
|
+
const wantedAccess = checkRequiredAccess(requiredAccess);
|
|
155
|
+
|
|
156
|
+
if (!file) return result(false, "No file to upload", "error", "bad-request");
|
|
157
|
+
|
|
158
|
+
const cancelled = () => result(false, "Upload cancelled", "error", "aborted");
|
|
159
|
+
if (!admitsAttempts()) return cancelled();
|
|
160
|
+
|
|
161
|
+
// A retry asks again rather than reading the memoized answer: the host that was
|
|
162
|
+
// unreachable (or briefly refused) is exactly the thing a second attempt is for.
|
|
163
|
+
let meta;
|
|
164
|
+
try {
|
|
165
|
+
meta = await hostMeta(attempt > 1 ? { fresh: true } : undefined);
|
|
166
|
+
} catch (_) {
|
|
167
|
+
meta = { extensions: [], document: null };
|
|
168
|
+
}
|
|
169
|
+
if (signal?.aborted) return cancelled();
|
|
170
|
+
// A host that should have answered and did not has not said this document cannot
|
|
171
|
+
// upload. It is a network failure, and asking again may well be answered.
|
|
172
|
+
if (hostMetaOutcome(meta) === "failed") {
|
|
173
|
+
return result(false, "Couldn't reach the host to upload", "error", "network");
|
|
174
|
+
}
|
|
175
|
+
if (!meta.extensions.includes("upload")) {
|
|
176
|
+
// Not an error. This is a document open from a plain file server, or a bare
|
|
177
|
+
// core host, and embedding is what it should do.
|
|
178
|
+
return result(false, "This host does not store uploaded files", "skipped", "unsupported");
|
|
179
|
+
}
|
|
180
|
+
// The host stores files, but not for this caller on this document: the spec withholds the
|
|
181
|
+
// document's upload block by omission, and a client that finds none does not upload. Not
|
|
182
|
+
// "unsupported", because embedding instead would route around the host's own decision.
|
|
183
|
+
if (signal?.aborted) return cancelled();
|
|
184
|
+
if (!meta.document?.upload) {
|
|
185
|
+
return result(false, "You can't add files to this document", "error", "forbidden");
|
|
186
|
+
}
|
|
187
|
+
// Asked for before the bytes leave: a caller that cannot use a publicly readable URL
|
|
188
|
+
// must not spend a person's upload finding out. Read from the host's own metadata and
|
|
189
|
+
// never inferred from the page URL, the plan or the filename.
|
|
190
|
+
if (signal?.aborted) return cancelled();
|
|
191
|
+
if (wantedAccess) {
|
|
192
|
+
const offered = meta.document.upload.access;
|
|
193
|
+
if (!wantedAccess.includes(offered)) {
|
|
194
|
+
return result(false, "This host does not keep uploaded files private to this document", "error", "privacy-unsupported");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// The one local pre-check the spec asks for, and the only reason `maxBytes` is
|
|
199
|
+
// published: refusing a 40 MB photo here costs nothing, and sending it to be
|
|
200
|
+
// refused costs the person the whole upload first. Deliberately the ONLY thing
|
|
201
|
+
// decided locally. `document.upload.allowed` is not read as a veto: it can be
|
|
202
|
+
// stale (a plan upgraded in another tab, a share link granted since load), and
|
|
203
|
+
// the host answers the real reason with its own code on the request itself.
|
|
204
|
+
if (signal?.aborted) return cancelled();
|
|
205
|
+
const cap = meta.document?.upload?.maxBytes;
|
|
206
|
+
if (typeof cap === "number" && cap > 0 && file.size > cap) {
|
|
207
|
+
return result(false, `That file is larger than this host accepts (${cap} bytes)`, "error", "too-large");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// The access check lives inside `postFile`, before it announces success: a downgrade
|
|
211
|
+
// must never have emitted `clay:upload-done` first.
|
|
212
|
+
return postFile(file, { onProgress, signal, taskId, attempt, requiredAccess: wantedAccess });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Check the access the host reported on a successful upload.
|
|
217
|
+
*
|
|
218
|
+
* A caller that asked for a private URL sends the bytes on the strength of metadata
|
|
219
|
+
* that can change under it (a deployment setting flipped, a proxy answering for the
|
|
220
|
+
* host). The returned uploads are the host's own claim about what it stored, so the
|
|
221
|
+
* claim is verified too. A downgrade keeps the uploads in the result — a person can
|
|
222
|
+
* still copy or download the URL deliberately — but the result is not a success, so
|
|
223
|
+
* no caller inserts it silently.
|
|
224
|
+
*/
|
|
225
|
+
function verifyAccess(answer, wantedAccess) {
|
|
226
|
+
if (!wantedAccess || !answer.ok) return answer;
|
|
227
|
+
const uploads = answer.uploads || [];
|
|
228
|
+
const kept = uploads.every((upload) => wantedAccess.includes(upload?.access));
|
|
229
|
+
if (kept) return answer;
|
|
230
|
+
return result(false, "The host stored the file without the access it promised", "error", "privacy-unsupported", uploads);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The request itself: one attempt, one ending.
|
|
235
|
+
*
|
|
236
|
+
* @param {File|Blob} file
|
|
237
|
+
* @param {Object} [options]
|
|
238
|
+
* @param {function} [options.onProgress]
|
|
239
|
+
* @param {AbortSignal} [options.signal]
|
|
240
|
+
* @param {string} [options.taskId]
|
|
241
|
+
* @param {number} [options.attempt]
|
|
242
|
+
* @param {?string|?string[]} [options.requiredAccess] - a name, or what `checkRequiredAccess` read it into
|
|
243
|
+
* @returns {Promise<{ok: boolean, msg: string, msgType: string, code: ?string, uploads: Array}>}
|
|
244
|
+
*/
|
|
245
|
+
export function postFile(file, { onProgress, signal, taskId, attempt, requiredAccess } = {}) {
|
|
246
|
+
const wantedAccess = Array.isArray(requiredAccess) ? requiredAccess : checkRequiredAccess(requiredAccess);
|
|
247
|
+
return new Promise((resolve) => {
|
|
248
|
+
// Additive, on the events only: a listener that shows one row per upload can tell
|
|
249
|
+
// two of them apart, and old callers that read only `msg` never notice the fields.
|
|
250
|
+
const events = { taskId, attempt };
|
|
251
|
+
const token = saveToken();
|
|
252
|
+
const path = token ? `${UPLOAD_PATH}/${token}` : UPLOAD_PATH;
|
|
253
|
+
// Absolute against the real origin, never left relative: a <base href> in the
|
|
254
|
+
// authored document would otherwise redirect this request, and the
|
|
255
|
+
// per-document token in its path, to an origin the document chose.
|
|
256
|
+
const url = new URL(path, window.location.origin).href;
|
|
257
|
+
|
|
258
|
+
let xhr = null;
|
|
259
|
+
let settled = false;
|
|
260
|
+
let timeoutId = null;
|
|
261
|
+
|
|
262
|
+
// One terminal function for every signal. Whichever one wins emits and resolves;
|
|
263
|
+
// every later one — a timeout firing during an already-cancelled request, a load
|
|
264
|
+
// arriving after an abort — finds `settled` set and changes nothing.
|
|
265
|
+
function finish(value, eventName, detail) {
|
|
266
|
+
if (settled) return false;
|
|
267
|
+
settled = true;
|
|
268
|
+
if (timeoutId !== null) clearTimeout(timeoutId);
|
|
269
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
270
|
+
if (eventName) emit(eventName, { ...detail, ...events });
|
|
271
|
+
resolve(value);
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function fail(msg, msgType, code) {
|
|
276
|
+
return finish(result(false, msg, msgType, code), "error", { msg, code });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function onTimeout() {
|
|
280
|
+
// A timeout is not evidence the upload failed. The bytes may well have
|
|
281
|
+
// landed, so this says "we do not know" rather than asserting a failure,
|
|
282
|
+
// and the content-hash name makes the retry idempotent if the caller resends.
|
|
283
|
+
// Settle first, abort second: aborting first would let the abort handler
|
|
284
|
+
// report a cancelled upload that was really a request nobody answered.
|
|
285
|
+
const value = result(false, "Upload timed out", "unknown", "timeout");
|
|
286
|
+
if (finish(value, "error", { msg: value.msg, code: value.code })) xhr.abort();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function onAbort() {
|
|
290
|
+
if (finish(result(false, "Upload cancelled", "skipped", "aborted"))) xhr?.abort();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// XMLHttpRequest for one reason: fetch cannot report upload progress. A photo
|
|
294
|
+
// over a phone connection is the case this whole capability is for, and a
|
|
295
|
+
// picker that sits there saying nothing for twenty seconds reads as broken.
|
|
296
|
+
try {
|
|
297
|
+
xhr = new XMLHttpRequest();
|
|
298
|
+
} catch (_) {
|
|
299
|
+
return fail("This browser could not start the upload", "error", "error");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
timeoutId = setTimeout(onTimeout, UPLOAD_TIMEOUT_MS);
|
|
303
|
+
|
|
304
|
+
if (signal) {
|
|
305
|
+
if (signal.aborted) return onAbort();
|
|
306
|
+
signal.addEventListener("abort", onAbort);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
xhr.upload.addEventListener("progress", (event) => {
|
|
310
|
+
if (settled) return;
|
|
311
|
+
if (!event.lengthComputable) return;
|
|
312
|
+
const detail = {
|
|
313
|
+
loaded: event.loaded,
|
|
314
|
+
total: event.total,
|
|
315
|
+
percent: Math.round((event.loaded / event.total) * 100)
|
|
316
|
+
};
|
|
317
|
+
emit("progress", { ...detail, ...events });
|
|
318
|
+
if (onProgress) {
|
|
319
|
+
try { onProgress(detail); } catch (_) { /* a caller's drawing must not kill the upload */ }
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
xhr.addEventListener("load", () => {
|
|
324
|
+
// After an abort or a timeout this answer is stale, and the request already has
|
|
325
|
+
// its ending. Reporting a second one would let a cancelled upload insert a URL.
|
|
326
|
+
if (settled) return;
|
|
327
|
+
let body = {};
|
|
328
|
+
try { body = JSON.parse(xhr.responseText || "{}"); } catch (_) { body = {}; }
|
|
329
|
+
|
|
330
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
331
|
+
const uploads = Array.isArray(body.uploads) ? body.uploads : [];
|
|
332
|
+
if (!uploads.length || typeof uploads[0]?.url !== "string") {
|
|
333
|
+
return fail("The host accepted the file but did not say where it put it", "error", "bad-response");
|
|
334
|
+
}
|
|
335
|
+
// The returned uploads are the host's own claim about what it stored, so the
|
|
336
|
+
// claim is checked before anything calls this a success: a downgrade keeps the
|
|
337
|
+
// uploads but announces an error, never a done.
|
|
338
|
+
const value = verifyAccess(
|
|
339
|
+
result(true, body.msg ?? "Uploaded", body.msgType || "success", body.code ?? null, uploads),
|
|
340
|
+
wantedAccess
|
|
341
|
+
);
|
|
342
|
+
if (value.ok) return finish(value, "done", { uploads });
|
|
343
|
+
return finish(value, "error", { msg: value.msg, code: value.code, uploads });
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// The host's own code when it sent one, so a caller branches on the reason
|
|
347
|
+
// rather than pattern-matching a message. Falling back to the status keeps
|
|
348
|
+
// a host that answers a bare 413 readable.
|
|
349
|
+
const code = body.code || STATUS_CODES[xhr.status] || "error";
|
|
350
|
+
return fail(body.msg || `Upload failed (${xhr.status})`, "error", code);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
xhr.addEventListener("error", () => fail("Upload failed", "error", "network"));
|
|
354
|
+
xhr.addEventListener("abort", () => finish(result(false, "Upload cancelled", "skipped", "aborted")));
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
xhr.open("POST", url, true);
|
|
358
|
+
// The exact equivalent of the save lane's `credentials: 'omit'` in the case
|
|
359
|
+
// where the two differ: a sandboxed document is cross-origin to its own host,
|
|
360
|
+
// and asking for credentials there needs Access-Control-Allow-Credentials
|
|
361
|
+
// back, which a token-minting host must never send.
|
|
362
|
+
xhr.withCredentials = false;
|
|
363
|
+
xhr.setRequestHeader("Document-URL", window.location.href);
|
|
364
|
+
|
|
365
|
+
// FormData, so the file streams as bytes. No FileReader and no base64: a
|
|
366
|
+
// whole-file string costs a third more bytes and holds the entire file in
|
|
367
|
+
// memory twice.
|
|
368
|
+
const form = new FormData();
|
|
369
|
+
form.append("file", file, file.name || "upload");
|
|
370
|
+
|
|
371
|
+
emit("start", { name: file.name || null, size: file.size ?? null, ...events });
|
|
372
|
+
xhr.send(form);
|
|
373
|
+
} catch (_) {
|
|
374
|
+
fail("The upload could not be sent", "error", "error");
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Start one upload and hand back the task that owns it.
|
|
381
|
+
*
|
|
382
|
+
* The first attempt begins here, so subscribing immediately sees `uploading` rather
|
|
383
|
+
* than an empty state that never lasted. The file is held until `dispose()` — a failed
|
|
384
|
+
* attempt must be retryable without asking the person to pick it again.
|
|
385
|
+
*
|
|
386
|
+
* @param {File|Blob} file
|
|
387
|
+
* @param {Object} [options]
|
|
388
|
+
* @param {?string} [options.requiredAccess] - "document", "local" or "document-or-local"
|
|
389
|
+
* @param {function} [options.onProgress] - ({loaded, total, percent}) for the current attempt
|
|
390
|
+
* @returns {Object} task
|
|
391
|
+
*/
|
|
392
|
+
export function start(file, options = {}) {
|
|
393
|
+
if (!file) throw new TypeError("clay.upload.start needs a file");
|
|
394
|
+
checkRequiredAccess(options.requiredAccess);
|
|
395
|
+
|
|
396
|
+
const id = nextId();
|
|
397
|
+
const requiredAccess = options.requiredAccess ?? null;
|
|
398
|
+
const onProgress = options.onProgress || null;
|
|
399
|
+
|
|
400
|
+
let held = file;
|
|
401
|
+
let attempt = 0;
|
|
402
|
+
let state = "uploading";
|
|
403
|
+
let loaded = 0;
|
|
404
|
+
let total = null;
|
|
405
|
+
let percent = null;
|
|
406
|
+
let outcome = null;
|
|
407
|
+
let controller = null;
|
|
408
|
+
let current = null;
|
|
409
|
+
let disposed = false;
|
|
410
|
+
// The attempt a cancel settled by hand. Its own late answer must not replace the
|
|
411
|
+
// cancelled state the person was already shown.
|
|
412
|
+
let cancelledAttempt = 0;
|
|
413
|
+
const listeners = new Set();
|
|
414
|
+
|
|
415
|
+
function readSnapshot() {
|
|
416
|
+
return { id, attempt, state, loaded, total, percent, result: outcome };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function publish() {
|
|
420
|
+
if (!listeners.size) return;
|
|
421
|
+
const snapshot = readSnapshot();
|
|
422
|
+
for (const listener of [...listeners]) {
|
|
423
|
+
try { listener(snapshot); } catch (_) { /* a caller's drawing must not kill the upload */ }
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function runAttempt() {
|
|
428
|
+
attempt += 1;
|
|
429
|
+
// The attempt this closure belongs to. Anything arriving for an older one is
|
|
430
|
+
// ignored rather than applied to the upload the person is now watching.
|
|
431
|
+
const mine = attempt;
|
|
432
|
+
controller = new AbortController();
|
|
433
|
+
state = "uploading";
|
|
434
|
+
loaded = 0;
|
|
435
|
+
total = null;
|
|
436
|
+
percent = null;
|
|
437
|
+
outcome = null;
|
|
438
|
+
|
|
439
|
+
current = runUpload(held, {
|
|
440
|
+
requiredAccess,
|
|
441
|
+
signal: controller.signal,
|
|
442
|
+
taskId: id,
|
|
443
|
+
attempt: mine,
|
|
444
|
+
onProgress: (progress) => {
|
|
445
|
+
if (disposed || mine !== attempt) return;
|
|
446
|
+
loaded = progress.loaded;
|
|
447
|
+
total = progress.total;
|
|
448
|
+
percent = progress.percent;
|
|
449
|
+
publish();
|
|
450
|
+
if (onProgress) {
|
|
451
|
+
try { onProgress(progress); } catch (_) { /* a caller's drawing must not kill the upload */ }
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}).then((value) => {
|
|
455
|
+
if (!disposed && mine === attempt && mine !== cancelledAttempt) {
|
|
456
|
+
outcome = value;
|
|
457
|
+
// 100% transmitted is not success: only the host's answer says the bytes were
|
|
458
|
+
// stored, and until it arrives the attempt is still uploading.
|
|
459
|
+
state = value.ok ? "succeeded" : value.code === "aborted" ? "cancelled" : "failed";
|
|
460
|
+
publish();
|
|
461
|
+
}
|
|
462
|
+
pending.changed();
|
|
463
|
+
return value;
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
publish();
|
|
467
|
+
pending.changed();
|
|
468
|
+
return current;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const task = {
|
|
472
|
+
id,
|
|
473
|
+
get attempt() { return attempt; },
|
|
474
|
+
get state() { return state; },
|
|
475
|
+
get file() { return held; },
|
|
476
|
+
get result() { return current; },
|
|
477
|
+
snapshot: readSnapshot,
|
|
478
|
+
subscribe(listener) {
|
|
479
|
+
if (typeof listener !== "function") throw new TypeError("subscribe needs a function");
|
|
480
|
+
if (disposed) return () => {};
|
|
481
|
+
listeners.add(listener);
|
|
482
|
+
try { listener(readSnapshot()); } catch (_) { /* a caller's drawing must not kill the upload */ }
|
|
483
|
+
return () => listeners.delete(listener);
|
|
484
|
+
},
|
|
485
|
+
// Terminal for this attempt only: the bytes may be on the host already, so nothing
|
|
486
|
+
// is deleted, and its eventual answer cannot be mistaken for a finished upload.
|
|
487
|
+
// A retry is what asks again.
|
|
488
|
+
cancel() {
|
|
489
|
+
if (disposed) return;
|
|
490
|
+
// The wait for the host's metadata is part of the attempt, so a cancel during it
|
|
491
|
+
// settles now rather than when the answer arrives. The request is still aborted,
|
|
492
|
+
// and whatever it eventually says cannot overwrite this.
|
|
493
|
+
if (state === "uploading") {
|
|
494
|
+
cancelledAttempt = attempt;
|
|
495
|
+
state = "cancelled";
|
|
496
|
+
outcome = result(false, "Upload cancelled", "error", "aborted");
|
|
497
|
+
publish();
|
|
498
|
+
pending.changed();
|
|
499
|
+
}
|
|
500
|
+
controller?.abort();
|
|
501
|
+
},
|
|
502
|
+
retry() {
|
|
503
|
+
// One attempt at a time, and no resurrecting a success: two calls from one button
|
|
504
|
+
// press share the attempt already running instead of sending the file twice.
|
|
505
|
+
// The same table the Retry button obeys, so the button never offers a retry the
|
|
506
|
+
// task would refuse.
|
|
507
|
+
// A closed session is not a reason to spend the recovery: the task keeps its ending
|
|
508
|
+
// and its file, and a retry after the next entry asks again.
|
|
509
|
+
if (disposed || !admitsAttempts() || !canRetry(readSnapshot())) return current;
|
|
510
|
+
return runAttempt();
|
|
511
|
+
},
|
|
512
|
+
dispose() {
|
|
513
|
+
if (disposed) return;
|
|
514
|
+
disposed = true;
|
|
515
|
+
controller?.abort();
|
|
516
|
+
listeners.clear();
|
|
517
|
+
held = null;
|
|
518
|
+
tasks.delete(task);
|
|
519
|
+
pending.changed();
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
tasks.add(task);
|
|
524
|
+
runAttempt();
|
|
525
|
+
return task;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Uploads as a mode participant.
|
|
530
|
+
*
|
|
531
|
+
* `prepare` takes the session's admission, closed until the commit; `enable` opens it,
|
|
532
|
+
* `quiesce` closes it before the exit barrier and `resume` reopens it for a refused exit.
|
|
533
|
+
* Adoption (`initial`) is a lease over a session that is already open, because an explicit
|
|
534
|
+
* edit boot admits uploads from its first keystroke. `dispose` closes it for a session that
|
|
535
|
+
* has ended and takes nothing else: the tasks started under it keep their selected files,
|
|
536
|
+
* their stored results and their own endings, because those belong to the callers that
|
|
537
|
+
* asked for them, not to the mode that was running.
|
|
538
|
+
*/
|
|
539
|
+
export function uploadParticipant() {
|
|
540
|
+
return {
|
|
541
|
+
id: "clay:upload",
|
|
542
|
+
order: 340,
|
|
543
|
+
prepare(context = {}) {
|
|
544
|
+
const lease = { admitted: context.initial === true };
|
|
545
|
+
session = lease;
|
|
546
|
+
return {
|
|
547
|
+
enable() {
|
|
548
|
+
if (session === lease) lease.admitted = true;
|
|
549
|
+
},
|
|
550
|
+
quiesce() {
|
|
551
|
+
if (session === lease) lease.admitted = false;
|
|
552
|
+
},
|
|
553
|
+
resume() {
|
|
554
|
+
if (session === lease) lease.admitted = true;
|
|
555
|
+
},
|
|
556
|
+
dispose() {
|
|
557
|
+
if (session === lease) lease.admitted = false;
|
|
558
|
+
},
|
|
559
|
+
};
|
|
560
|
+
},
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const STATUS_CODES = {
|
|
565
|
+
402: "payment-required",
|
|
566
|
+
413: "too-large",
|
|
567
|
+
415: "unsupported-type",
|
|
568
|
+
401: "unauthorized",
|
|
569
|
+
403: "forbidden",
|
|
570
|
+
404: "not-found"
|
|
571
|
+
};
|