@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
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* store.js — the IndexedDB half of the recovery service.
|
|
3
|
+
*
|
|
4
|
+
* One database per browser (`clay-recovery`), three stores:
|
|
5
|
+
*
|
|
6
|
+
* records every recovery copy, text keys and immutable ids alike
|
|
7
|
+
* watermarks per text key, the revision an accepted save has proved it covers
|
|
8
|
+
* meta the running byte totals the budgets are checked against
|
|
9
|
+
*
|
|
10
|
+
* Loaded only when an adapter registers (core/recovery.js imports it dynamically),
|
|
11
|
+
* so a page that journals nothing never opens the database and never downloads this
|
|
12
|
+
* file. It is also the reason a test-only IndexedDB stays out of the runtime: no
|
|
13
|
+
* module here imports one.
|
|
14
|
+
*
|
|
15
|
+
* Durable means one completed IndexedDB transaction. It does not promise that the
|
|
16
|
+
* browser will keep the bytes: storage can be evicted, and a suspended tab or a
|
|
17
|
+
* power cut can lose the last write. Every failure is reported as a code rather
|
|
18
|
+
* than thrown, because the caller has to keep the in-memory text and say so.
|
|
19
|
+
*
|
|
20
|
+
* Composite keys are arrays, never delimited strings: `[scope, channel, key,
|
|
21
|
+
* writerId]` is a key no path with a `:` in it can collide with.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export const DB_NAME = "clay-recovery";
|
|
25
|
+
export const DB_VERSION = 1;
|
|
26
|
+
export const RECORDS = "records";
|
|
27
|
+
export const WATERMARKS = "watermarks";
|
|
28
|
+
export const META = "meta";
|
|
29
|
+
|
|
30
|
+
export const SCOPE_BUDGET_BYTES = 32 * 1024 * 1024;
|
|
31
|
+
export const DB_BUDGET_BYTES = 64 * 1024 * 1024;
|
|
32
|
+
|
|
33
|
+
const OPEN_TIMEOUT_MS = 5000;
|
|
34
|
+
const BY_SCOPE = "byScope";
|
|
35
|
+
const TOTAL_USAGE = ["usage", "*"];
|
|
36
|
+
|
|
37
|
+
const encoder = typeof TextEncoder === "function" ? new TextEncoder() : null;
|
|
38
|
+
|
|
39
|
+
function utf8Length(value) {
|
|
40
|
+
return encoder ? encoder.encode(value).length : value.length * 2;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function valueBytes(value) {
|
|
44
|
+
if (typeof value === "string") return utf8Length(value);
|
|
45
|
+
if (value && typeof value.byteLength === "number" && (value instanceof Uint8Array || value instanceof ArrayBuffer)) return value.byteLength;
|
|
46
|
+
if (Array.isArray(value)) return value.reduce((sum, item) => sum + valueBytes(item), 0);
|
|
47
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return 8;
|
|
48
|
+
if (!value || typeof value !== "object") return 0;
|
|
49
|
+
let bytes = 0;
|
|
50
|
+
for (const [field, item] of Object.entries(value)) bytes += utf8Length(field) + valueBytes(item);
|
|
51
|
+
return bytes;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* What one record costs against the budgets: its strings measured as UTF-8, binary
|
|
56
|
+
* payloads by their byte length, everything else by its serialized metadata.
|
|
57
|
+
*
|
|
58
|
+
* @param {Object} record
|
|
59
|
+
* @returns {number}
|
|
60
|
+
*/
|
|
61
|
+
export function recordBytes(record) {
|
|
62
|
+
if (!record || typeof record !== "object") return 0;
|
|
63
|
+
let bytes = 0;
|
|
64
|
+
for (const [field, value] of Object.entries(record)) bytes += utf8Length(field) + valueBytes(value);
|
|
65
|
+
return bytes;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The failure code a thrown IndexedDB error means to a caller.
|
|
70
|
+
*
|
|
71
|
+
* Only one of these is about this device being full (`quota`); the rest are about
|
|
72
|
+
* the database being unreachable, refused, or mid-upgrade somewhere else. None of
|
|
73
|
+
* them is a save result, and none of them is durable.
|
|
74
|
+
*
|
|
75
|
+
* @param {*} error
|
|
76
|
+
* @returns {string}
|
|
77
|
+
*/
|
|
78
|
+
export function codeFor(error) {
|
|
79
|
+
const name = error?.name || "";
|
|
80
|
+
if (name === "QuotaExceededError" || name === "NS_ERROR_DOM_QUOTA_REACHED") return "quota";
|
|
81
|
+
if (name === "VersionError") return "unknown-schema";
|
|
82
|
+
if (name === "DataCloneError") return "uncloneable";
|
|
83
|
+
if (name === "AbortError") return "aborted";
|
|
84
|
+
if (name === "TransactionInactiveError" || name === "InvalidStateError") return "aborted";
|
|
85
|
+
if (name === "SecurityError" || name === "UnknownError" || name === "NotSupportedError") return "unavailable";
|
|
86
|
+
return "error";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Are these two plain values the same?
|
|
91
|
+
*
|
|
92
|
+
* Contexts are compared inside a transaction, never by reference: the caller builds
|
|
93
|
+
* a fresh object for every operation, and the stored one came back through a
|
|
94
|
+
* structured clone. Binary parts are compared byte for byte.
|
|
95
|
+
*
|
|
96
|
+
* @param {*} left
|
|
97
|
+
* @param {*} right
|
|
98
|
+
* @returns {boolean}
|
|
99
|
+
*/
|
|
100
|
+
function sameValue(left, right) {
|
|
101
|
+
if (left === right) return true;
|
|
102
|
+
if (left instanceof ArrayBuffer) left = new Uint8Array(left);
|
|
103
|
+
if (right instanceof ArrayBuffer) right = new Uint8Array(right);
|
|
104
|
+
if (left instanceof Uint8Array || right instanceof Uint8Array) {
|
|
105
|
+
if (!(left instanceof Uint8Array) || !(right instanceof Uint8Array)) return false;
|
|
106
|
+
if (left.length !== right.length) return false;
|
|
107
|
+
for (let at = 0; at < left.length; at++) if (left[at] !== right[at]) return false;
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if (!left || !right || typeof left !== "object" || typeof right !== "object") return false;
|
|
111
|
+
if (Array.isArray(left) !== Array.isArray(right)) return false;
|
|
112
|
+
const keys = Object.keys(left);
|
|
113
|
+
if (keys.length !== Object.keys(right).length) return false;
|
|
114
|
+
return keys.every((field) => Object.prototype.hasOwnProperty.call(right, field) && sameValue(left[field], right[field]));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Has this writer already retired (checkpointed or compacted) this revision?
|
|
119
|
+
*
|
|
120
|
+
* @param {?Array<Array<number>>} ranges
|
|
121
|
+
* @param {number} revision
|
|
122
|
+
* @returns {boolean}
|
|
123
|
+
*/
|
|
124
|
+
function isRetired(ranges, revision) {
|
|
125
|
+
if (!Array.isArray(ranges) || typeof revision !== "number") return false;
|
|
126
|
+
for (const range of ranges) {
|
|
127
|
+
if (Array.isArray(range) && revision >= range[0] && revision <= range[1]) return true;
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Add one retired revision to a writer's ranges.
|
|
134
|
+
*
|
|
135
|
+
* A page that journals for an hour retires thousands of revisions, and the entry has
|
|
136
|
+
* to stay small for as long as the writer lives, so neighbouring revisions become a
|
|
137
|
+
* range instead of a list. Nothing is ever dropped by age: the writer is still alive
|
|
138
|
+
* and a queued write for any of these revisions must still be refused.
|
|
139
|
+
*
|
|
140
|
+
* @param {?Array<Array<number>>} ranges
|
|
141
|
+
* @param {number} revision
|
|
142
|
+
* @returns {Array<Array<number>>}
|
|
143
|
+
*/
|
|
144
|
+
function retireRange(ranges, revision) {
|
|
145
|
+
const sorted = (Array.isArray(ranges) ? ranges : []).map((range) => [range[0], range[1]]);
|
|
146
|
+
sorted.push([revision, revision]);
|
|
147
|
+
sorted.sort((a, b) => a[0] - b[0]);
|
|
148
|
+
const merged = [];
|
|
149
|
+
for (const range of sorted) {
|
|
150
|
+
const last = merged[merged.length - 1];
|
|
151
|
+
if (last && range[0] <= last[1] + 1) last[1] = Math.max(last[1], range[1]);
|
|
152
|
+
else merged.push([range[0], range[1]]);
|
|
153
|
+
}
|
|
154
|
+
return merged;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function slotKey(slot) {
|
|
158
|
+
return JSON.stringify(slot);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function request(req) {
|
|
162
|
+
return new Promise((resolve, reject) => {
|
|
163
|
+
req.onsuccess = () => resolve(req.result);
|
|
164
|
+
req.onerror = () => reject(req.error);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function cursorAll(source, key) {
|
|
169
|
+
return new Promise((resolve, reject) => {
|
|
170
|
+
const req = key === undefined ? source.openCursor() : source.openCursor(key);
|
|
171
|
+
const out = [];
|
|
172
|
+
req.onerror = () => reject(req.error);
|
|
173
|
+
req.onsuccess = () => {
|
|
174
|
+
const cursor = req.result;
|
|
175
|
+
if (!cursor) {
|
|
176
|
+
resolve(out);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
out.push(cursor.value);
|
|
180
|
+
cursor.continue();
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function upgrade(db) {
|
|
186
|
+
if (!db.objectStoreNames.contains(RECORDS)) {
|
|
187
|
+
// Keyed by the full text key, `[scope, channel, key, writerId]`, so two documents
|
|
188
|
+
// or two modes on one origin cannot collide on a local id. `slots` holds it
|
|
189
|
+
// rather than `key`, which is the adapter's own name for what it is journaling.
|
|
190
|
+
// Immutable records carry a unique id and use it as their slot.
|
|
191
|
+
const records = db.createObjectStore(RECORDS, { keyPath: "slot" });
|
|
192
|
+
records.createIndex(BY_SCOPE, "scope");
|
|
193
|
+
}
|
|
194
|
+
if (!db.objectStoreNames.contains(WATERMARKS)) {
|
|
195
|
+
db.createObjectStore(WATERMARKS, { keyPath: "key" });
|
|
196
|
+
}
|
|
197
|
+
if (!db.objectStoreNames.contains(META)) {
|
|
198
|
+
db.createObjectStore(META, { keyPath: "key" });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Open (and, the first time, create) the recovery database.
|
|
204
|
+
*
|
|
205
|
+
* A version this build does not know is left alone: opening an older version than
|
|
206
|
+
* the one on disk throws VersionError, which is reported as `unknown-schema` so the
|
|
207
|
+
* caller can offer the stored bytes for download without migrating or dropping
|
|
208
|
+
* them. A blocked upgrade is reported as `blocked` rather than waited on forever.
|
|
209
|
+
*
|
|
210
|
+
* @param {?IDBFactory} factory
|
|
211
|
+
* @returns {Promise<{ok: boolean, code?: string, db?: IDBDatabase}>}
|
|
212
|
+
*/
|
|
213
|
+
export function openDatabase(factory = globalThis.indexedDB, { name = DB_NAME, version = DB_VERSION } = {}) {
|
|
214
|
+
return new Promise((resolve) => {
|
|
215
|
+
if (!factory || typeof factory.open !== "function") {
|
|
216
|
+
resolve({ ok: false, code: "unavailable" });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
let req;
|
|
220
|
+
try {
|
|
221
|
+
req = factory.open(name, version);
|
|
222
|
+
} catch (err) {
|
|
223
|
+
resolve({ ok: false, code: codeFor(err) });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
let settled = false;
|
|
227
|
+
const finish = (answer) => {
|
|
228
|
+
if (settled) return;
|
|
229
|
+
settled = true;
|
|
230
|
+
clearTimeout(timer);
|
|
231
|
+
resolve(answer);
|
|
232
|
+
};
|
|
233
|
+
const timer = setTimeout(() => finish({ ok: false, code: "blocked" }), OPEN_TIMEOUT_MS);
|
|
234
|
+
req.onupgradeneeded = () => upgrade(req.result);
|
|
235
|
+
req.onsuccess = () => {
|
|
236
|
+
const db = req.result;
|
|
237
|
+
// Another tab opening a newer version needs this connection to stand aside,
|
|
238
|
+
// or its upgrade sits blocked for as long as this page stays open.
|
|
239
|
+
db.onversionchange = () => {
|
|
240
|
+
try {
|
|
241
|
+
db.close();
|
|
242
|
+
} catch (_) {}
|
|
243
|
+
};
|
|
244
|
+
finish({ ok: true, db });
|
|
245
|
+
};
|
|
246
|
+
req.onerror = () => finish({ ok: false, code: codeFor(req.error) });
|
|
247
|
+
req.onblocked = () => finish({ ok: false, code: "blocked" });
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The durable half of one document's recovery store.
|
|
253
|
+
*
|
|
254
|
+
* @param {Object} options
|
|
255
|
+
* @param {string[]} options.scope - `[origin, identityKind, identity, mode]`
|
|
256
|
+
* @param {?IDBFactory} [options.indexedDB]
|
|
257
|
+
* @param {Function} [options.now]
|
|
258
|
+
* @param {number} [options.maxScopeBytes]
|
|
259
|
+
* @param {number} [options.maxDbBytes]
|
|
260
|
+
* @returns {Object}
|
|
261
|
+
*/
|
|
262
|
+
export function createStore({
|
|
263
|
+
scope,
|
|
264
|
+
indexedDB: factory = globalThis.indexedDB,
|
|
265
|
+
now = () => Date.now(),
|
|
266
|
+
maxScopeBytes = SCOPE_BUDGET_BYTES,
|
|
267
|
+
maxDbBytes = DB_BUDGET_BYTES,
|
|
268
|
+
} = {}) {
|
|
269
|
+
if (!Array.isArray(scope) || scope.length !== 4) throw new TypeError("createStore needs a four-part scope");
|
|
270
|
+
|
|
271
|
+
let opening = null;
|
|
272
|
+
let db = null;
|
|
273
|
+
let openAnswer = null;
|
|
274
|
+
const usage = { scopeBytes: 0, dbBytes: null, maxScopeBytes, maxDbBytes };
|
|
275
|
+
|
|
276
|
+
const status = () => ({
|
|
277
|
+
ok: openAnswer?.ok !== false,
|
|
278
|
+
code: openAnswer?.code ?? null,
|
|
279
|
+
durable: openAnswer?.ok === true,
|
|
280
|
+
usage: { ...usage },
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
async function open() {
|
|
284
|
+
if (!opening) {
|
|
285
|
+
opening = openDatabase(factory).then((answer) => {
|
|
286
|
+
openAnswer = answer.ok ? { ok: true } : { ok: false, code: answer.code };
|
|
287
|
+
if (answer.ok) db = answer.db;
|
|
288
|
+
return answer;
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
return opening;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const scopeUsageKey = ["usage", ...scope];
|
|
295
|
+
|
|
296
|
+
async function readUsageTotals(meta, records) {
|
|
297
|
+
const [scopeEntry, totalEntry] = await Promise.all([
|
|
298
|
+
request(meta.get(scopeUsageKey)),
|
|
299
|
+
request(meta.get(TOTAL_USAGE)),
|
|
300
|
+
]);
|
|
301
|
+
if (scopeEntry && totalEntry) return { scopeBytes: scopeEntry.value, dbBytes: totalEntry.value };
|
|
302
|
+
const all = await cursorAll(records);
|
|
303
|
+
const recomputed = { scopeBytes: 0, dbBytes: 0 };
|
|
304
|
+
for (const record of all) {
|
|
305
|
+
const bytes = record.bytes ?? recordBytes(record);
|
|
306
|
+
recomputed.dbBytes += bytes;
|
|
307
|
+
if (sameScope(record.scope)) recomputed.scopeBytes += bytes;
|
|
308
|
+
}
|
|
309
|
+
return recomputed;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function writeUsageTotals(meta, totals) {
|
|
313
|
+
usage.scopeBytes = totals.scopeBytes;
|
|
314
|
+
usage.dbBytes = totals.dbBytes;
|
|
315
|
+
await Promise.all([
|
|
316
|
+
request(meta.put({ key: scopeUsageKey, value: totals.scopeBytes })),
|
|
317
|
+
request(meta.put({ key: TOTAL_USAGE, value: totals.dbBytes })),
|
|
318
|
+
]);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function sameScope(other) {
|
|
322
|
+
return Array.isArray(other) && other.length === 4 && other.every((part, at) => part === scope[at]);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// The durable slot for one record id. Text ids are local to their channel; the
|
|
326
|
+
// scope goes in front of them here and nowhere else.
|
|
327
|
+
function keyOf(id) {
|
|
328
|
+
return Array.isArray(id) ? [...scope, ...id] : id;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function runWrite(body) {
|
|
332
|
+
return new Promise((resolve) => {
|
|
333
|
+
if (!db) {
|
|
334
|
+
resolve({ ok: false, code: openAnswer?.code || "unavailable" });
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
let tx;
|
|
338
|
+
try {
|
|
339
|
+
tx = db.transaction([RECORDS, WATERMARKS, META], "readwrite");
|
|
340
|
+
} catch (err) {
|
|
341
|
+
resolve({ ok: false, code: codeFor(err) });
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const wasUsage = { ...usage };
|
|
345
|
+
let settled = false;
|
|
346
|
+
let failure = null;
|
|
347
|
+
let result = null;
|
|
348
|
+
const finish = (answer) => {
|
|
349
|
+
if (settled) return;
|
|
350
|
+
settled = true;
|
|
351
|
+
resolve(answer);
|
|
352
|
+
};
|
|
353
|
+
const abort = (code) => {
|
|
354
|
+
if (!failure) failure = code;
|
|
355
|
+
try {
|
|
356
|
+
tx.abort();
|
|
357
|
+
} catch (_) {}
|
|
358
|
+
};
|
|
359
|
+
// A transaction that did not commit rolled every byte back, so the in-memory
|
|
360
|
+
// totals have to roll back with it: the next budget check reads them.
|
|
361
|
+
tx.oncomplete = () => {
|
|
362
|
+
if (failure) Object.assign(usage, wasUsage);
|
|
363
|
+
finish(failure ? { ok: false, code: failure } : { ok: true, result });
|
|
364
|
+
};
|
|
365
|
+
tx.onerror = () => {
|
|
366
|
+
if (!failure) failure = codeFor(tx.error);
|
|
367
|
+
};
|
|
368
|
+
tx.onabort = () => {
|
|
369
|
+
Object.assign(usage, wasUsage);
|
|
370
|
+
finish({ ok: false, code: failure || codeFor(tx.error) || "aborted" });
|
|
371
|
+
};
|
|
372
|
+
const ctx = {
|
|
373
|
+
tx,
|
|
374
|
+
records: tx.objectStore(RECORDS),
|
|
375
|
+
watermarks: tx.objectStore(WATERMARKS),
|
|
376
|
+
meta: tx.objectStore(META),
|
|
377
|
+
abort,
|
|
378
|
+
};
|
|
379
|
+
Promise.resolve()
|
|
380
|
+
.then(() => body(ctx))
|
|
381
|
+
.then(
|
|
382
|
+
(value) => {
|
|
383
|
+
result = value;
|
|
384
|
+
},
|
|
385
|
+
(err) => abort(codeFor(err))
|
|
386
|
+
);
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function writeRecord(ctx, record) {
|
|
391
|
+
const { records, watermarks, meta } = ctx;
|
|
392
|
+
const existing = await request(records.get(record.slot));
|
|
393
|
+
if (existing && existing.revision > record.revision) return { ok: true, skipped: true, id: record.id, revision: record.revision };
|
|
394
|
+
if (record.kind === "text") {
|
|
395
|
+
const mark = await request(watermarks.get(record.slot));
|
|
396
|
+
if (mark && mark.ackRevision >= record.revision) return { ok: true, skipped: true, id: record.id, revision: record.revision };
|
|
397
|
+
}
|
|
398
|
+
const totals = await readUsageTotals(meta, records);
|
|
399
|
+
const delta = record.bytes - (existing?.bytes ?? 0);
|
|
400
|
+
if (totals.scopeBytes + delta > maxScopeBytes || totals.dbBytes + delta > maxDbBytes) {
|
|
401
|
+
return { ok: false, code: "over-budget" };
|
|
402
|
+
}
|
|
403
|
+
if (existing?.created) record.created = existing.created;
|
|
404
|
+
await request(records.put(record));
|
|
405
|
+
await writeUsageTotals(meta, { scopeBytes: totals.scopeBytes + delta, dbBytes: totals.dbBytes + delta });
|
|
406
|
+
return { ok: true, id: record.id, revision: record.revision };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
scope,
|
|
411
|
+
|
|
412
|
+
open,
|
|
413
|
+
|
|
414
|
+
status,
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Write one recovery record.
|
|
418
|
+
*
|
|
419
|
+
* A revision an accepted save has already proved it covers, or one older than
|
|
420
|
+
* the copy already on disk, is skipped rather than written: a queued write that
|
|
421
|
+
* arrives late cannot resurrect writing that has been saved or replaced. An
|
|
422
|
+
* over-budget write is refused with the older copy left exactly as it was.
|
|
423
|
+
*
|
|
424
|
+
* @param {Object} record
|
|
425
|
+
* @returns {Promise<{ok: boolean, code?: string, id?: *, revision?: number, skipped?: boolean}>}
|
|
426
|
+
*/
|
|
427
|
+
async put(record) {
|
|
428
|
+
const opened = await open();
|
|
429
|
+
if (!opened.ok) return { ok: false, code: opened.code };
|
|
430
|
+
const prepared = { ...record, scope: [...scope], slot: keyOf(record.id) };
|
|
431
|
+
prepared.bytes = recordBytes(prepared);
|
|
432
|
+
const outcome = await runWrite((ctx) => writeRecord(ctx, prepared));
|
|
433
|
+
if (!outcome.ok) return { ok: false, code: outcome.code };
|
|
434
|
+
return outcome.result;
|
|
435
|
+
},
|
|
436
|
+
|
|
437
|
+
async get(id) {
|
|
438
|
+
const opened = await open();
|
|
439
|
+
if (!opened.ok) return { ok: false, code: opened.code };
|
|
440
|
+
let tx;
|
|
441
|
+
try {
|
|
442
|
+
tx = db.transaction([RECORDS], "readonly");
|
|
443
|
+
} catch (err) {
|
|
444
|
+
return { ok: false, code: codeFor(err) };
|
|
445
|
+
}
|
|
446
|
+
const record = await request(tx.objectStore(RECORDS).get(keyOf(id)));
|
|
447
|
+
if (!record || !sameScope(record.scope)) return { ok: false, code: "missing" };
|
|
448
|
+
return { ok: true, record };
|
|
449
|
+
},
|
|
450
|
+
|
|
451
|
+
async list() {
|
|
452
|
+
const opened = await open();
|
|
453
|
+
if (!opened.ok) return { ok: false, code: opened.code, records: [], status: status() };
|
|
454
|
+
let tx;
|
|
455
|
+
try {
|
|
456
|
+
tx = db.transaction([RECORDS, META], "readonly");
|
|
457
|
+
} catch (err) {
|
|
458
|
+
return { ok: false, code: codeFor(err), records: [], status: status() };
|
|
459
|
+
}
|
|
460
|
+
const records = tx.objectStore(RECORDS);
|
|
461
|
+
const meta = tx.objectStore(META);
|
|
462
|
+
const [all, scopeEntry, totalEntry] = await Promise.all([
|
|
463
|
+
cursorAll(records.index(BY_SCOPE), [...scope]),
|
|
464
|
+
request(meta.get(scopeUsageKey)),
|
|
465
|
+
request(meta.get(TOTAL_USAGE)),
|
|
466
|
+
]);
|
|
467
|
+
if (scopeEntry) usage.scopeBytes = scopeEntry.value;
|
|
468
|
+
else usage.scopeBytes = all.reduce((sum, record) => sum + (record.bytes ?? 0), 0);
|
|
469
|
+
usage.dbBytes = totalEntry ? totalEntry.value : null;
|
|
470
|
+
return { ok: true, records: all, status: status() };
|
|
471
|
+
},
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Compare-and-delete one record.
|
|
475
|
+
*
|
|
476
|
+
* The revision is what makes this safe: a review panel that was drawn over
|
|
477
|
+
* revision 3 must not delete revision 5, which arrived while the person was
|
|
478
|
+
* reading.
|
|
479
|
+
*
|
|
480
|
+
* @param {*} id
|
|
481
|
+
* @param {{revision?: number}} [options]
|
|
482
|
+
* @returns {Promise<{ok: boolean, code?: string, id?: *}>}
|
|
483
|
+
*/
|
|
484
|
+
async remove(id, { revision } = {}) {
|
|
485
|
+
const opened = await open();
|
|
486
|
+
if (!opened.ok) return { ok: false, code: opened.code };
|
|
487
|
+
const outcome = await runWrite(async (ctx) => {
|
|
488
|
+
const slot = keyOf(id);
|
|
489
|
+
const existing = await request(ctx.records.get(slot));
|
|
490
|
+
if (!existing || !sameScope(existing.scope)) return { ok: false, code: "missing" };
|
|
491
|
+
if (revision !== undefined && existing.revision !== revision) return { ok: false, code: "stale" };
|
|
492
|
+
const totals = await readUsageTotals(ctx.meta, ctx.records);
|
|
493
|
+
await request(ctx.records.delete(slot));
|
|
494
|
+
await writeUsageTotals(ctx.meta, {
|
|
495
|
+
scopeBytes: Math.max(0, totals.scopeBytes - existing.bytes),
|
|
496
|
+
dbBytes: Math.max(0, totals.dbBytes - existing.bytes),
|
|
497
|
+
});
|
|
498
|
+
return { ok: true, id };
|
|
499
|
+
});
|
|
500
|
+
return outcome.ok ? outcome.result : { ok: false, code: outcome.code };
|
|
501
|
+
},
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Record that an accepted save covers one text revision, and drop what it covers.
|
|
505
|
+
*
|
|
506
|
+
* The watermark is written before and independently of the record because its job
|
|
507
|
+
* outlives the record: a queued write for a covered revision can still be sitting
|
|
508
|
+
* in memory, and it must be refused after the copy is gone. A newer revision from
|
|
509
|
+
* the same writer, and every other writer, is untouched.
|
|
510
|
+
*
|
|
511
|
+
* @param {*} id - a text key
|
|
512
|
+
* @param {{revision: number, etag?: ?string, captureId?: *, drop?: boolean}} options
|
|
513
|
+
* @returns {Promise<{ok: boolean, code?: string, acknowledged?: number, deleted?: boolean}>}
|
|
514
|
+
*/
|
|
515
|
+
async acknowledge(id, { revision, etag = null, captureId = null, drop = true } = {}) {
|
|
516
|
+
if (typeof revision !== "number" || !Array.isArray(id)) return { ok: false, code: "invalid" };
|
|
517
|
+
const opened = await open();
|
|
518
|
+
if (!opened.ok) return { ok: false, code: opened.code };
|
|
519
|
+
const slot = keyOf(id);
|
|
520
|
+
const outcome = await runWrite(async (ctx) => {
|
|
521
|
+
const mark = await request(ctx.watermarks.get(slot));
|
|
522
|
+
const ackRevision = Math.max(mark?.ackRevision ?? 0, revision);
|
|
523
|
+
await request(ctx.watermarks.put({ key: slot, ackRevision, etag, captureId, updated: now() }));
|
|
524
|
+
const existing = await request(ctx.records.get(slot));
|
|
525
|
+
if (!existing || !sameScope(existing.scope) || !drop || existing.revision > ackRevision) {
|
|
526
|
+
return { ok: true, acknowledged: ackRevision, deleted: false };
|
|
527
|
+
}
|
|
528
|
+
const totals = await readUsageTotals(ctx.meta, ctx.records);
|
|
529
|
+
await request(ctx.records.delete(slot));
|
|
530
|
+
await writeUsageTotals(ctx.meta, {
|
|
531
|
+
scopeBytes: Math.max(0, totals.scopeBytes - existing.bytes),
|
|
532
|
+
dbBytes: Math.max(0, totals.dbBytes - existing.bytes),
|
|
533
|
+
});
|
|
534
|
+
return { ok: true, acknowledged: ackRevision, deleted: true };
|
|
535
|
+
});
|
|
536
|
+
return outcome.ok ? outcome.result : { ok: false, code: outcome.code };
|
|
537
|
+
},
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Append one batch record, creating its writer's capsule on the first append.
|
|
541
|
+
*
|
|
542
|
+
* Everything that could go wrong is decided inside the one transaction: the
|
|
543
|
+
* capsule's channel, version and context, the writer's retired revisions, the
|
|
544
|
+
* record's own revision and the budget. A revision an accepted checkpoint or a
|
|
545
|
+
* compaction already retired is skipped, so a write that was queued while the
|
|
546
|
+
* browser was busy cannot put back what was proved durable and then deleted.
|
|
547
|
+
*
|
|
548
|
+
* @param {Object} record - one `binary-batch` record
|
|
549
|
+
* @param {?Object} capsule - the capsule to create when the writer has none yet
|
|
550
|
+
* @returns {Promise<{ok: boolean, code?: string, id?: *, revision?: number, skipped?: boolean}>}
|
|
551
|
+
*/
|
|
552
|
+
async appendBinary(record, capsule = null) {
|
|
553
|
+
if (!record || record.id === undefined || typeof record.revision !== "number") return { ok: false, code: "invalid" };
|
|
554
|
+
const opened = await open();
|
|
555
|
+
if (!opened.ok) return { ok: false, code: opened.code };
|
|
556
|
+
const prepared = { ...record, kind: "binary-batch", scope: [...scope], slot: keyOf(record.id) };
|
|
557
|
+
prepared.bytes = recordBytes(prepared);
|
|
558
|
+
const capsuleSlot = keyOf(record.capsuleId);
|
|
559
|
+
const outcome = await runWrite(async (ctx) => {
|
|
560
|
+
const head = await request(ctx.records.get(capsuleSlot));
|
|
561
|
+
if (head) {
|
|
562
|
+
if (!sameScope(head.scope) || head.kind !== "binary" || head.channel !== prepared.channel) {
|
|
563
|
+
return { ok: false, code: "channel-mismatch" };
|
|
564
|
+
}
|
|
565
|
+
if (head.version !== prepared.version) return { ok: false, code: "unknown-version" };
|
|
566
|
+
if (!sameValue(head.context, prepared.context)) return { ok: false, code: "context-mismatch" };
|
|
567
|
+
}
|
|
568
|
+
const mark = await request(ctx.watermarks.get(capsuleSlot));
|
|
569
|
+
if (isRetired(mark?.retired, prepared.revision)) {
|
|
570
|
+
return { ok: true, skipped: true, id: prepared.id, revision: prepared.revision };
|
|
571
|
+
}
|
|
572
|
+
const there = await request(ctx.records.get(prepared.slot));
|
|
573
|
+
if (there && sameScope(there.scope) && there.revision >= prepared.revision) {
|
|
574
|
+
return { ok: true, skipped: true, id: prepared.id, revision: prepared.revision };
|
|
575
|
+
}
|
|
576
|
+
let fresh = null;
|
|
577
|
+
if (!head) {
|
|
578
|
+
if (!capsule) return { ok: false, code: "missing" };
|
|
579
|
+
fresh = { ...capsule, kind: "binary", scope: [...scope], slot: capsuleSlot };
|
|
580
|
+
fresh.bytes = recordBytes(fresh);
|
|
581
|
+
}
|
|
582
|
+
const totals = await readUsageTotals(ctx.meta, ctx.records);
|
|
583
|
+
const delta = prepared.bytes + (fresh?.bytes ?? 0) - (there?.bytes ?? 0);
|
|
584
|
+
if (totals.scopeBytes + delta > maxScopeBytes || totals.dbBytes + delta > maxDbBytes) {
|
|
585
|
+
return { ok: false, code: "over-budget" };
|
|
586
|
+
}
|
|
587
|
+
if (fresh) await request(ctx.records.put(fresh));
|
|
588
|
+
await request(ctx.records.put(prepared));
|
|
589
|
+
await writeUsageTotals(ctx.meta, { scopeBytes: totals.scopeBytes + delta, dbBytes: totals.dbBytes + delta });
|
|
590
|
+
return { ok: true, id: prepared.id, revision: prepared.revision };
|
|
591
|
+
});
|
|
592
|
+
return outcome.ok ? outcome.result : { ok: false, code: outcome.code };
|
|
593
|
+
},
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Record that a room accepted this writer's work, and change nothing else.
|
|
597
|
+
*
|
|
598
|
+
* An acknowledgement is metadata: the bytes stay exactly where they are and no
|
|
599
|
+
* revision is retired. Only a checkpoint does that. A record the caller observed
|
|
600
|
+
* at another revision, or one that is already gone, is left alone.
|
|
601
|
+
*
|
|
602
|
+
* @param {{channel: string, version: number, records: Array<{id: *, revision: number}>, authority?: *, epoch?: *, sequence?: *}} options
|
|
603
|
+
* @returns {Promise<{ok: boolean, code?: string, acknowledged?: number}>}
|
|
604
|
+
*/
|
|
605
|
+
async acknowledgeBinary({ channel, version, writerId, records: targets = [], authority = null, epoch = null, sequence = null } = {}) {
|
|
606
|
+
if (!Array.isArray(targets) || !targets.length) return { ok: true, acknowledged: 0 };
|
|
607
|
+
const opened = await open();
|
|
608
|
+
if (!opened.ok) return { ok: false, code: opened.code };
|
|
609
|
+
const outcome = await runWrite(async (ctx) => {
|
|
610
|
+
const capsules = new Map();
|
|
611
|
+
for (const target of targets) {
|
|
612
|
+
if (!target || target.id === undefined || typeof target.revision !== "number") continue;
|
|
613
|
+
const slot = keyOf(target.id);
|
|
614
|
+
const existing = await request(ctx.records.get(slot));
|
|
615
|
+
if (!existing || !sameScope(existing.scope)) continue;
|
|
616
|
+
if (existing.channel !== channel || existing.version !== version) continue;
|
|
617
|
+
if (existing.writerId !== writerId) continue;
|
|
618
|
+
if (existing.revision !== target.revision) continue;
|
|
619
|
+
let capsule = existing;
|
|
620
|
+
let capsuleSlot = slot;
|
|
621
|
+
if (existing.kind !== "binary") {
|
|
622
|
+
capsuleSlot = keyOf(existing.capsuleId);
|
|
623
|
+
capsule = await request(ctx.records.get(capsuleSlot));
|
|
624
|
+
if (!capsule || !sameScope(capsule.scope)) continue;
|
|
625
|
+
if (capsule.channel !== channel || capsule.version !== version) continue;
|
|
626
|
+
}
|
|
627
|
+
capsules.set(slotKey(capsuleSlot), { capsuleSlot, capsule });
|
|
628
|
+
}
|
|
629
|
+
if (!capsules.size) return { ok: true, acknowledged: 0 };
|
|
630
|
+
const acknowledged = { authority, epoch, sequence };
|
|
631
|
+
const totals = await readUsageTotals(ctx.meta, ctx.records);
|
|
632
|
+
let delta = 0;
|
|
633
|
+
for (const { capsuleSlot, capsule } of capsules.values()) {
|
|
634
|
+
const updated = { ...capsule, acknowledged };
|
|
635
|
+
updated.bytes = recordBytes(updated);
|
|
636
|
+
delta += updated.bytes - capsule.bytes;
|
|
637
|
+
await request(ctx.records.put(updated));
|
|
638
|
+
const mark = await request(ctx.watermarks.get(capsuleSlot));
|
|
639
|
+
await request(
|
|
640
|
+
ctx.watermarks.put({
|
|
641
|
+
...(mark || {}),
|
|
642
|
+
key: capsuleSlot,
|
|
643
|
+
kind: "binary",
|
|
644
|
+
retired: mark?.retired ?? [],
|
|
645
|
+
acknowledged,
|
|
646
|
+
updated: now(),
|
|
647
|
+
})
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
await writeUsageTotals(ctx.meta, { scopeBytes: Math.max(0, totals.scopeBytes + delta), dbBytes: Math.max(0, totals.dbBytes + delta) });
|
|
651
|
+
return { ok: true, acknowledged: capsules.size };
|
|
652
|
+
});
|
|
653
|
+
return outcome.ok ? outcome.result : { ok: false, code: outcome.code };
|
|
654
|
+
},
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Delete records a proof covers, and retire their revisions in the same commit.
|
|
658
|
+
*
|
|
659
|
+
* The watermark has to land with the deletion: a queued append for a covered
|
|
660
|
+
* revision can still arrive afterwards, and it must be refused after the copy is
|
|
661
|
+
* gone. Every revision is re-read here, so a record that changed after the proof
|
|
662
|
+
* was checked stays. Nothing is deleted by channel, writer or age.
|
|
663
|
+
*
|
|
664
|
+
* @param {{channel: string, version: number, records: Array<{id: *, revision: number}>}} options
|
|
665
|
+
* @returns {Promise<{ok: boolean, code?: string, retired?: Array<{id: *, revision: number}>}>}
|
|
666
|
+
*/
|
|
667
|
+
async checkpointBinary({ channel, version, writerId, records: targets = [] } = {}) {
|
|
668
|
+
if (!Array.isArray(targets) || !targets.length) return { ok: true, retired: [] };
|
|
669
|
+
const opened = await open();
|
|
670
|
+
if (!opened.ok) return { ok: false, code: opened.code };
|
|
671
|
+
const outcome = await runWrite(async (ctx) => {
|
|
672
|
+
const doomed = [];
|
|
673
|
+
const marks = new Map();
|
|
674
|
+
let bytes = 0;
|
|
675
|
+
for (const target of targets) {
|
|
676
|
+
if (!target || target.id === undefined || typeof target.revision !== "number") continue;
|
|
677
|
+
const slot = keyOf(target.id);
|
|
678
|
+
const existing = await request(ctx.records.get(slot));
|
|
679
|
+
if (!existing || !sameScope(existing.scope)) continue;
|
|
680
|
+
if (existing.channel !== channel || existing.version !== version) continue;
|
|
681
|
+
if (existing.writerId !== writerId) continue;
|
|
682
|
+
if (existing.revision !== target.revision) continue;
|
|
683
|
+
doomed.push({ slot, existing });
|
|
684
|
+
bytes += existing.bytes;
|
|
685
|
+
const markSlot = existing.kind === "binary" ? slot : keyOf(existing.capsuleId);
|
|
686
|
+
const at = slotKey(markSlot);
|
|
687
|
+
if (!marks.has(at)) {
|
|
688
|
+
marks.set(at, { markSlot, ranges: (await request(ctx.watermarks.get(markSlot)))?.retired ?? [] });
|
|
689
|
+
}
|
|
690
|
+
marks.get(at).ranges = retireRange(marks.get(at).ranges, existing.revision);
|
|
691
|
+
}
|
|
692
|
+
if (!doomed.length) return { ok: true, retired: [] };
|
|
693
|
+
const totals = await readUsageTotals(ctx.meta, ctx.records);
|
|
694
|
+
for (const { slot } of doomed) await request(ctx.records.delete(slot));
|
|
695
|
+
for (const { markSlot, ranges } of marks.values()) {
|
|
696
|
+
const mark = await request(ctx.watermarks.get(markSlot));
|
|
697
|
+
await request(
|
|
698
|
+
ctx.watermarks.put({
|
|
699
|
+
...(mark || {}),
|
|
700
|
+
key: markSlot,
|
|
701
|
+
kind: "binary",
|
|
702
|
+
retired: ranges,
|
|
703
|
+
acknowledged: mark?.acknowledged ?? null,
|
|
704
|
+
updated: now(),
|
|
705
|
+
})
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
await writeUsageTotals(ctx.meta, {
|
|
709
|
+
scopeBytes: Math.max(0, totals.scopeBytes - bytes),
|
|
710
|
+
dbBytes: Math.max(0, totals.dbBytes - bytes),
|
|
711
|
+
});
|
|
712
|
+
return { ok: true, retired: doomed.map(({ existing }) => ({ id: existing.id, revision: existing.revision })) };
|
|
713
|
+
});
|
|
714
|
+
return outcome.ok ? outcome.result : { ok: false, code: outcome.code };
|
|
715
|
+
},
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Swap merged batches into their capsule, if the head is still the one that was read.
|
|
719
|
+
*
|
|
720
|
+
* The merge happened outside this transaction; this is only the swap. Every input
|
|
721
|
+
* is re-read at the exact revision the caller observed and the capsule is checked
|
|
722
|
+
* against the revision it had, so a compaction that lost a race changes nothing
|
|
723
|
+
* and can be retried from the new head. Inputs are deleted only after a successful
|
|
724
|
+
* comparison, a replacement that does not fit keeps every original, and the input
|
|
725
|
+
* revisions are retired in this same commit.
|
|
726
|
+
*
|
|
727
|
+
* @param {{capsuleId: *, expectedRevision: number, replacement: Object, inputs?: Array<{id: *, revision: number}>}} options
|
|
728
|
+
* @returns {Promise<{ok: boolean, code?: string, retired?: Array, revision?: number}>}
|
|
729
|
+
*/
|
|
730
|
+
async compactBinary({ channel, version, capsuleId, expectedRevision, replacement = null, inputs = [] } = {}) {
|
|
731
|
+
if (!replacement || typeof replacement.revision !== "number") return { ok: false, code: "invalid" };
|
|
732
|
+
const opened = await open();
|
|
733
|
+
if (!opened.ok) return { ok: false, code: opened.code };
|
|
734
|
+
const capsuleSlot = keyOf(capsuleId);
|
|
735
|
+
const outcome = await runWrite(async (ctx) => {
|
|
736
|
+
const capsule = await request(ctx.records.get(capsuleSlot));
|
|
737
|
+
if (!capsule || !sameScope(capsule.scope) || capsule.kind !== "binary") return { ok: false, code: "missing" };
|
|
738
|
+
if (capsule.channel !== channel) return { ok: false, code: "channel-mismatch" };
|
|
739
|
+
if (capsule.version !== version) return { ok: false, code: "unknown-version" };
|
|
740
|
+
if (capsule.revision !== expectedRevision) return { ok: false, code: "stale" };
|
|
741
|
+
if (replacement.revision <= capsule.revision) return { ok: false, code: "invalid" };
|
|
742
|
+
const doomed = [];
|
|
743
|
+
for (const input of inputs) {
|
|
744
|
+
if (!input || input.id === undefined || typeof input.revision !== "number") return { ok: false, code: "stale" };
|
|
745
|
+
const slot = keyOf(input.id);
|
|
746
|
+
const existing = await request(ctx.records.get(slot));
|
|
747
|
+
if (!existing || !sameScope(existing.scope)) return { ok: false, code: "stale" };
|
|
748
|
+
if (existing.kind !== "binary-batch" || existing.channel !== capsule.channel || existing.version !== capsule.version) {
|
|
749
|
+
return { ok: false, code: "stale" };
|
|
750
|
+
}
|
|
751
|
+
if (!sameValue(existing.capsuleId, capsule.id) || existing.revision !== input.revision) return { ok: false, code: "stale" };
|
|
752
|
+
doomed.push({ slot, existing });
|
|
753
|
+
}
|
|
754
|
+
const updated = { ...capsule, revision: replacement.revision, payload: replacement.payload, updated: replacement.updated };
|
|
755
|
+
updated.bytes = recordBytes(updated);
|
|
756
|
+
let delta = updated.bytes - capsule.bytes;
|
|
757
|
+
for (const { existing } of doomed) delta -= existing.bytes;
|
|
758
|
+
const totals = await readUsageTotals(ctx.meta, ctx.records);
|
|
759
|
+
if (totals.scopeBytes + delta > maxScopeBytes || totals.dbBytes + delta > maxDbBytes) {
|
|
760
|
+
return { ok: false, code: "over-budget" };
|
|
761
|
+
}
|
|
762
|
+
for (const { slot } of doomed) await request(ctx.records.delete(slot));
|
|
763
|
+
await request(ctx.records.put(updated));
|
|
764
|
+
const mark = await request(ctx.watermarks.get(capsuleSlot));
|
|
765
|
+
let retired = mark?.retired ?? [];
|
|
766
|
+
for (const { existing } of doomed) retired = retireRange(retired, existing.revision);
|
|
767
|
+
await request(
|
|
768
|
+
ctx.watermarks.put({
|
|
769
|
+
...(mark || {}),
|
|
770
|
+
key: capsuleSlot,
|
|
771
|
+
kind: "binary",
|
|
772
|
+
retired,
|
|
773
|
+
acknowledged: mark?.acknowledged ?? capsule.acknowledged ?? null,
|
|
774
|
+
updated: now(),
|
|
775
|
+
})
|
|
776
|
+
);
|
|
777
|
+
await writeUsageTotals(ctx.meta, { scopeBytes: totals.scopeBytes + delta, dbBytes: totals.dbBytes + delta });
|
|
778
|
+
return { ok: true, retired: doomed.map(({ existing }) => ({ id: existing.id, revision: existing.revision })), revision: updated.revision };
|
|
779
|
+
});
|
|
780
|
+
return outcome.ok ? outcome.result : { ok: false, code: outcome.code };
|
|
781
|
+
},
|
|
782
|
+
|
|
783
|
+
close() {
|
|
784
|
+
try {
|
|
785
|
+
db?.close();
|
|
786
|
+
} catch (_) {}
|
|
787
|
+
db = null;
|
|
788
|
+
opening = null;
|
|
789
|
+
openAnswer = null;
|
|
790
|
+
},
|
|
791
|
+
};
|
|
792
|
+
}
|