@panphora/clayjs 1.15.2 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -5
- package/THIRD-PARTY-NOTICES.md +16 -0
- package/compat/highlight-11.9.0.min.js +1213 -0
- package/compat/highlight-LICENSE +29 -0
- package/compat/journal-assets.lock.json +22 -0
- package/compat/overtype-2.6.1.min.js +116 -0
- package/compat/overtype-LICENSE +21 -0
- package/dist/all.js +90 -0
- package/dist/clay-dom.js +84 -0
- package/dist/clay-events.js +103 -0
- package/dist/clay-internals.js +85 -0
- package/dist/clay-options.js +93 -0
- package/dist/clay-router.js +98 -0
- package/dist/clay-ui.js +95 -0
- package/dist/clay-utils.js +84 -0
- package/dist/clay.js +146 -0
- package/dist/clay.standalone.js +84291 -57611
- package/dist/m/attrs/add-from-T5LJXFFI.js +1 -0
- package/dist/m/attrs/onaftersave-OKSWEJQF.js +1 -0
- package/dist/m/attrs/refetch-on-save-ZUE7PSCI.js +1 -0
- package/dist/m/attrs/save-freeze-MYNVXREN.js +1 -0
- package/dist/m/c-2AZDZWCK.js +1 -0
- package/dist/m/c-2SPVQSHC.js +1 -0
- package/dist/m/c-2YYDMLNZ.js +1 -0
- package/dist/m/c-336SGU5W.js +1 -0
- package/dist/m/c-33JJ6CBX.js +2 -0
- package/dist/m/c-3GJ4SDE2.js +1 -0
- package/dist/m/c-3H6BZDNJ.js +6 -0
- package/dist/m/c-3OWDMNJR.js +2 -0
- package/dist/m/c-3RUU4TPX.js +91 -0
- package/dist/m/c-3XIRD4NU.js +1 -0
- package/dist/m/c-46XYM55L.js +1 -0
- package/dist/m/c-4DFEJ2PO.js +3 -0
- package/dist/m/c-4JZDZFF6.js +1 -0
- package/dist/m/c-4L3BTFRK.js +1 -0
- package/dist/m/c-4Q2XTQ2N.js +2 -0
- package/dist/m/c-4WKFKEKF.js +1 -0
- package/dist/m/c-56QRZTZH.js +1 -0
- package/dist/m/c-5J4E7O4Y.js +4 -0
- package/dist/m/c-5LKDWQTL.js +19 -0
- package/dist/m/c-5QWQ7A5L.js +1 -0
- package/dist/m/c-5RJFTLSF.js +1 -0
- package/dist/m/c-5V3MOVJT.js +1 -0
- package/dist/m/c-5VYOWDTI.js +1 -0
- package/dist/m/c-5YIHJ3TJ.js +1 -0
- package/dist/m/c-6AZOGAPE.js +1 -0
- package/dist/m/c-6CRUUXOX.js +5 -0
- package/dist/m/c-6HMB24EV.js +1 -0
- package/dist/m/c-6KEVEQ7L.js +1 -0
- package/dist/m/c-6NC5I2TV.js +1 -0
- package/dist/m/c-74MGYWZF.js +1 -0
- package/dist/m/c-7DJDKPGL.js +1 -0
- package/dist/m/c-7OLLJPWP.js +1 -0
- package/dist/m/c-7SWMYKAI.js +1 -0
- package/dist/m/c-A3E3NIMC.js +1 -0
- package/dist/m/c-A7LS5UZW.js +1 -0
- package/dist/m/c-AMSNPOCX.js +1 -0
- package/dist/m/c-AQLFEATB.js +1 -0
- package/dist/m/c-B3JM5ZXC.js +1 -0
- package/dist/m/c-BBVGTQG7.js +1 -0
- package/dist/m/c-BPF2NSPV.js +1 -0
- package/dist/m/c-BQ6LOBEE.js +1 -0
- package/dist/m/c-BTDI4Y23.js +1 -0
- package/dist/m/c-BWFVSBDF.js +1 -0
- package/dist/m/c-BYTKL6UX.js +1 -0
- package/dist/m/c-CFHW5AMY.js +6 -0
- package/dist/m/c-CJ3A7ZKE.js +1 -0
- package/dist/m/c-CQHPGKX2.js +1 -0
- package/dist/m/c-CYWLPPUS.js +1 -0
- package/dist/m/c-CZUSOB7V.js +1 -0
- package/dist/m/c-DCFSEJZG.js +1 -0
- package/dist/m/c-DCXOXBF3.js +1 -0
- package/dist/m/c-DFS4XZJV.js +2 -0
- package/dist/m/c-DNENWY4O.js +1 -0
- package/dist/m/c-DNZ5IFM6.js +1 -0
- package/dist/m/c-E4LIR5X5.js +1 -0
- package/dist/m/c-EJTGYSLN.js +1 -0
- package/dist/m/c-EVCQ3BKA.js +1 -0
- package/dist/m/c-EZU56NQV.js +34 -0
- package/dist/m/c-FDNHHZY5.js +1 -0
- package/dist/m/c-FILLTJA7.js +1 -0
- package/dist/m/c-FRLHSY7E.js +1 -0
- package/dist/m/c-FROJUZMV.js +1 -0
- package/dist/m/c-G2JPKCTP.js +1 -0
- package/dist/m/c-GB6TU7WV.js +1 -0
- package/dist/m/c-GFJJEBTU.js +1 -0
- package/dist/m/c-GJAEGGXY.js +1 -0
- package/dist/m/c-GOWBDM6J.js +1 -0
- package/dist/m/c-GQ6F6IDK.js +1 -0
- package/dist/m/c-GSFIGKOP.js +1 -0
- package/dist/m/c-GY422N3Q.js +1 -0
- package/dist/m/c-H4LG442Q.js +1 -0
- package/dist/m/c-HA6INT6J.js +1 -0
- package/dist/m/c-HAWFNBYG.js +1 -0
- package/dist/m/c-HDDEEL77.js +1 -0
- package/dist/m/c-HFKUVACH.js +1 -0
- package/dist/m/c-HHDFCNHV.js +1 -0
- package/dist/m/c-HPURYVZS.js +2 -0
- package/dist/m/c-I2ZATQCD.js +2 -0
- package/dist/m/c-IEM4NTCS.js +1 -0
- package/dist/m/c-IJUK2GPW.js +1 -0
- package/dist/m/c-IW6GGOU2.js +1 -0
- package/dist/m/c-JAX2667S.js +6 -0
- package/dist/m/c-JDCX26UP.js +25 -0
- package/dist/m/c-JRKY44DK.js +1 -0
- package/dist/m/c-JU3NK25H.js +1 -0
- package/dist/m/c-KAU6E43D.js +1 -0
- package/dist/m/c-KBS3UV6B.js +1 -0
- package/dist/m/c-KGVGROZT.js +1 -0
- package/dist/m/c-KI7M4AY3.js +1 -0
- package/dist/m/c-KIPQJX4B.js +1 -0
- package/dist/m/c-KQ6TYJAP.js +135 -0
- package/dist/m/c-KU3ZFADW.js +1 -0
- package/dist/m/c-L4WQSC27.js +1 -0
- package/dist/m/c-LFPHCXY3.js +1 -0
- package/dist/m/c-LRPUO2MX.js +1 -0
- package/dist/m/c-LWLBHLVC.js +3 -0
- package/dist/m/c-LZJPNR2W.js +1 -0
- package/dist/m/c-M2L47S2X.js +7 -0
- package/dist/m/c-MFP2ZWD4.js +1 -0
- package/dist/m/c-MKSQ4Y57.js +1 -0
- package/dist/m/c-MTQHWQC3.js +1 -0
- package/dist/m/c-N326R2JD.js +1 -0
- package/dist/m/c-N7JPUPGC.js +1 -0
- package/dist/m/c-NRPJEVJS.js +1 -0
- package/dist/m/c-NUFFJVVJ.js +1 -0
- package/dist/m/c-NUHP2XDB.js +1 -0
- package/dist/m/c-NUHSPT5P.js +1 -0
- package/dist/m/c-O72HVYSA.js +1 -0
- package/dist/m/c-OCSZ4QGN.js +1 -0
- package/dist/m/c-OIBUTDXI.js +1 -0
- package/dist/m/c-OIDRZKAF.js +1 -0
- package/dist/m/c-OW5QIGG3.js +1 -0
- package/dist/m/c-P7KNDG34.js +1 -0
- package/dist/m/c-PGRCK73F.js +1 -0
- package/dist/m/c-PQ5LK6QE.js +993 -0
- package/dist/m/c-PS5ARVWR.js +9 -0
- package/dist/m/c-PZ4XEPUP.js +2 -0
- package/dist/m/c-Q622NS2Y.js +1 -0
- package/dist/m/c-Q7NQD65V.js +1 -0
- package/dist/m/c-QH63LQOS.js +1 -0
- package/dist/m/c-QHIWXHRO.js +1 -0
- package/dist/m/c-QHWJ5ZIJ.js +1 -0
- package/dist/m/c-QICP2YR5.js +1 -0
- package/dist/m/c-QJ4OM3G6.js +1 -0
- package/dist/m/c-QNKREOL6.js +1 -0
- package/dist/m/c-R3L7ZJGB.js +2 -0
- package/dist/m/c-R7IWVINQ.js +1 -0
- package/dist/m/c-RM7J7BV2.js +1 -0
- package/dist/m/c-RSMOODDU.js +1 -0
- package/dist/m/c-RVMWGXEJ.js +1 -0
- package/dist/m/c-RYUXUUBX.js +1 -0
- package/dist/m/c-S4ZEH46B.js +9 -0
- package/dist/m/c-S7H2DRCB.js +1 -0
- package/dist/m/c-S7LK4KUE.js +1 -0
- package/dist/m/c-SDAXDM2D.js +1 -0
- package/dist/m/c-SGIA4IEQ.js +188 -0
- package/dist/m/c-SMBZG5X4.js +1 -0
- package/dist/m/c-SMGU7RT3.js +235 -0
- package/dist/m/c-SPIORRYS.js +1 -0
- package/dist/m/c-SWLRT66R.js +1 -0
- package/dist/m/c-SZGYW5HT.js +1 -0
- package/dist/m/c-TCCA5BZN.js +2 -0
- package/dist/m/c-TCJTVAFN.js +1 -0
- package/dist/m/c-TJ6WOJL3.js +67 -0
- package/dist/m/c-TQEMVJCS.js +1 -0
- package/dist/m/c-TRS6TM6O.js +1 -0
- package/dist/m/c-TYLXX4NF.js +21 -0
- package/dist/m/c-U62OB4BT.js +1 -0
- package/dist/m/c-U6YWUAR3.js +8 -0
- package/dist/m/c-UDCSU5G5.js +2 -0
- package/dist/m/c-UDUUJPFZ.js +1 -0
- package/dist/m/c-UGHH4MB3.js +1 -0
- package/dist/m/c-UL2YUY52.js +1 -0
- package/dist/m/c-VAK7SG37.js +1 -0
- package/dist/m/c-VDSS4K3Z.js +1 -0
- package/dist/m/c-VE6ZMYMC.js +1 -0
- package/dist/m/c-VG5XIN4T.js +1 -0
- package/dist/m/c-VHM5XQHK.js +1 -0
- package/dist/m/c-VHNSBD2M.js +1 -0
- package/dist/m/c-VI5RMSVY.js +1 -0
- package/dist/m/c-VKEDWYDF.js +1 -0
- package/dist/m/c-VRZN56XY.js +1 -0
- package/dist/m/c-W5MCHEGA.js +1 -0
- package/dist/m/c-WDC5GVKU.js +1 -0
- package/dist/m/c-WN5AY3OZ.js +1 -0
- package/dist/m/c-WNYBLIOZ.js +1 -0
- package/dist/m/c-WRBKEJD4.js +1 -0
- package/dist/m/c-WWXIDH4O.js +28 -0
- package/dist/m/c-WYFDLITL.js +1 -0
- package/dist/m/c-XCNTK76U.js +1 -0
- package/dist/m/c-XMS6DPBR.js +1 -0
- package/dist/m/c-XN6MBKI6.js +67 -0
- package/dist/m/c-XPELB6E5.js +1 -0
- package/dist/m/c-XQXZAO7N.js +1 -0
- package/dist/m/c-Y5HBBIQ4.js +1 -0
- package/dist/m/c-ZE46QDIH.js +1 -0
- package/dist/m/c-ZUCOKOHT.js +1 -0
- package/dist/m/c-ZVPVXTJF.js +4 -0
- package/dist/m/c-ZVRKXKM7.js +1 -0
- package/dist/m/c-ZWIOJGMT.js +1 -0
- package/dist/m/core/admin-attrs-QHOKPJ7W.js +1 -0
- package/dist/m/core/autosave-HRYGUWV6.js +1 -0
- package/dist/m/core/conflict-notice-6MROJN2L.js +1 -0
- package/dist/m/core/draft-mode-WSQAT67S.js +1 -0
- package/dist/m/core/host-permission-FNXVEFNE.js +1 -0
- package/dist/m/core/is-edit-mode-KTWA3O5N.js +1 -0
- package/dist/m/core/persist-JFYRHMRJ.js +3 -0
- package/dist/m/core/recovery-FLTCC2OC.js +1 -0
- package/dist/m/core/save-HQZ4S667.js +1 -0
- package/dist/m/core/save-core-KGQWFUNH.js +1 -0
- package/dist/m/core/snapshot-EGQZNT45.js +1 -0
- package/dist/m/core/unsaved-warning-IDJLD66H.js +1 -0
- package/dist/m/dom/all-APS5ZSKP.js +1 -0
- package/dist/m/dom/dom-helpers-L7OSR6S4.js +1 -0
- package/dist/m/events/index-RN2JNGBV.js +1 -0
- package/dist/m/internals/index-PVXGPUDA.js +1 -0
- package/dist/m/lib/cache-bust-MUMFXZK6.js +1 -0
- package/dist/m/lib/region-policy-SB2AWVG5.js +1 -0
- package/dist/m/loader-S4I7GKXR.js +1 -0
- package/dist/m/manifest-4C419D67.js +1 -0
- package/dist/m/manifest-edit-B077F288.js +1 -0
- package/dist/m/manifest.json +3988 -0
- package/dist/m/options/options-EJMS3K24.js +1 -0
- package/dist/m/page/editor-NNJA4OKD.js +202 -0
- package/dist/m/plugins/ai-edit-QN4PMMNL.js +6 -0
- package/dist/m/plugins/bind-text-NX46N5UV.js +1 -0
- package/dist/m/plugins/cms-HP3LSTPN.js +2293 -0
- package/dist/m/plugins/demo-RM5IR6AO.js +1 -0
- package/dist/m/plugins/derive-UAAA4V4S.js +1 -0
- package/dist/m/plugins/indicator-URDBBOQO.js +1 -0
- package/dist/m/plugins/live-HFWD4OIH.js +1 -0
- package/dist/m/plugins/menu-3HIEQSI6.js +138 -0
- package/dist/m/plugins/message-LQ6EDH4V.js +1 -0
- package/dist/m/plugins/page-JOKB6XPR.js +2 -0
- package/dist/m/plugins/page-view-NXOJHNW6.js +1 -0
- package/dist/m/plugins/people-SGPJKANK.js +1 -0
- package/dist/m/plugins/quickcrop-QNY3PKLD.js +26 -0
- package/dist/m/plugins/quickcrop-QNY3PKLD.js.LEGAL.txt +7 -0
- package/dist/m/plugins/richclay-NRQDSWX6.js +517 -0
- package/dist/m/plugins/richclay-NRQDSWX6.js.LEGAL.txt +1 -0
- package/dist/m/plugins/sortable-GWT4PWDV.js +1 -0
- package/dist/m/plugins/source-4ZRSAIYM.js +1 -0
- package/dist/m/plugins/undo-VDA5P7FH.js +1 -0
- package/dist/m/plugins/unsent-HJFLQJ2O.js +1 -0
- package/dist/m/plugins/upgrade-W7FG5VFG.js +1 -0
- package/dist/m/plugins/upload-WLYKK6TL.js +1 -0
- package/dist/m/plugins/visitor-drafts-LIDY7NH6.js +21 -0
- package/dist/m/plugins/visitor-drafts-owner-ZXCKBZE7.js +1 -0
- package/dist/m/plugins/wire-4N6GYAF6.js +1 -0
- package/dist/m/reader-core-DXYCURFF.js +68 -0
- package/dist/m/route/route-KOCZSEGN.js +1 -0
- package/dist/m/sync/live-sync-J7VMYTKS.js +1 -0
- package/dist/m/ui/index-3JLVI27O.js +1 -0
- package/dist/m/utils/index-4TJCVLT7.js +1 -0
- package/dist/m/vendor/Sortable.vendor-A755ITLN.js +3 -0
- package/dist/m/vendor/Sortable.vendor-A755ITLN.js.LEGAL.txt +1 -0
- package/dist/m/vendor/sap-core.vendor-A7BMUYZT.js +6 -0
- package/dist/offline-manifest.json +4034 -0
- package/dist/page/themes/light-1.css +430 -0
- package/dist/server/core.cjs +23748 -22855
- package/dist/server/offline-runtime.cjs +4954 -0
- package/dist/server/offline-runtime.mjs +4923 -0
- package/dist/server/rooms.mjs +9 -0
- package/docs/guide.md +1078 -669
- package/entries/all.js +2 -0
- package/entries/clay-data.js +1 -1
- package/entries/clay-dom.js +2 -0
- package/entries/clay-events.js +2 -0
- package/entries/clay-internals.js +2 -0
- package/entries/clay-options.js +2 -0
- package/entries/clay-router.js +2 -0
- package/entries/clay-ui.js +2 -0
- package/entries/clay-utils.js +2 -0
- package/entries/clay.js +2 -0
- package/entries/sap.js +6 -6
- package/fonts/ibm-plex-mono-LICENSE +93 -0
- package/fonts/ibm-plex-mono-latin-300-normal.woff2 +0 -0
- package/fonts/ibm-plex-mono-latin-400-italic.woff2 +0 -0
- package/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- package/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- package/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
- package/fonts/ibm-plex-sans-LICENSE +93 -0
- package/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
- package/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
- package/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
- package/fonts/ibm-plex.css +71 -0
- package/fonts/inter-LICENSE +93 -0
- package/fonts/inter-cyrillic-ext-wght-normal.woff2 +0 -0
- package/fonts/inter-cyrillic-wght-normal.woff2 +0 -0
- package/fonts/inter-greek-ext-wght-normal.woff2 +0 -0
- package/fonts/inter-greek-wght-normal.woff2 +0 -0
- package/fonts/inter-latin-ext-wght-normal.woff2 +0 -0
- package/fonts/inter-latin-wght-normal.woff2 +0 -0
- package/fonts/inter-vietnamese-wght-normal.woff2 +0 -0
- package/fonts/inter.css +62 -0
- package/fonts/newsreader-LICENSE +93 -0
- package/fonts/newsreader-latin-ext-opsz-normal.woff2 +0 -0
- package/fonts/newsreader-latin-opsz-normal.woff2 +0 -0
- package/fonts/newsreader-vietnamese-opsz-normal.woff2 +0 -0
- package/fonts/newsreader.css +26 -0
- package/fonts/work-sans-LICENSE +93 -0
- package/fonts/work-sans-latin-wght-normal.woff2 +0 -0
- package/fonts/work-sans.css +8 -0
- package/package.json +20 -3
- package/packed-contract.json +1 -0
- package/src/attrs/add-from.js +27 -4
- package/src/attrs/save-freeze.js +7 -52
- package/src/core/admin-attrs.js +119 -12
- package/src/core/admin-contenteditable.js +12 -10
- package/src/core/admin-inputs.js +14 -15
- package/src/core/admin-onclick.js +2 -0
- package/src/core/autosave.js +74 -21
- package/src/core/boot-phase.js +2 -1
- package/src/core/conflict-download.js +19 -23
- package/src/core/conflict-notice-ui.js +1173 -0
- package/src/core/conflict-notice.js +140 -697
- package/src/core/download.js +32 -0
- package/src/core/edit-mode.js +147 -14
- package/src/core/etag.js +30 -12
- package/src/core/host-meta.js +90 -8
- package/src/core/host-permission.js +29 -0
- package/src/core/is-edit-mode.js +145 -12
- package/src/core/mode-key.js +125 -0
- package/src/core/mode-lifecycle.js +544 -0
- package/src/core/mode-receipt.js +30 -0
- package/src/core/mode-refusal.js +13 -0
- package/src/core/mode-resources.js +52 -0
- package/src/core/mode-support.js +82 -0
- package/src/core/mode-switch.js +259 -0
- package/src/core/mode-transition.js +112 -0
- package/src/core/page-data-write.js +305 -0
- package/src/core/page-data.js +26 -227
- package/src/core/page-format-wire.js +4 -0
- package/src/core/page-format.js +58 -0
- package/src/core/persist.js +37 -9
- package/src/core/preserve-authored.js +114 -0
- package/src/core/recovery.js +772 -0
- package/src/core/runtime-url.js +105 -0
- package/src/core/save-core.js +113 -12
- package/src/core/save-replace.js +72 -0
- package/src/core/save-size-notice.js +134 -0
- package/src/core/save-size.js +140 -0
- package/src/core/save.js +806 -119
- package/src/core/snapshot.js +159 -11
- package/src/core/source-map.js +157 -9
- package/src/core/unsaved-warning.js +14 -4
- package/src/core/upgrade-run.js +408 -0
- package/src/core/version.js +18 -0
- package/src/lib/assets.js +30 -0
- package/src/lib/autosave-debug-gate.js +50 -0
- package/src/lib/autosave-scheduler.js +140 -0
- package/src/lib/boot-imports.js +8 -0
- package/src/lib/derived.js +3 -1
- package/src/lib/dirty-gate.js +15 -0
- package/src/lib/doc-storage.js +33 -0
- package/src/lib/effect.js +9 -4
- package/src/lib/mutation.js +4 -2
- package/src/lib/offline-runtime.js +226 -0
- package/src/lib/preload.js +48 -0
- package/src/lib/root-attrs.js +1 -1
- package/src/lib/trash.js +18 -0
- package/src/lib/undo/capture-move.js +152 -0
- package/src/live/local-channel.js +61 -0
- package/src/live/local-lane.js +60 -9
- package/src/live/server-lane.js +6 -3
- package/src/loader-logic.js +76 -15
- package/src/loader.js +218 -30
- package/src/page/adopt-boot.js +76 -22
- package/src/page/adopt.js +25 -4
- package/src/page/awareness.js +230 -0
- package/src/page/blocks.js +113 -6
- package/src/page/blocksel.js +20 -4
- package/src/page/board-face.js +13 -0
- package/src/page/board-panel.js +5 -17
- package/src/page/board-style.js +2 -13
- package/src/page/board-view.js +1 -11
- package/src/page/board-views.js +3 -0
- package/src/page/board.js +2 -2
- package/src/page/boot-pack.js +26 -0
- package/src/page/boot.js +237 -0
- package/src/page/chart-tables.js +11 -0
- package/src/page/chart.js +6 -10
- package/src/page/checklist.js +1 -1
- package/src/page/checkpoint-proof.js +289 -0
- package/src/page/chrome.js +431 -63
- package/src/page/csv-import.js +17 -2
- package/src/page/document-title-dom.js +52 -0
- package/src/page/document-title-runtime.js +90 -0
- package/src/page/document-title.js +12 -0
- package/src/page/drag.js +48 -13
- package/src/page/drop.js +2 -1
- package/src/page/editor.js +399 -90
- package/src/page/emoji-data.js +162 -0
- package/src/page/feature-loader.js +72 -0
- package/src/page/features/board.js +21 -0
- package/src/page/features/chart.js +11 -0
- package/src/page/features/code.js +13 -0
- package/src/page/features/dates.js +180 -0
- package/src/page/features/emoji.js +187 -0
- package/src/page/features/media.js +290 -0
- package/src/page/features/mentions.js +188 -0
- package/src/page/features/registry.js +28 -0
- package/src/page/features/table.js +383 -0
- package/src/page/{tool-view.js → features/tool.js} +33 -2
- package/src/page/format-codec.js +215 -0
- package/src/page/format-command.js +9 -0
- package/src/page/format-gate.js +179 -0
- package/src/page/format-host.js +54 -0
- package/src/page/format-level.js +157 -0
- package/src/page/format-native-commands.js +142 -0
- package/src/page/format-native.js +42 -0
- package/src/page/highlight-slot.js +23 -0
- package/src/page/highlight.js +30 -16
- package/src/page/history-intent.js +78 -0
- package/src/page/inline.js +45 -28
- package/src/page/items.js +67 -14
- package/src/page/link.js +1 -0
- package/src/page/markdown.js +112 -3
- package/src/page/media-views.js +3 -0
- package/src/page/media.js +33 -245
- package/src/page/mention-picker.js +99 -0
- package/src/page/merge.js +1 -1
- package/src/page/outline-cache.js +11 -0
- package/src/page/placeholder.js +27 -7
- package/src/page/projection.js +240 -51
- package/src/page/reader-patch.js +306 -0
- package/src/page/recover.js +1 -1
- package/src/page/rich/page-tool.js +1 -4
- package/src/page/runtime-owner.js +27 -0
- package/src/page/save-projection.js +46 -4
- package/src/page/save-states.js +1 -1
- package/src/page/schema.js +47 -0
- package/src/page/table-rect-key.js +3 -0
- package/src/page/table-select.js +4 -3
- package/src/page/table-style.js +1 -1
- package/src/page/table-views.js +3 -0
- package/src/page/table.js +8 -293
- package/src/page/themes/content.css +25 -1
- package/src/page/themes/light-1.css +163 -2
- package/src/page/themes/light-1.source.css +269 -0
- package/src/page/tool-capability.js +3 -0
- package/src/page/tool-views.js +3 -0
- package/src/page/view-hooks.js +32 -0
- package/src/page/view.js +390 -107
- package/src/page/ybind/awareness.js +172 -0
- package/src/page/ybind/binding.js +40 -5
- package/src/page/ybind/bridge.js +50 -47
- package/src/page/ybind/decode.js +52 -19
- package/src/page/ybind/doc.js +27 -2
- package/src/page/ybind/group.js +12 -3
- package/src/page/ybind/index.js +2 -0
- package/src/page/ybind/lineage.js +71 -0
- package/src/page/ybind/marks.js +26 -1
- package/src/page/ybind/migrate.js +48 -13
- package/src/page/ybind/moves.js +32 -21
- package/src/page/ybind/positions.js +50 -0
- package/src/page/ybind/provider.js +97 -24
- package/src/page/ybind/recovery-proof.js +191 -0
- package/src/page/ybind/recovery-replay.js +488 -0
- package/src/page/ybind/tokens.js +15 -6
- package/src/page/ybind/walk.js +10 -3
- package/src/page/ybind/writer.js +48 -4
- package/src/plugins/ai-edit.js +167 -32
- package/src/plugins/bind-text.js +218 -0
- package/src/plugins/cms.js +93 -0
- package/src/plugins/indicator.js +57 -5
- package/src/plugins/live.js +319 -35
- package/src/plugins/menu/claygoo.svg.js +1 -0
- package/src/plugins/menu/seo-head.js +323 -0
- package/src/plugins/menu/seo.js +849 -0
- package/src/plugins/menu.js +425 -0
- package/src/plugins/page-view.js +131 -0
- package/src/plugins/page.js +1131 -270
- package/src/plugins/people.js +158 -31
- package/src/plugins/richclay.js +119 -9
- package/src/plugins/sortable.js +370 -60
- package/src/plugins/source.js +189 -366
- package/src/plugins/undo.js +289 -19
- package/src/plugins/unsent.js +132 -0
- package/src/plugins/upgrade-run.js +1 -0
- package/src/plugins/upgrade.js +210 -0
- package/src/plugins/upload-task.js +571 -0
- package/src/plugins/upload-ui.js +265 -0
- package/src/plugins/upload.js +34 -153
- package/src/plugins/visitor-drafts/inert.js +32 -0
- package/src/plugins/visitor-drafts/intro.js +2 -1
- package/src/plugins/visitor-drafts/owner-chip.js +3 -0
- package/src/plugins/visitor-drafts/prompt.js +2 -2
- package/src/plugins/visitor-drafts-owner.js +85 -0
- package/src/plugins/visitor-drafts.js +31 -42
- package/src/plugins/wire.js +82 -1
- package/src/reader-core.js +26 -0
- package/src/recovery/conflicts.js +287 -0
- package/src/recovery/identity.js +154 -0
- package/src/recovery/store.js +792 -0
- package/src/recovery/undo.js +257 -0
- package/src/server/checkpoint/index.js +68 -4
- package/src/server/offline-runtime/index.js +214 -0
- package/src/server/rooms/index.js +2 -1
- package/src/server/rooms/live-data-rooms.js +2 -2
- package/src/server/rooms/page-admission.js +33 -0
- package/src/server/rooms/page-lineage.js +56 -0
- package/src/server/rooms/page-rebase.js +11 -5
- package/src/server/rooms/page-state.js +2 -0
- package/src/server/rooms/page-write-admission.js +9 -3
- package/src/source/session.js +705 -0
- package/src/sync/conflict-revert.js +5 -4
- package/src/sync/conflicts-edit.js +119 -0
- package/src/sync/conflicts-facade.js +49 -0
- package/src/sync/conflicts.js +99 -1
- package/src/sync/editing-marker.js +37 -6
- package/src/sync/editing-sender.js +8 -1
- package/src/sync/live-sync-edit.js +63 -0
- package/src/sync/live-sync.js +364 -119
- package/src/sync/local-presence.js +187 -0
- package/src/sync/merge-declarations.js +10 -0
- package/src/sync/merge-engine.js +12 -0
- package/src/sync/presence-store.js +412 -0
- package/src/sync/presence-target.js +243 -0
- package/src/sync/presence.js +72 -41
- package/src/sync/saved-handoff.js +200 -0
- package/src/sync/saved-stream.js +544 -0
- package/src/sync/version-guard.js +92 -0
- package/src/sync/wire-profile.js +81 -0
- package/src/ui/bevel-controls.js +2 -8
- package/src/ui/page-scheme.js +7 -0
- package/src/ui/skins/cms.js +1 -1
- package/src/ui/theme-config.js +150 -0
- package/src/ui/theme.js +6 -138
- package/src/ui/toast-lazy.js +11 -0
- package/src/vendor/LICENSES-optional.md +253 -0
- package/src/vendor/highlight.vendor.js +3 -0
- package/src/vendor/hljs-bash.vendor.js +2 -0
- package/src/vendor/hljs-css.vendor.js +2 -0
- package/src/vendor/hljs-javascript.vendor.js +2 -0
- package/src/vendor/hljs-json.vendor.js +2 -0
- package/src/vendor/hljs-python.vendor.js +2 -0
- package/src/vendor/hljs-xml.vendor.js +2 -0
- package/src/vendor/hyper-html-api-data.vendor.js +1 -1
- package/src/vendor/hyper-html-api-upgrade.vendor.js +5 -0
- package/src/vendor/hyper-morph-identity.vendor.js +2 -0
- package/src/vendor/hyper-morph-json.vendor.js +3 -0
- package/src/vendor/hyper-morph.vendor.js +6 -6
- package/src/vendor/hyper-undo.vendor.js +1 -1
- package/src/vendor/hypercms.vendor.js +169 -84
- package/src/vendor/marked.vendor.js +68 -0
- package/src/vendor/overtype.vendor.js +994 -0
- package/src/vendor/richclay.vendor.js +17 -17
- package/src/vendor/sap-core.vendor.js +5 -5
- package/src/vendor/sap-ownership.vendor.js +11 -7
package/src/page/boot.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
export const PHASES = Object.freeze({
|
|
2
|
+
loading: "loading",
|
|
3
|
+
joining: "joining",
|
|
4
|
+
adopting: "adopting",
|
|
5
|
+
ready: "ready",
|
|
6
|
+
blocked: "blocked",
|
|
7
|
+
error: "error",
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const TERMINAL = new Set([PHASES.ready, PHASES.blocked, PHASES.error]);
|
|
11
|
+
|
|
12
|
+
let installs = 0;
|
|
13
|
+
|
|
14
|
+
const monotonic = () => globalThis.performance?.now?.() ?? Date.now();
|
|
15
|
+
|
|
16
|
+
export function createInstall({ gen = 0, isCurrent = () => true, onPhase = null } = {}) {
|
|
17
|
+
installs += 1;
|
|
18
|
+
return {
|
|
19
|
+
installId: installs,
|
|
20
|
+
gen,
|
|
21
|
+
isCurrent,
|
|
22
|
+
onPhase: typeof onPhase === "function" ? onPhase : null,
|
|
23
|
+
phase: PHASES.loading,
|
|
24
|
+
result: null,
|
|
25
|
+
at: monotonic(),
|
|
26
|
+
controller: new AbortController(),
|
|
27
|
+
disposers: [],
|
|
28
|
+
disposed: false,
|
|
29
|
+
steps: null,
|
|
30
|
+
deferred: null,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isCurrent(inst) {
|
|
35
|
+
return !inst.disposed && !inst.controller.signal.aborted && inst.isCurrent() !== false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// One owner: the view this installation mounted, and nothing a later or a cancelled
|
|
39
|
+
// installation staged. The final input gate asks this before it accepts a key.
|
|
40
|
+
export function ownsView(inst, mounted) {
|
|
41
|
+
return isCurrent(inst) && !!mounted?.view && inst.mounted === mounted.view;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function setPhase(inst, phase, reason = null) {
|
|
45
|
+
if (TERMINAL.has(inst.phase) && inst.result) return inst.result;
|
|
46
|
+
inst.phase = phase;
|
|
47
|
+
inst.at = monotonic();
|
|
48
|
+
inst.result = Object.freeze({ phase, reason: reason ?? null, installId: inst.installId, at: inst.at });
|
|
49
|
+
try {
|
|
50
|
+
inst.onPhase?.(inst.result);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error("clayjs: page boot phase callback failed", error);
|
|
53
|
+
}
|
|
54
|
+
return inst.result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function finishBlocked(inst, reason) {
|
|
58
|
+
const result = setPhase(inst, PHASES.blocked, reason);
|
|
59
|
+
inst.editorReadyResolve?.();
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function finishError(inst, reason) {
|
|
64
|
+
return setPhase(inst, PHASES.error, reason);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function addDisposer(inst, dispose) {
|
|
68
|
+
if (typeof dispose === "function") inst.disposers.push(dispose);
|
|
69
|
+
return dispose;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function disposeInstall(inst) {
|
|
73
|
+
if (inst.disposed) return;
|
|
74
|
+
inst.disposed = true;
|
|
75
|
+
inst.mounted = null;
|
|
76
|
+
try {
|
|
77
|
+
inst.controller.abort();
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error("clayjs: page boot abort failed", error);
|
|
80
|
+
}
|
|
81
|
+
const disposers = inst.disposers.splice(0).reverse();
|
|
82
|
+
for (const dispose of disposers) {
|
|
83
|
+
try {
|
|
84
|
+
dispose();
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error("clayjs: a page boot disposer failed", error);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function mark(name, inst) {
|
|
92
|
+
const perf = globalThis.performance;
|
|
93
|
+
if (typeof perf?.mark !== "function") return;
|
|
94
|
+
try {
|
|
95
|
+
perf.mark(name, { detail: { installId: inst.installId } });
|
|
96
|
+
} catch (error) {
|
|
97
|
+
try { perf.mark(name); } catch (ignored) { return; }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// A boot that ends without an editor gives up what its steps acquired on the way
|
|
102
|
+
// there, through the install's own teardown, before it reports where it stopped.
|
|
103
|
+
function release(inst, steps) {
|
|
104
|
+
try {
|
|
105
|
+
steps.release?.(inst);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error("clayjs: page boot release failed", error);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function stop(inst, result, steps) {
|
|
112
|
+
if (result.cancelled) return null;
|
|
113
|
+
release(inst, steps);
|
|
114
|
+
return finishBlocked(inst, result.reason ?? "boot-incomplete");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// A branch of the parallel boot reports its own failure here, so a rejection in one
|
|
118
|
+
// cannot leave an unhandled rejection or a late continuation mounting the other. The
|
|
119
|
+
// mark it owns is emitted when it settles, which is what proves the two overlapped.
|
|
120
|
+
function branch(promise, inst, reason, name = null) {
|
|
121
|
+
return Promise.resolve(promise)
|
|
122
|
+
.then((result) => result ?? { ok: false, reason }, (error) => {
|
|
123
|
+
console.error(`clayjs: page boot ${reason}`, error);
|
|
124
|
+
return { ok: false, reason };
|
|
125
|
+
})
|
|
126
|
+
.then((result) => {
|
|
127
|
+
if (result.ok && name && isCurrent(inst)) mark(name, inst);
|
|
128
|
+
return result;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// The wait on the parallel branches: the first one to fail, or the install's abort,
|
|
133
|
+
// answers it at once. A boot blocked by the download that failed must not sit in
|
|
134
|
+
// `joining` on the join that never answered, and a disposed install must not wait for
|
|
135
|
+
// either of them.
|
|
136
|
+
function settleBranches(inst, branches) {
|
|
137
|
+
return new Promise((resolve) => {
|
|
138
|
+
const signal = inst.controller.signal;
|
|
139
|
+
let done = false;
|
|
140
|
+
const finish = (outcome) => {
|
|
141
|
+
if (done) return;
|
|
142
|
+
done = true;
|
|
143
|
+
resolve(outcome);
|
|
144
|
+
};
|
|
145
|
+
if (signal.aborted) return finish({ cancelled: true });
|
|
146
|
+
signal.addEventListener("abort", () => finish({ cancelled: true }), { once: true });
|
|
147
|
+
const results = new Array(branches.length);
|
|
148
|
+
let left = branches.length;
|
|
149
|
+
branches.forEach((branch, index) => {
|
|
150
|
+
branch.then((result) => {
|
|
151
|
+
if (done) return;
|
|
152
|
+
if (!result.ok) return finish({ result, branch });
|
|
153
|
+
results[index] = result;
|
|
154
|
+
left -= 1;
|
|
155
|
+
if (left === 0) finish({ results });
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// One coordinator for the edit boot: read the source, take the assets while
|
|
162
|
+
// authority is prepared, mount, adopt, then enable input. Every step reports an
|
|
163
|
+
// outcome and every await is followed by an ownership check, so a late or
|
|
164
|
+
// cancelled installation can neither mutate the page nor mark it ready.
|
|
165
|
+
export async function runBoot(inst, steps, { activate = true } = {}) {
|
|
166
|
+
mark("document:boot", inst);
|
|
167
|
+
setPhase(inst, PHASES.loading);
|
|
168
|
+
|
|
169
|
+
const read = steps.readSource(inst);
|
|
170
|
+
if (!isCurrent(inst)) return null;
|
|
171
|
+
if (!read.ok) return finishBlocked(inst, read.reason);
|
|
172
|
+
const source = read.source;
|
|
173
|
+
|
|
174
|
+
// The editor's code arrives while authority is prepared, so the wait is the longer
|
|
175
|
+
// of the two plus adoption rather than their sum. Neither branch subscribes a
|
|
176
|
+
// resource or touches the page: only the coordinator does, after both are in.
|
|
177
|
+
setPhase(inst, PHASES.joining);
|
|
178
|
+
const settled = await settleBranches(inst, [
|
|
179
|
+
branch(steps.loadEditorAssets(inst, source), inst, "editor-assets-failed", "document:assets-ready"),
|
|
180
|
+
branch(steps.prepareAuthority(inst, source), inst, "authority-failed", "document:authority-ready"),
|
|
181
|
+
]);
|
|
182
|
+
if (settled.cancelled || !isCurrent(inst)) return null;
|
|
183
|
+
if (!settled.results) {
|
|
184
|
+
// The branch still on the wire is not worth waiting for, and it must not go on to
|
|
185
|
+
// subscribe anything: the abort ends it where its next ownership check is.
|
|
186
|
+
try {
|
|
187
|
+
inst.controller.abort();
|
|
188
|
+
} catch (error) {
|
|
189
|
+
console.error("clayjs: page boot abort failed", error);
|
|
190
|
+
}
|
|
191
|
+
return stop(inst, settled.result, steps);
|
|
192
|
+
}
|
|
193
|
+
const [loaded, admitted] = settled.results;
|
|
194
|
+
|
|
195
|
+
const mounted = steps.mountAndAttach(inst, source, loaded, admitted);
|
|
196
|
+
if (!isCurrent(inst)) return null;
|
|
197
|
+
if (!mounted.ok) return stop(inst, mounted, steps);
|
|
198
|
+
inst.mounted = mounted.view ?? null;
|
|
199
|
+
|
|
200
|
+
setPhase(inst, PHASES.adopting);
|
|
201
|
+
const adoption = await steps.adoptSource(inst, source, mounted);
|
|
202
|
+
if (!isCurrent(inst)) return null;
|
|
203
|
+
if (!adoption.ok) return stop(inst, adoption, steps);
|
|
204
|
+
|
|
205
|
+
// A session that has not committed yet stages the editor and leaves input and every
|
|
206
|
+
// write closed: the checks input waits for run now, so the commit that opens it has
|
|
207
|
+
// nothing left to wait on or fail.
|
|
208
|
+
if (!activate) {
|
|
209
|
+
if (steps.prepareInput) {
|
|
210
|
+
const prepared = await steps.prepareInput(inst, mounted);
|
|
211
|
+
if (!isCurrent(inst)) return null;
|
|
212
|
+
if (!prepared.ok) return stop(inst, prepared, steps);
|
|
213
|
+
}
|
|
214
|
+
inst.deferred = mounted;
|
|
215
|
+
return { phase: inst.phase, view: mounted.view, deferred: true };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const enabled = await steps.enableInput(inst, mounted);
|
|
219
|
+
if (!isCurrent(inst)) return null;
|
|
220
|
+
if (!enabled.ok) return stop(inst, enabled, steps);
|
|
221
|
+
|
|
222
|
+
mark("document:input-ready", inst);
|
|
223
|
+
return setPhase(inst, PHASES.ready);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// The commit that enters edit opens the input the preparation held back. Every check
|
|
227
|
+
// it needs passed for this very mount, so it is synchronous: it changes activation
|
|
228
|
+
// only, never durable data.
|
|
229
|
+
export function activateInput(inst) {
|
|
230
|
+
const mounted = inst.deferred;
|
|
231
|
+
inst.deferred = null;
|
|
232
|
+
if (!mounted || !isCurrent(inst)) return inst.result ?? null;
|
|
233
|
+
const opened = inst.steps?.openInput?.(inst, mounted);
|
|
234
|
+
if (!opened?.ok) return finishBlocked(inst, opened?.reason ?? "input-refused");
|
|
235
|
+
mark("document:input-ready", inst);
|
|
236
|
+
return setPhase(inst, PHASES.ready);
|
|
237
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { schema } from "./schema.js";
|
|
2
|
+
import { orderedIds } from "./model.js";
|
|
3
|
+
|
|
4
|
+
// The tables a chart can be drawn from: a top-level table with at least one
|
|
5
|
+
// column. The insert menu reads this without the chart module itself, which only
|
|
6
|
+
// a document that takes a chart loads.
|
|
7
|
+
export function chartTables(doc) {
|
|
8
|
+
const tables = [];
|
|
9
|
+
doc.forEach((node) => { if (node.type === schema.nodes.table && orderedIds(node.attrs.cols).length) tables.push(node); });
|
|
10
|
+
return tables;
|
|
11
|
+
}
|
package/src/page/chart.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { state } from "../vendor/prosemirror.vendor.js";
|
|
2
1
|
import { schema } from "./schema.js";
|
|
3
2
|
import { blockJson, keyBetween, newId, orderedIds } from "./model.js";
|
|
3
|
+
import { chartTables } from "./chart-tables.js";
|
|
4
4
|
import { chartModel, renderChartSvg } from "./rich/chart.js";
|
|
5
5
|
import { openPageMenu } from "./chrome.js";
|
|
6
6
|
import { project } from "./projection.js";
|
|
@@ -50,11 +50,7 @@ export function insertChartAfterTable(view, tablePos) {
|
|
|
50
50
|
return true;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
export
|
|
54
|
-
const tables = [];
|
|
55
|
-
doc.forEach((node) => { if (node.type === schema.nodes.table && orderedIds(node.attrs.cols).length) tables.push(node); });
|
|
56
|
-
return tables;
|
|
57
|
-
}
|
|
53
|
+
export { chartTables };
|
|
58
54
|
|
|
59
55
|
// /chart: an empty line becomes the chart, otherwise it goes below the line.
|
|
60
56
|
// One table is charted at once; several open a picker named by their columns.
|
|
@@ -213,8 +209,8 @@ export class ChartView {
|
|
|
213
209
|
}
|
|
214
210
|
}
|
|
215
211
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
212
|
+
// The hook the chart view part registers for its editor view: every change can
|
|
213
|
+
// have moved a chart's source table.
|
|
214
|
+
export function refreshCharts(view) {
|
|
215
|
+
for (const chart of views.get(view) ?? []) chart.refresh();
|
|
220
216
|
}
|
package/src/page/checklist.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { state, view as pmView } from "../vendor/prosemirror.vendor.js";
|
|
2
2
|
import { schema } from "./schema.js";
|
|
3
3
|
import { asStep } from "./blocksel.js";
|
|
4
|
-
import { el, cardFace } from "./board-
|
|
4
|
+
import { el, cardFace } from "./board-face.js";
|
|
5
5
|
|
|
6
6
|
const { Plugin } = state;
|
|
7
7
|
const { Decoration, DecorationSet } = pmView;
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* checkpoint-proof.js — the proof one accepted save produces, and the conditional read
|
|
3
|
+
* that answers for bytes this tab did not write.
|
|
4
|
+
*
|
|
5
|
+
* Step 2 built the proof itself: the recorded revisions, the accepted page state, the
|
|
6
|
+
* final article and the receipt, checked by `validatePageCheckpoint` before a store
|
|
7
|
+
* retires anything. What it left here is the producer's half. A proof is made at the
|
|
8
|
+
* accepted-save event, from the page state pinned to THAT capture, never from the live
|
|
9
|
+
* store: an edit made after the capture is in the store and is not in the bytes the host
|
|
10
|
+
* took, and reading the store when the response lands would cover it and clear it.
|
|
11
|
+
*
|
|
12
|
+
* Checklist item 2 lives here too. A state that no longer projects to the article that
|
|
13
|
+
* went to disk proves nothing about those bytes, so an authored transform that changed
|
|
14
|
+
* meaning, a page rebuilt from another article or a stale tag all produce no proof, and
|
|
15
|
+
* the records stay.
|
|
16
|
+
*
|
|
17
|
+
* The second half is the conditional page-state client: the host's own copy of the page
|
|
18
|
+
* state, asked for by the accepted ETag, for the cases a local capture cannot answer —
|
|
19
|
+
* the server's checkpoint, another writer's, and a lost reply that only a verified
|
|
20
|
+
* refetch can reconcile. Nothing here reads a cookie, a room sequence or a version
|
|
21
|
+
* string: a 412, a 428 or a 404 keeps every record, and so does any state whose
|
|
22
|
+
* identity, generation, digest or projection disagrees with what was accepted.
|
|
23
|
+
*
|
|
24
|
+
* It loads only when a recovery channel is registered: nothing else imports it.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { saveToken } from "../core/host-attrs.js";
|
|
28
|
+
import { setCapturePageState } from "../core/snapshot.js";
|
|
29
|
+
import { canonicalPageBytes, digestHex, pageProof, validatePageCheckpoint } from "./ybind/recovery-proof.js";
|
|
30
|
+
import { articleOfPageState, pageStateOfClone } from "./save-projection.js";
|
|
31
|
+
|
|
32
|
+
// The capture half. A save capture carries its page state only once something can prove
|
|
33
|
+
// it, so the reader is installed here rather than from the page bundle: a page that
|
|
34
|
+
// journals nothing never pays for it, and a page that does gets the state of the exact
|
|
35
|
+
// clone whose bytes it sends, after every transform.
|
|
36
|
+
setCapturePageState(pageStateOfClone);
|
|
37
|
+
|
|
38
|
+
/** The only page-state resource version this client reads. */
|
|
39
|
+
export const PAGE_STATE_VERSION = 1;
|
|
40
|
+
|
|
41
|
+
export class PageCheckpointError extends Error {
|
|
42
|
+
constructor(code, message) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.name = "PageCheckpointError";
|
|
45
|
+
this.code = code;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const fail = (code, message) => { throw new PageCheckpointError(code, message); };
|
|
50
|
+
const refused = (code, message) => ({ ok: false, code, message });
|
|
51
|
+
const HEX64 = /^[0-9a-f]{64}$/;
|
|
52
|
+
const GENERATION = /^[0-9a-f]{32}$/;
|
|
53
|
+
|
|
54
|
+
const isObject = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
55
|
+
|
|
56
|
+
// The state as this tab holds it, copied: a pinned capture's value shares its `y` map
|
|
57
|
+
// with the store's at the moment of the copy and must not share it after.
|
|
58
|
+
function copyState(state) {
|
|
59
|
+
return { v: state.v, g: state.g, y: { ...(state.y ?? {}) } };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The state the bytes are decoded from, strictly, and its canonical form.
|
|
63
|
+
function stateOfTag(tag) {
|
|
64
|
+
let state;
|
|
65
|
+
try { state = JSON.parse(tag); }
|
|
66
|
+
catch (_) { fail("invalid-state", "The page state is not JSON"); }
|
|
67
|
+
if (!isObject(state) || state.v !== 1) fail("invalid-state", "Expected a version 1 Yjs page value");
|
|
68
|
+
return { state: copyState(state), bytes: canonicalPageBytes(state) };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const idKey = (value) => (value === null || value === undefined ? "" : String(value));
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Ask the host for the page state of one accepted version, conditionally.
|
|
75
|
+
*
|
|
76
|
+
* The ETag is the one the accepted save carried, so the answer is about those exact
|
|
77
|
+
* bytes or about nothing. 412 (another version is stored), 428 (no condition was sent,
|
|
78
|
+
* which no answer of ours can be), 404 (this host keeps no page state) and every other
|
|
79
|
+
* failure are refusals with a code: the caller keeps its records and asks again later.
|
|
80
|
+
*
|
|
81
|
+
* @param {Object} options
|
|
82
|
+
* @param {string} options.href - the resource discovery named in `document.pageState.href`
|
|
83
|
+
* @param {string} options.etag - the ETag the accepted bytes were stored under
|
|
84
|
+
* @param {Function} [options.fetch]
|
|
85
|
+
* @param {AbortSignal} [options.signal]
|
|
86
|
+
* @returns {Promise<{ok: true, version: number, documentId: *, generation: string, etag: string, stateHash: string, tag: string}|{ok: false, code: string, message: string}>}
|
|
87
|
+
*/
|
|
88
|
+
export async function readPageState({ href, etag, fetch: doFetch = globalThis.fetch, signal = null } = {}) {
|
|
89
|
+
if (typeof etag !== "string" || !etag) return refused("missing-etag", "A conditional read needs the accepted ETag");
|
|
90
|
+
if (typeof href !== "string" || !href) return refused("no-page-state", "This host names no page-state resource");
|
|
91
|
+
if (typeof doFetch !== "function") return refused("unavailable", "This tab cannot read the page state");
|
|
92
|
+
const base = globalThis.location?.href ?? "http://localhost/";
|
|
93
|
+
let url;
|
|
94
|
+
try { url = new URL(href, base).href; }
|
|
95
|
+
catch (_) { return refused("no-page-state", "The page-state resource is not a URL"); }
|
|
96
|
+
const token = saveToken();
|
|
97
|
+
let res;
|
|
98
|
+
try {
|
|
99
|
+
res = await doFetch(url, {
|
|
100
|
+
method: "GET",
|
|
101
|
+
credentials: token ? "omit" : "same-origin",
|
|
102
|
+
headers: { "If-Match": etag, "Document-URL": globalThis.location?.href ?? base },
|
|
103
|
+
cache: "no-store",
|
|
104
|
+
redirect: "manual",
|
|
105
|
+
signal,
|
|
106
|
+
});
|
|
107
|
+
} catch (_) {
|
|
108
|
+
return refused("unavailable", "The page-state request did not complete");
|
|
109
|
+
}
|
|
110
|
+
if (!res || typeof res.status !== "number") return refused("unavailable", "The page-state request did not answer");
|
|
111
|
+
if (res.status === 412) return refused("etag-mismatch", "Another version of this document is stored");
|
|
112
|
+
if (res.status === 428) return refused("etag-required", "The host needs a condition this read cannot send");
|
|
113
|
+
if (res.status === 404) return refused("no-page-state", "This host keeps no page state");
|
|
114
|
+
if (!res.ok) return refused("unavailable", `The host answered ${res.status}`);
|
|
115
|
+
|
|
116
|
+
let body;
|
|
117
|
+
try { body = JSON.parse(await res.text()); }
|
|
118
|
+
catch (_) { return refused("invalid-state", "The page-state answer is not JSON"); }
|
|
119
|
+
if (!isObject(body) || body.version !== PAGE_STATE_VERSION) return refused("unsupported-version", "Unknown page-state version");
|
|
120
|
+
if (typeof body.tag !== "string" || !body.tag) return refused("invalid-state", "The page-state answer carries no page tag");
|
|
121
|
+
if (typeof body.stateHash !== "string" || !HEX64.test(body.stateHash)) return refused("invalid-state", "The page-state answer carries no state digest");
|
|
122
|
+
if (typeof body.generation !== "string" || !GENERATION.test(body.generation)) return refused("invalid-state", "The page-state answer carries no generation");
|
|
123
|
+
if (typeof body.etag !== "string" || !body.etag) return refused("invalid-state", "The page-state answer carries no ETag");
|
|
124
|
+
if (!Object.hasOwn(body, "documentId")) return refused("invalid-state", "The page-state answer carries no document identity");
|
|
125
|
+
return {
|
|
126
|
+
ok: true,
|
|
127
|
+
version: body.version,
|
|
128
|
+
documentId: body.documentId,
|
|
129
|
+
generation: body.generation,
|
|
130
|
+
etag: body.etag,
|
|
131
|
+
stateHash: body.stateHash,
|
|
132
|
+
tag: body.tag,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Whether a conditional read answers for the version this tab accepted.
|
|
138
|
+
*
|
|
139
|
+
* Identity, generation, ETag, digest and projection, in that order: a state for another
|
|
140
|
+
* document, another generation, another version of the same document or another article
|
|
141
|
+
* is refused rather than used, and the receipt names the pair the host persisted.
|
|
142
|
+
*
|
|
143
|
+
* @param {Object} read - an answer from readPageState()
|
|
144
|
+
* @param {Object} options
|
|
145
|
+
* @param {{authority: Object, lineage: Object}} options.accepted
|
|
146
|
+
* @param {string} options.article - the final outgoing canonical article
|
|
147
|
+
* @param {string} options.etag - the ETag the accepted save carried
|
|
148
|
+
* @returns {Promise<{ok: true, state: Object, receipt: {file: string, pageState: string}}|{ok: false, code: string, message: string}>}
|
|
149
|
+
*/
|
|
150
|
+
export async function verifyPageState(read, { accepted, article, etag } = {}) {
|
|
151
|
+
if (!read || read.ok !== true) return refused(read?.code ?? "unavailable", read?.message ?? "The page state was not read");
|
|
152
|
+
if (idKey(read.documentId) !== idKey(accepted?.authority?.documentId)) return refused("foreign-document", "The page state belongs to another document");
|
|
153
|
+
if (typeof accepted?.lineage?.generation !== "string" || read.generation !== accepted.lineage.generation) return refused("foreign-generation", "The page state belongs to another generation");
|
|
154
|
+
if (read.etag !== etag) return refused("etag-mismatch", "The page state belongs to another version of this document");
|
|
155
|
+
let stateBytes, state;
|
|
156
|
+
try { ({ state, bytes: stateBytes } = stateOfTag(read.tag)); }
|
|
157
|
+
catch (error) { return refused(error?.code ?? "invalid-state", error?.message ?? "The page state does not decode"); }
|
|
158
|
+
if (await digestHex(stateBytes) !== read.stateHash) return refused("state-digest-mismatch", "The page state is not the one the host hashed");
|
|
159
|
+
if (typeof article !== "string") return refused("missing-article", "A proof names the final outgoing article");
|
|
160
|
+
let projected;
|
|
161
|
+
try { projected = articleOfPageState(state); }
|
|
162
|
+
catch (error) { return refused("invalid-state", error?.message ?? "The page state does not project"); }
|
|
163
|
+
if (projected !== article) return refused("article-mismatch", "The page state does not project to the accepted article");
|
|
164
|
+
return { ok: true, state, receipt: { file: read.etag, pageState: read.stateHash } };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// A proof is only worth the records the validator confirms, over the revisions the
|
|
168
|
+
// caller observed: a proof that covers nothing keeps everything.
|
|
169
|
+
async function covered(proof, records) {
|
|
170
|
+
const observed = (records || []).map(({ id, revision }) => ({ id, revision }));
|
|
171
|
+
try {
|
|
172
|
+
return { ok: true, proof, covered: await validatePageCheckpoint(proof, observed) };
|
|
173
|
+
} catch (error) {
|
|
174
|
+
return refused(error?.code ?? "invalid-proof", error?.message ?? "The proof does not validate");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The proof of one accepted save, from the page state pinned to its capture.
|
|
180
|
+
*
|
|
181
|
+
* The receipt is the accepted ETag and the digest of the state the proof is made over,
|
|
182
|
+
* which is this tab's own account that this capture was written and hashed. Nothing
|
|
183
|
+
* about a later edit, a store value or an acknowledgement reaches this function.
|
|
184
|
+
*
|
|
185
|
+
* @param {Object} options
|
|
186
|
+
* @param {string} options.capture - the capture the save belongs to
|
|
187
|
+
* @param {Array<Object>} options.records - {id, revision, update, context} the caller observed
|
|
188
|
+
* @param {{authority: Object, lineage: Object}} options.accepted
|
|
189
|
+
* @param {string} options.article - the final outgoing canonical article
|
|
190
|
+
* @param {Object} options.state - the page state pinned to that capture
|
|
191
|
+
* @param {string} options.etag - the ETag the accepted save carried
|
|
192
|
+
* @returns {Promise<{ok: true, proof: Object, covered: Array}|{ok: false, code: string, message: string}>}
|
|
193
|
+
*/
|
|
194
|
+
export async function captureProof({ capture, records = [], accepted, article, state = null, etag = null } = {}) {
|
|
195
|
+
if (!state) return refused("no-capture-state", "No page state was pinned to this capture");
|
|
196
|
+
if (typeof etag !== "string" || !etag) return refused("missing-etag", "An accepted save with no ETag proves no stored pair");
|
|
197
|
+
if (typeof article !== "string") return refused("missing-article", "The accepted bytes hold no article");
|
|
198
|
+
let projected, stateBytes;
|
|
199
|
+
try {
|
|
200
|
+
projected = articleOfPageState(state);
|
|
201
|
+
stateBytes = canonicalPageBytes(state);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
return refused(error?.code ?? "invalid-state", error?.message ?? "The pinned page state does not decode");
|
|
204
|
+
}
|
|
205
|
+
if (projected !== article) return refused("article-mismatch", "The pinned page state does not project to the accepted article");
|
|
206
|
+
let proof;
|
|
207
|
+
try {
|
|
208
|
+
proof = await pageProof({
|
|
209
|
+
capture,
|
|
210
|
+
records,
|
|
211
|
+
accepted,
|
|
212
|
+
article,
|
|
213
|
+
state,
|
|
214
|
+
receipt: { file: etag, pageState: await digestHex(stateBytes) },
|
|
215
|
+
});
|
|
216
|
+
} catch (error) {
|
|
217
|
+
return refused(error?.code ?? "invalid-proof", error?.message ?? "The proof could not be built");
|
|
218
|
+
}
|
|
219
|
+
return covered(proof, records);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The proof of an accepted save, rebuilt from the page state the host persisted.
|
|
224
|
+
*
|
|
225
|
+
* The answer for the server's checkpoint, another writer's, and a save whose reply was
|
|
226
|
+
* lost: identity, generation and digest are checked against that exact ETag first, and
|
|
227
|
+
* only then is coverage asked. A host that cannot answer leaves the journal alone.
|
|
228
|
+
*
|
|
229
|
+
* @param {Object} options
|
|
230
|
+
* @param {Object} options.read - an answer from readPageState()
|
|
231
|
+
* @returns {Promise<{ok: true, proof: Object, covered: Array}|{ok: false, code: string, message: string}>}
|
|
232
|
+
*/
|
|
233
|
+
export async function refetchedProof({ read, capture, records = [], accepted, article, etag = null } = {}) {
|
|
234
|
+
const verified = await verifyPageState(read, { accepted, article, etag });
|
|
235
|
+
if (!verified.ok) return verified;
|
|
236
|
+
let proof;
|
|
237
|
+
try {
|
|
238
|
+
proof = await pageProof({ capture, records, accepted, article, state: verified.state, receipt: verified.receipt });
|
|
239
|
+
} catch (error) {
|
|
240
|
+
return refused(error?.code ?? "invalid-proof", error?.message ?? "The proof could not be built");
|
|
241
|
+
}
|
|
242
|
+
return covered(proof, records);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Retire exactly the records one accepted save covers.
|
|
247
|
+
*
|
|
248
|
+
* The pinned capture is tried first, because it needs no request and no host. Only a
|
|
249
|
+
* capture that cannot prove these bytes asks the host, conditionally on the accepted
|
|
250
|
+
* ETag. Whichever answers, the channel is handed the caller's observed list and the
|
|
251
|
+
* proof together: it narrows the list to what the proof covers and never widens it, and
|
|
252
|
+
* a refusal here retires nothing.
|
|
253
|
+
*
|
|
254
|
+
* @param {Object} options
|
|
255
|
+
* @param {Object} options.channel - the `page.yjs` channel api
|
|
256
|
+
* @param {Array<Object>} options.records - {id, revision, update, context} the caller observed
|
|
257
|
+
* @param {string} options.capture
|
|
258
|
+
* @param {{authority: Object, lineage: Object}} options.accepted
|
|
259
|
+
* @param {string} options.article
|
|
260
|
+
* @param {Object} [options.state] - the page state pinned to the capture
|
|
261
|
+
* @param {string} options.etag
|
|
262
|
+
* @param {?string} [options.href] - the page-state resource, or null
|
|
263
|
+
* @param {Function} [options.read]
|
|
264
|
+
* @param {Function} [options.fetch]
|
|
265
|
+
* @returns {Promise<{ok: boolean, retired: Array, proof: ?Object, code?: string}>}
|
|
266
|
+
*/
|
|
267
|
+
export async function checkpointAccepted({
|
|
268
|
+
channel,
|
|
269
|
+
records = [],
|
|
270
|
+
capture,
|
|
271
|
+
accepted,
|
|
272
|
+
article,
|
|
273
|
+
state = null,
|
|
274
|
+
etag = null,
|
|
275
|
+
href = null,
|
|
276
|
+
read = readPageState,
|
|
277
|
+
fetch: doFetch,
|
|
278
|
+
} = {}) {
|
|
279
|
+
if (!channel || typeof channel.checkpoint !== "function") return { ok: false, code: "no-channel", retired: [], proof: null };
|
|
280
|
+
let proven = await captureProof({ capture, records, accepted, article, state, etag });
|
|
281
|
+
if (!proven.ok) {
|
|
282
|
+
const answer = await read({ href, etag, fetch: doFetch });
|
|
283
|
+
proven = await refetchedProof({ read: answer, capture, records, accepted, article, etag });
|
|
284
|
+
}
|
|
285
|
+
if (!proven.ok) return { ok: false, code: proven.code, retired: [], proof: null };
|
|
286
|
+
const answer = await channel.checkpoint({ records: proven.covered, proof: proven.proof });
|
|
287
|
+
if (!answer || answer.ok !== true) return { ok: false, code: answer?.code ?? "checkpoint-failed", retired: [], proof: proven.proof };
|
|
288
|
+
return { ok: true, retired: answer.retired ?? [], proof: proven.proof };
|
|
289
|
+
}
|