@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,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* recovery-proof.js — what a checkpoint covers, and what makes a proof a proof.
|
|
3
|
+
*
|
|
4
|
+
* A checkpoint covers a journaled record only when merging that record's owned bytes
|
|
5
|
+
* into the accepted page state changes neither an insertion nor a deletion. A state
|
|
6
|
+
* vector cannot answer that: inserting `abc` and then deleting `b` leaves the state
|
|
7
|
+
* vector where it was while the bytes carry the deletion. So coverage here is a byte
|
|
8
|
+
* comparison, it is sufficient but not necessary, and malformed input is an error that
|
|
9
|
+
* keeps the record rather than retiring it.
|
|
10
|
+
*
|
|
11
|
+
* A proof is made by the save integration from the capture it saved under and the bytes
|
|
12
|
+
* it read back. No room acknowledgement, `requestCheckpoint()` success, state vector or
|
|
13
|
+
* ETag order becomes a proof, and nothing here asks a host anything: the accepted page
|
|
14
|
+
* state and the receipt arrive in the proof itself.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { encodeStateAsUpdateV2, mergeUpdatesV2 } from "../../vendor/yjs.vendor.js";
|
|
18
|
+
import { docFromValue } from "./provider.js";
|
|
19
|
+
|
|
20
|
+
export class PageProofError extends Error {
|
|
21
|
+
constructor(code, message) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "PageProofError";
|
|
24
|
+
this.code = code;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const fail = (code, message) => { throw new PageProofError(code, message); };
|
|
29
|
+
const HEX = /^[0-9a-f]{64}$/;
|
|
30
|
+
const encoder = typeof TextEncoder === "function" ? new TextEncoder() : null;
|
|
31
|
+
const bytesOf = (value) => {
|
|
32
|
+
if (value instanceof Uint8Array) return value;
|
|
33
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
34
|
+
return null;
|
|
35
|
+
};
|
|
36
|
+
const identityKey = (id) => (typeof id === "string" ? id : JSON.stringify(id));
|
|
37
|
+
const copyId = (id) => (Array.isArray(id) ? [...id] : id);
|
|
38
|
+
const copyState = (state) => ({ v: state.v, g: state.g, y: { ...(state.y ?? {}) } });
|
|
39
|
+
|
|
40
|
+
export const equalBytes = (a, b) => a.length === b.length && a.every((value, index) => value === b[index]);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Whether merging `update` into `checkpoint` adds nothing: no insertion, no deletion.
|
|
44
|
+
*
|
|
45
|
+
* @param {Uint8Array} checkpoint - canonical or merged Yjs V2 update bytes
|
|
46
|
+
* @param {Uint8Array} update - the bytes to look for
|
|
47
|
+
* @param {Object} [Yjs] - the vendored Yjs module
|
|
48
|
+
* @returns {boolean}
|
|
49
|
+
*/
|
|
50
|
+
export function containsUpdate(checkpoint, update, Yjs = null) {
|
|
51
|
+
const merge = Yjs?.mergeUpdatesV2 ?? mergeUpdatesV2;
|
|
52
|
+
const canonical = merge([checkpoint]);
|
|
53
|
+
return equalBytes(canonical, merge([canonical, update]));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function digestHex(bytes) {
|
|
57
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
|
|
58
|
+
return [...new Uint8Array(digest)].map((value) => value.toString(16).padStart(2, "0")).join("");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The accepted page state as one canonical update, decoded strictly.
|
|
63
|
+
*
|
|
64
|
+
* Pending structs, missing part and a delete set naming an unknown item all refuse:
|
|
65
|
+
* a checkpoint nobody can decode proves nothing about a journaled record.
|
|
66
|
+
*
|
|
67
|
+
* @param {Object} state - a version 1 page value
|
|
68
|
+
* @returns {Uint8Array}
|
|
69
|
+
*/
|
|
70
|
+
export function canonicalPageBytes(state) {
|
|
71
|
+
let doc;
|
|
72
|
+
try {
|
|
73
|
+
doc = docFromValue(state, { strict: true });
|
|
74
|
+
} catch (error) {
|
|
75
|
+
fail("invalid-state", `The accepted page state does not decode: ${error.message}`);
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
return encodeStateAsUpdateV2(doc);
|
|
79
|
+
} finally {
|
|
80
|
+
doc.destroy();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The same answer `sameAuthority` gives in recovery/identity.js, written here because the
|
|
85
|
+
// edit bundle already pays for this module through the provider: importing the generic
|
|
86
|
+
// recovery layer from the page proof would split it into a chunk every editor load pays.
|
|
87
|
+
const sameAuthority = (a, b) => Boolean(a && b) && Boolean(a.host && a.documentId && a.principalId) && a.host === b.host && a.documentId === b.documentId && a.principalId === b.principalId;
|
|
88
|
+
const sameGeneration = (a, b) => Boolean(a && b) && typeof a.generation === "string" && Boolean(a.generation) && a.generation === b.generation;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Assemble the proof a checkpoint is retired with.
|
|
92
|
+
*
|
|
93
|
+
* Each named record carries the bytes it owns and the context it was journaled under, so
|
|
94
|
+
* the validator can refuse a record written under another generation without asking the
|
|
95
|
+
* store or a host. `state` must be the exact accepted page state and `article` the final
|
|
96
|
+
* outgoing canonical article; their digests are taken here, over these bytes.
|
|
97
|
+
*
|
|
98
|
+
* @param {Object} options
|
|
99
|
+
* @param {string} options.capture - the capture the save belongs to
|
|
100
|
+
* @param {Array<{id: *, revision: number, update: Uint8Array, context: ?Object}>} options.records
|
|
101
|
+
* @param {{authority: Object, lineage: Object}} options.accepted - the identity and generation it was saved under
|
|
102
|
+
* @param {string} options.article - the final outgoing canonical article
|
|
103
|
+
* @param {Object} options.state - the accepted page state
|
|
104
|
+
* @param {{file: string, pageState: string}} options.receipt - the persisted pair
|
|
105
|
+
* @param {boolean} [options.saved]
|
|
106
|
+
* @returns {Promise<Object>}
|
|
107
|
+
*/
|
|
108
|
+
export async function pageProof({ capture, records = [], accepted, article, state, receipt, saved = true } = {}) {
|
|
109
|
+
const entries = [];
|
|
110
|
+
for (const record of records) {
|
|
111
|
+
if (!record || record.id === undefined || typeof record.revision !== "number") fail("invalid-record", "A proof names each record by id and revision");
|
|
112
|
+
const update = bytesOf(record.update);
|
|
113
|
+
if (!update || !update.length) fail("invalid-record", "A proof needs the bytes each record owns");
|
|
114
|
+
entries.push({ id: copyId(record.id), revision: record.revision, update: update.slice(), context: record.context ?? null });
|
|
115
|
+
}
|
|
116
|
+
if (typeof article !== "string") fail("missing-article", "A proof names the final outgoing article");
|
|
117
|
+
const stateBytes = canonicalPageBytes(state);
|
|
118
|
+
return {
|
|
119
|
+
saved: saved === true,
|
|
120
|
+
capture,
|
|
121
|
+
accepted,
|
|
122
|
+
article,
|
|
123
|
+
state: copyState(state),
|
|
124
|
+
stateDigest: await digestHex(stateBytes),
|
|
125
|
+
articleDigest: await digestHex(encoder.encode(article)),
|
|
126
|
+
receipt,
|
|
127
|
+
records: entries,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Checklist items 1, 2, 4 and 5, over the proof alone. Item 3 is the per-record coverage
|
|
132
|
+
// test below, and the projection of the state onto the article is the producer's to make:
|
|
133
|
+
// a proof whose state is not the one it was made over fails its own digest.
|
|
134
|
+
async function checkProof(proof) {
|
|
135
|
+
if (!proof || typeof proof !== "object" || Array.isArray(proof)) fail("missing-proof", "A page checkpoint needs a proof");
|
|
136
|
+
if (proof.saved !== true) fail("not-saved", "Only a save that succeeded under admission makes a proof");
|
|
137
|
+
if (typeof proof.capture !== "string" || !proof.capture) fail("missing-capture", "A proof belongs to the capture it saved");
|
|
138
|
+
const receipt = proof.receipt;
|
|
139
|
+
if (!receipt || typeof receipt.file !== "string" || !receipt.file || typeof receipt.pageState !== "string" || !receipt.pageState) {
|
|
140
|
+
fail("missing-receipt", "A proof names the persisted file and page-state pair");
|
|
141
|
+
}
|
|
142
|
+
const accepted = proof.accepted;
|
|
143
|
+
if (!accepted || !accepted.authority || !accepted.lineage || typeof accepted.lineage.generation !== "string" || !accepted.lineage.generation) {
|
|
144
|
+
fail("missing-identity", "A proof names the authority and generation it was saved under");
|
|
145
|
+
}
|
|
146
|
+
if (typeof proof.article !== "string") fail("missing-article", "A proof names the final outgoing article");
|
|
147
|
+
if (!Array.isArray(proof.records)) fail("missing-records", "A proof names the records it covers");
|
|
148
|
+
if (typeof proof.stateDigest !== "string" || !HEX.test(proof.stateDigest)) fail("missing-digest", "A proof carries the accepted state digest");
|
|
149
|
+
if (typeof proof.articleDigest !== "string" || !HEX.test(proof.articleDigest)) fail("missing-digest", "A proof carries the final article digest");
|
|
150
|
+
const stateBytes = canonicalPageBytes(proof.state);
|
|
151
|
+
if (await digestHex(stateBytes) !== proof.stateDigest) fail("state-digest-mismatch", "The page state is not the one the proof was made over");
|
|
152
|
+
if (await digestHex(encoder.encode(proof.article)) !== proof.articleDigest) fail("article-digest-mismatch", "The article is not the one the proof was made over");
|
|
153
|
+
const entries = new Map();
|
|
154
|
+
for (const entry of proof.records) {
|
|
155
|
+
if (!entry || entry.id === undefined || typeof entry.revision !== "number") fail("invalid-record", "A proof names each record by id and revision");
|
|
156
|
+
const update = bytesOf(entry.update);
|
|
157
|
+
if (!update || !update.length) fail("invalid-record", "A proof needs the bytes each record owns");
|
|
158
|
+
entries.set(identityKey(entry.id), entry);
|
|
159
|
+
}
|
|
160
|
+
return { stateBytes, entries, accepted };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// A record is covered only by a state that still carries it, saved under the same
|
|
164
|
+
// authority and the same generation it was journaled under.
|
|
165
|
+
function covers(checked, entry) {
|
|
166
|
+
const context = entry.context;
|
|
167
|
+
if (!context || !sameAuthority(context.authority, checked.accepted.authority)) return false;
|
|
168
|
+
if (!sameGeneration(context.lineage, checked.accepted.lineage)) return false;
|
|
169
|
+
const update = bytesOf(entry.update);
|
|
170
|
+
return Boolean(update) && update.length > 0 && containsUpdate(checked.stateBytes, update);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The records a page checkpoint proof covers, and no others.
|
|
175
|
+
*
|
|
176
|
+
* Registered as `validateCheckpoint` on the `page.yjs` channel. It runs before the
|
|
177
|
+
* store's transaction, may only narrow the caller's list, and throws on a proof it
|
|
178
|
+
* cannot decode so that an error keeps every record.
|
|
179
|
+
*
|
|
180
|
+
* @param {Object} proof
|
|
181
|
+
* @param {Array<{id: *, revision: number}>} records - what the caller observed
|
|
182
|
+
* @returns {Promise<Array<{id: *, revision: number}>>}
|
|
183
|
+
*/
|
|
184
|
+
export async function validatePageCheckpoint(proof, records = []) {
|
|
185
|
+
const checked = await checkProof(proof);
|
|
186
|
+
return (records || []).filter((record) => {
|
|
187
|
+
if (!record || record.id === undefined || typeof record.revision !== "number") return false;
|
|
188
|
+
const entry = checked.entries.get(identityKey(record.id));
|
|
189
|
+
return Boolean(entry) && entry.revision === record.revision && covers(checked, entry);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* recovery-replay.js — rebuild a page from one journaled capsule, and decide whether it
|
|
3
|
+
* may be replayed at all.
|
|
4
|
+
*
|
|
5
|
+
* A capsule is one immutable base and everything the episode put on top of it: the
|
|
6
|
+
* dependencies it received (a peer update a later local insert was built on) and the
|
|
7
|
+
* local work it owns. Replaying all three into a fresh Y.Doc rebuilds the page as this
|
|
8
|
+
* tab had it, read back through the strict decoder — no room, no host, no store.
|
|
9
|
+
*
|
|
10
|
+
* Admission is a separate question from reconstruction: a journal replays only into the
|
|
11
|
+
* authority, principal and generation it was written under, only while the current page
|
|
12
|
+
* still contains the capsule's base, and only into a page sharing its ancestry.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as Y from "../../vendor/yjs.vendor.js";
|
|
16
|
+
import { toBase64 } from "../../vendor/yjs.vendor.js";
|
|
17
|
+
import { NET } from "./layout.js";
|
|
18
|
+
import { blocksFromYValue } from "./decode.js";
|
|
19
|
+
import { sameAuthority, hostIdentityOf, pathIdentityOf } from "../../recovery/identity.js";
|
|
20
|
+
import { sameLineage, sharesAncestry, lineageOf } from "./lineage.js";
|
|
21
|
+
import { canonicalPageBytes, containsUpdate, digestHex, validatePageCheckpoint } from "./recovery-proof.js";
|
|
22
|
+
import { RECOVERY_ORIGIN } from "./provider.js";
|
|
23
|
+
|
|
24
|
+
/** The channel this adapter journals and replays: one name, one version, everywhere. */
|
|
25
|
+
export const PAGE_CHANNEL = "page.yjs";
|
|
26
|
+
export const PAGE_CHANNEL_VERSION = 1;
|
|
27
|
+
|
|
28
|
+
// When one writer's batches are merged into its capsule: past either threshold, and never
|
|
29
|
+
// over a record the store has not confirmed.
|
|
30
|
+
export const COMPACT_BATCHES = 64;
|
|
31
|
+
export const COMPACT_BYTES = 1024 * 1024;
|
|
32
|
+
|
|
33
|
+
/** The channel's own merge, for the binary store's compaction. */
|
|
34
|
+
export const mergePageUpdates = (parts) => Y.mergeUpdatesV2(parts);
|
|
35
|
+
|
|
36
|
+
export class PageReplayError extends Error {
|
|
37
|
+
constructor(code, message) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = "PageReplayError";
|
|
40
|
+
this.code = code;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const fail = (code, message) => { throw new PageReplayError(code, message); };
|
|
45
|
+
const identityKey = (id) => (typeof id === "string" ? id : JSON.stringify(id));
|
|
46
|
+
const bytesOf = (value) => {
|
|
47
|
+
if (value instanceof Uint8Array) return value;
|
|
48
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
49
|
+
return null;
|
|
50
|
+
};
|
|
51
|
+
const sameId = (a, b) => identityKey(a) === identityKey(b);
|
|
52
|
+
|
|
53
|
+
function merge(values) {
|
|
54
|
+
const parts = values.map(bytesOf).filter((bytes) => bytes && bytes.length);
|
|
55
|
+
if (!parts.length) return new Uint8Array(0);
|
|
56
|
+
if (parts.length === 1) return parts[0].slice();
|
|
57
|
+
return Y.mergeUpdatesV2(parts);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* One writer's capsule and its batches as three self-contained updates.
|
|
62
|
+
*
|
|
63
|
+
* Batches are folded in by origin, so a duplicate batch or an update already compacted
|
|
64
|
+
* into the capsule changes nothing: Yjs merges by operation identity. A batch naming
|
|
65
|
+
* another capsule is an error rather than another page's bytes.
|
|
66
|
+
*
|
|
67
|
+
* @param {{capsule: Object, batches?: Array<Object>}} options
|
|
68
|
+
* @returns {Object}
|
|
69
|
+
*/
|
|
70
|
+
export function accumulateCapsule({ capsule, batches = [] } = {}) {
|
|
71
|
+
if (!capsule || capsule.kind !== "binary" || !capsule.payload) fail("invalid-capsule", "Replay needs a binary capsule");
|
|
72
|
+
if (capsule.version !== undefined && capsule.version !== 1) fail("unknown-version", `Unknown page recovery capsule version: ${capsule.version}`);
|
|
73
|
+
const dependencies = [capsule.payload.dependencies];
|
|
74
|
+
const owned = [capsule.payload.owned];
|
|
75
|
+
const seen = new Set();
|
|
76
|
+
for (const batch of batches) {
|
|
77
|
+
if (!batch || batch.kind !== "binary-batch" || !batch.payload) fail("invalid-batch", "Replay reads binary batches only");
|
|
78
|
+
if (batch.capsuleId !== undefined && !sameId(batch.capsuleId, capsule.id)) fail("foreign-batch", "A batch belongs to another capsule");
|
|
79
|
+
if (batch.id !== undefined) {
|
|
80
|
+
if (seen.has(identityKey(batch.id))) continue;
|
|
81
|
+
seen.add(identityKey(batch.id));
|
|
82
|
+
}
|
|
83
|
+
if (batch.origin === "dependency") dependencies.push(batch.payload);
|
|
84
|
+
else owned.push(batch.payload);
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
id: capsule.id,
|
|
88
|
+
context: capsule.context ?? null,
|
|
89
|
+
revision: capsule.revision ?? 0,
|
|
90
|
+
acknowledged: capsule.acknowledged ?? null,
|
|
91
|
+
base: merge([capsule.payload.base]),
|
|
92
|
+
dependencies: merge(dependencies),
|
|
93
|
+
owned: merge(owned),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// The capsule as one page value: the exact bytes a save would carry, so the strict
|
|
98
|
+
// decoder reads the same state a checkpoint would.
|
|
99
|
+
export function replayPageValue({ capsule, batches = [] } = {}) {
|
|
100
|
+
const parts = accumulateCapsule({ capsule, batches });
|
|
101
|
+
const doc = new Y.Doc();
|
|
102
|
+
try {
|
|
103
|
+
for (const bytes of [parts.base, parts.dependencies, parts.owned]) if (bytes.length) Y.applyUpdateV2(doc, bytes, NET);
|
|
104
|
+
if (doc.store.pendingStructs || doc.store.pendingDs) fail("unresolved-update", "The capsule's updates are missing a prerequisite");
|
|
105
|
+
return { v: 1, g: 0, y: { seed: toBase64(Y.encodeStateAsUpdateV2(doc)) } };
|
|
106
|
+
} finally {
|
|
107
|
+
doc.destroy();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The whole page a capsule reconstructs, decoded through the strict decoder.
|
|
113
|
+
*
|
|
114
|
+
* @param {{capsule: Object, batches?: Array<Object>}} options
|
|
115
|
+
* @returns {Object} the block model, by block id
|
|
116
|
+
*/
|
|
117
|
+
export function reconstructPage({ capsule, batches = [] } = {}) {
|
|
118
|
+
return blocksFromYValue(replayPageValue({ capsule, batches }));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The capsule's parts, if this page may replay them.
|
|
123
|
+
*
|
|
124
|
+
* `current` is the page as it is now: its own authority, generation and accepted state.
|
|
125
|
+
* A journal from another authority or principal is never applied, a journal from another
|
|
126
|
+
* generation is a page that was replaced while the tab was away, and a page that no
|
|
127
|
+
* longer contains the capsule's base has nothing to replay it onto.
|
|
128
|
+
*
|
|
129
|
+
* @param {{capsule: Object, batches?: Array<Object>, current: {context: Object, state: Object}}} options
|
|
130
|
+
* @returns {Object} the accumulated parts, ready to apply
|
|
131
|
+
*/
|
|
132
|
+
export function admitCapsule({ capsule, batches = [], current } = {}) {
|
|
133
|
+
const parts = accumulateCapsule({ capsule, batches });
|
|
134
|
+
if (!parts.context?.authority) fail("missing-identity", "A capsule with no authority is never replayed");
|
|
135
|
+
if (!current?.context?.authority) fail("missing-identity", "Replay needs the page's own authority");
|
|
136
|
+
if (!sameAuthority(parts.context.authority, current.context.authority)) fail("foreign-authority", "This journal belongs to another document or account");
|
|
137
|
+
if (!sameLineage(parts.context.lineage, current.context.lineage)) fail("foreign-generation", "This journal belongs to another generation of this page");
|
|
138
|
+
const stateBytes = canonicalPageBytes(current.state);
|
|
139
|
+
if (parts.base.length && !containsUpdate(stateBytes, parts.base)) fail("missing-base", "The page no longer contains the journal's base");
|
|
140
|
+
if (!parts.owned.length) fail("nothing-owned", "This capsule owns no local work");
|
|
141
|
+
// The reconstructed page is what has to share this page's ancestry, base included:
|
|
142
|
+
// this tab's own inserts carry its client alone, and never the page's.
|
|
143
|
+
const rebuilt = merge([parts.base, parts.dependencies, parts.owned]);
|
|
144
|
+
if (!sharesAncestry(current.state, { v: 1, g: 0, y: { seed: toBase64(rebuilt) } })) fail("foreign-lineage", "This journal shares no ancestry with this page");
|
|
145
|
+
return parts;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* One writer's capsules, each with the batches that belong to it.
|
|
150
|
+
*
|
|
151
|
+
* A session that died before its batches were compacted leaves them behind, so a
|
|
152
|
+
* capsule is only ever read together with every batch naming it, whichever session
|
|
153
|
+
* wrote it.
|
|
154
|
+
*
|
|
155
|
+
* @param {Array<Object>} [records] - what the channel listed
|
|
156
|
+
* @returns {Array<{capsule: Object, batches: Array<Object>}>}
|
|
157
|
+
*/
|
|
158
|
+
export function capsulesOf(records = []) {
|
|
159
|
+
const capsules = new Map();
|
|
160
|
+
const batches = new Map();
|
|
161
|
+
for (const record of records || []) {
|
|
162
|
+
if (!record || record.supported === false) continue;
|
|
163
|
+
if (record.kind === "binary") {
|
|
164
|
+
capsules.set(identityKey(record.id), record);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (record.kind !== "binary-batch" || record.capsuleId === undefined) continue;
|
|
168
|
+
const key = identityKey(record.capsuleId);
|
|
169
|
+
const list = batches.get(key) ?? [];
|
|
170
|
+
list.push(record);
|
|
171
|
+
batches.set(key, list);
|
|
172
|
+
}
|
|
173
|
+
return [...capsules.entries()].map(([key, capsule]) => ({ capsule, batches: batches.get(key) ?? [] }));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Apply an admitted capsule's three parts into a live document.
|
|
178
|
+
*
|
|
179
|
+
* The recovery origin is its own: the provider offers the result to the room, and the
|
|
180
|
+
* journal never takes it as this tab's own work, so a recovery is never journaled twice.
|
|
181
|
+
*
|
|
182
|
+
* @param {Object} doc - the Y.Doc the page is being edited in
|
|
183
|
+
* @param {Object} parts - an `accumulateCapsule` result
|
|
184
|
+
*/
|
|
185
|
+
export function applyCapsule(doc, parts) {
|
|
186
|
+
for (const bytes of [parts.base, parts.dependencies, parts.owned]) {
|
|
187
|
+
if (bytes && bytes.length) Y.applyUpdateV2(doc, bytes, RECOVERY_ORIGIN);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Replay every capsule this page may replay, and name the ones it may not.
|
|
193
|
+
*
|
|
194
|
+
* Nothing is applied for a capsule that fails admission: a changed authority, a changed
|
|
195
|
+
* generation, a base the page no longer contains or no shared ancestry is review, not a
|
|
196
|
+
* merge. A capsule with no owned work has nothing to restore and is skipped silently.
|
|
197
|
+
*
|
|
198
|
+
* @param {{records?: Array<Object>, current: {context: Object, state: Object}, into: Object}} options
|
|
199
|
+
* @returns {{replayed: Array<Object>, review: Array<Object>}}
|
|
200
|
+
*/
|
|
201
|
+
export function replayJournals({ records = [], current, into } = {}) {
|
|
202
|
+
const replayed = [];
|
|
203
|
+
const review = [];
|
|
204
|
+
for (const entry of capsulesOf(records)) {
|
|
205
|
+
let parts;
|
|
206
|
+
try {
|
|
207
|
+
parts = admitCapsule({ capsule: entry.capsule, batches: entry.batches, current });
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (error?.code === "nothing-owned") continue;
|
|
210
|
+
review.push({ capsule: entry.capsule, code: error?.code ?? "invalid-capsule", message: error?.message ?? "This journal cannot be replayed" });
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
applyCapsule(into, parts);
|
|
214
|
+
replayed.push({ id: entry.capsule.id, revision: parts.revision, owned: parts.owned.length });
|
|
215
|
+
}
|
|
216
|
+
return { replayed, review };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* The context one episode's capsules are journaled under.
|
|
221
|
+
*
|
|
222
|
+
* A host that persists a page generation names it; without one, the generation is the
|
|
223
|
+
* digest of the accepted base, so a page rebuilt from other bytes is another generation
|
|
224
|
+
* and a page that has not changed keeps its own.
|
|
225
|
+
*
|
|
226
|
+
* @param {Object} options
|
|
227
|
+
* @param {{host: string, documentId: string, principalId: string}} options.authority
|
|
228
|
+
* @param {?string} options.generation
|
|
229
|
+
* @param {?Uint8Array} options.base - the accepted state captured at the episode's start
|
|
230
|
+
* @param {number} [options.formatFloor]
|
|
231
|
+
* @returns {Promise<Object>}
|
|
232
|
+
*/
|
|
233
|
+
export async function pageJournalContext({ authority, generation = null, base = null, formatFloor = 0 } = {}) {
|
|
234
|
+
const bytes = bytesOf(base) ?? new Uint8Array(0);
|
|
235
|
+
const token = typeof generation === "string" && generation ? generation : await digestHex(bytes);
|
|
236
|
+
return { authority, lineage: await lineageOf(token, bytes), formatFloor };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The authority this page journals under, or null when it cannot be pinned.
|
|
241
|
+
*
|
|
242
|
+
* A host that names people names the account too, and an answer without one is not a
|
|
243
|
+
* principal: replay waits for the next boot rather than filing work under a guess. A
|
|
244
|
+
* host with no people support gives its document to whoever holds it, so the document's
|
|
245
|
+
* own host identity is the principal there.
|
|
246
|
+
*
|
|
247
|
+
* @param {Object} [options]
|
|
248
|
+
* @param {?Object} [options.meta] - the host's discovery answer
|
|
249
|
+
* @param {?Location} [options.location]
|
|
250
|
+
* @param {?Element} [options.root]
|
|
251
|
+
* @returns {?{host: string, documentId: string, principalId: string}}
|
|
252
|
+
*/
|
|
253
|
+
export function pageAuthority({ meta = null, location: where = globalThis.location, root = globalThis.document?.documentElement } = {}) {
|
|
254
|
+
const host = where?.origin ?? "";
|
|
255
|
+
if (!host) return null;
|
|
256
|
+
const documentId = hostIdentityOf(root)?.identity ?? pathIdentityOf(where?.pathname ?? "/").identity;
|
|
257
|
+
if (!documentId) return null;
|
|
258
|
+
const people = meta?.document?.people;
|
|
259
|
+
const principalId = people && typeof people === "object" ? people.me?.id ?? null : documentId;
|
|
260
|
+
if (!principalId) return null;
|
|
261
|
+
return { host, documentId, principalId };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const journalKey = (id) => (typeof id === "string" ? id : JSON.stringify(id));
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* The accepted page state an episode starts from, as the bytes its capsule keeps.
|
|
268
|
+
*
|
|
269
|
+
* @param {Object} state - a version 1 page value
|
|
270
|
+
* @returns {Uint8Array}
|
|
271
|
+
*/
|
|
272
|
+
export const journalBase = (state) => canonicalPageBytes(state);
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* The page channel of one recovery service, registered once per page load.
|
|
276
|
+
*
|
|
277
|
+
* A channel is registered at its own version and nothing calls it until a page can
|
|
278
|
+
* actually journal: a reader and a page without a pinned identity register nothing.
|
|
279
|
+
*
|
|
280
|
+
* @param {Object} service - `clay.recovery`
|
|
281
|
+
* @returns {Object} the channel api
|
|
282
|
+
*/
|
|
283
|
+
const channels = new WeakMap();
|
|
284
|
+
export function pageChannel(service) {
|
|
285
|
+
if (!service) return null;
|
|
286
|
+
const existing = channels.get(service);
|
|
287
|
+
if (existing) return existing;
|
|
288
|
+
const channel = service.register({
|
|
289
|
+
channel: PAGE_CHANNEL,
|
|
290
|
+
version: PAGE_CHANNEL_VERSION,
|
|
291
|
+
validateCheckpoint: validatePageCheckpoint,
|
|
292
|
+
merge: mergePageUpdates,
|
|
293
|
+
});
|
|
294
|
+
channels.set(service, channel);
|
|
295
|
+
return channel;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* One session's journal in the shared recovery store.
|
|
300
|
+
*
|
|
301
|
+
* Every local update is handed over as it happens: the bytes and the id are fixed
|
|
302
|
+
* synchronously, so a buffer reused after the call cannot change what was written, and
|
|
303
|
+
* the append resolves only once its transaction committed. Nothing here waits for the
|
|
304
|
+
* room, the network or the editor. Compaction names only the records the store has
|
|
305
|
+
* confirmed, because an append still in flight or refused has no revision to swap and
|
|
306
|
+
* must keep exactly the bytes it was given.
|
|
307
|
+
*
|
|
308
|
+
* @param {Object} options
|
|
309
|
+
* @param {Object} options.channel - the `page.yjs` channel api
|
|
310
|
+
* @param {string} options.key - the document this episode belongs to
|
|
311
|
+
* @param {Function|Object} options.context - (base) => Promise<context>, or the context
|
|
312
|
+
* @param {?Uint8Array} [options.base] - the accepted state the episode started from
|
|
313
|
+
* @returns {Object}
|
|
314
|
+
*/
|
|
315
|
+
export function createPageJournal({ channel, key, context, base = null, onChange = () => {} }) {
|
|
316
|
+
const records = new Map();
|
|
317
|
+
const waiting = [];
|
|
318
|
+
const inflight = new Set();
|
|
319
|
+
const bytesOfBase = bytesOf(base) ?? new Uint8Array(0);
|
|
320
|
+
let resolved = null;
|
|
321
|
+
let failure = null;
|
|
322
|
+
let batches = 0;
|
|
323
|
+
let batchBytes = 0;
|
|
324
|
+
let episode = false;
|
|
325
|
+
let compacting = null;
|
|
326
|
+
let closed = false;
|
|
327
|
+
|
|
328
|
+
const journal = {
|
|
329
|
+
channel,
|
|
330
|
+
key,
|
|
331
|
+
onChange,
|
|
332
|
+
context: () => ready,
|
|
333
|
+
local: (bytes) => send(bytes, "local"),
|
|
334
|
+
// Only while an episode is open: a peer's updates on their own are not this tab's work
|
|
335
|
+
// to keep, and a page that has journaled nothing owns nothing.
|
|
336
|
+
dependency: (bytes) => { if (episode) send(bytes, "dependency"); },
|
|
337
|
+
records: () => [...records.values()],
|
|
338
|
+
names,
|
|
339
|
+
pending: () => inflight.size + (failure ? 1 : 0),
|
|
340
|
+
get failure() { return failure; },
|
|
341
|
+
retire,
|
|
342
|
+
compact,
|
|
343
|
+
// An acknowledgement is metadata: it says a room holds the bytes and retires nothing.
|
|
344
|
+
acknowledged: () => {
|
|
345
|
+
if (closed || !resolved || !records.size || typeof channel.markAcknowledged !== "function") return Promise.resolve(null);
|
|
346
|
+
return Promise.resolve(channel.markAcknowledged({ records: names(), authority: resolved.authority })).catch(() => null);
|
|
347
|
+
},
|
|
348
|
+
close: () => {
|
|
349
|
+
closed = true;
|
|
350
|
+
waiting.length = 0;
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const ready = Promise.resolve()
|
|
355
|
+
.then(() => (typeof context === "function" ? context(bytesOfBase) : context))
|
|
356
|
+
.catch(() => null)
|
|
357
|
+
.then((value) => {
|
|
358
|
+
resolved = value && value.authority ? value : null;
|
|
359
|
+
if (!resolved) {
|
|
360
|
+
closed = true;
|
|
361
|
+
failure = "unavailable";
|
|
362
|
+
waiting.length = 0;
|
|
363
|
+
journal.onChange();
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
for (const run of waiting.splice(0)) run(resolved);
|
|
367
|
+
return resolved;
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
function retire(list) {
|
|
371
|
+
for (const item of list || []) records.delete(journalKey(item?.id));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function names() {
|
|
375
|
+
return [...records.values()].map(({ id, revision }) => ({ id, revision }));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function compact() {
|
|
379
|
+
if (compacting || closed || !records.size || typeof channel.compact !== "function") return compacting;
|
|
380
|
+
const named = names();
|
|
381
|
+
compacting = Promise.resolve()
|
|
382
|
+
.then(() => channel.compact({ records: named }))
|
|
383
|
+
.then((receipt) => {
|
|
384
|
+
if (!receipt?.ok) return receipt;
|
|
385
|
+
retire(receipt.retired);
|
|
386
|
+
batches = Math.max(0, batches - (receipt.retired?.length ?? 0));
|
|
387
|
+
if (!batches) batchBytes = 0;
|
|
388
|
+
return receipt;
|
|
389
|
+
})
|
|
390
|
+
.catch(() => null)
|
|
391
|
+
.then((receipt) => {
|
|
392
|
+
compacting = null;
|
|
393
|
+
journal.onChange();
|
|
394
|
+
return receipt;
|
|
395
|
+
});
|
|
396
|
+
return compacting;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function write(context, bytes, origin, first) {
|
|
400
|
+
let answer;
|
|
401
|
+
try {
|
|
402
|
+
answer = channel.append({ key, context, update: bytes, origin, base: first ? bytesOfBase : null });
|
|
403
|
+
} catch (_) {
|
|
404
|
+
failure = "error";
|
|
405
|
+
journal.onChange();
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const token = {};
|
|
409
|
+
inflight.add(token);
|
|
410
|
+
Promise.resolve(answer).then(
|
|
411
|
+
(receipt) => {
|
|
412
|
+
inflight.delete(token);
|
|
413
|
+
if (!receipt || receipt.ok !== true) {
|
|
414
|
+
failure = receipt?.code ?? "error";
|
|
415
|
+
journal.onChange();
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
failure = null;
|
|
419
|
+
if (!receipt.skipped) {
|
|
420
|
+
records.set(journalKey(receipt.id), { id: receipt.id, revision: receipt.revision, update: bytes, context });
|
|
421
|
+
batches += 1;
|
|
422
|
+
batchBytes += bytes.byteLength;
|
|
423
|
+
}
|
|
424
|
+
journal.onChange();
|
|
425
|
+
if (batches >= COMPACT_BATCHES || batchBytes >= COMPACT_BYTES) compact();
|
|
426
|
+
},
|
|
427
|
+
() => {
|
|
428
|
+
inflight.delete(token);
|
|
429
|
+
failure = "error";
|
|
430
|
+
journal.onChange();
|
|
431
|
+
},
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function send(bytes, origin) {
|
|
436
|
+
if (closed || !bytes || !bytes.byteLength || typeof channel.append !== "function") return;
|
|
437
|
+
const first = !episode;
|
|
438
|
+
episode = true;
|
|
439
|
+
if (resolved) write(resolved, bytes, origin, first);
|
|
440
|
+
else waiting.push((context) => write(context, bytes, origin, first));
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return journal;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* This session's journal, opened from the host's own answer, or null.
|
|
448
|
+
*
|
|
449
|
+
* A host that cannot name who is writing, or a tab with no recovery service, journals
|
|
450
|
+
* nothing: a page that cannot pin its identity never replays across sessions.
|
|
451
|
+
*
|
|
452
|
+
* @param {Object} options
|
|
453
|
+
* @param {?Object} options.meta - the host's discovery answer
|
|
454
|
+
* @param {Object} options.service - `clay.recovery`
|
|
455
|
+
* @param {?Uint8Array} options.base - the accepted state captured before input
|
|
456
|
+
* @param {Function} [options.onChange]
|
|
457
|
+
* @returns {?Object}
|
|
458
|
+
*/
|
|
459
|
+
export function openPageJournal({ meta = null, service = null, base = null, onChange = () => {}, location, root } = {}) {
|
|
460
|
+
const authority = pageAuthority({ meta, location, root });
|
|
461
|
+
if (!authority) return null;
|
|
462
|
+
const channel = pageChannel(service);
|
|
463
|
+
if (!channel) return null;
|
|
464
|
+
const generation = meta?.document?.pageState?.generation ?? null;
|
|
465
|
+
const formatFloor = Array.isArray(meta?.extensions) && meta.extensions.includes("page-format-v1") ? 1 : 0;
|
|
466
|
+
return createPageJournal({
|
|
467
|
+
channel,
|
|
468
|
+
key: [authority.host, authority.documentId].join(":"),
|
|
469
|
+
context: (accepted) => pageJournalContext({ authority, generation, base: accepted, formatFloor }),
|
|
470
|
+
base,
|
|
471
|
+
onChange,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Replay this page's journal once, after the authority snapshot is adopted.
|
|
477
|
+
*
|
|
478
|
+
* @param {{journal: Object, state: Object, into: Object}} options
|
|
479
|
+
* @returns {Promise<{replayed: Array<Object>, review: Array<Object>}>}
|
|
480
|
+
*/
|
|
481
|
+
export async function replayInto({ journal, state, into } = {}) {
|
|
482
|
+
if (!journal) return { replayed: [], review: [] };
|
|
483
|
+
const context = await journal.context();
|
|
484
|
+
if (!context) return { replayed: [], review: [] };
|
|
485
|
+
const listed = await journal.channel.list();
|
|
486
|
+
if (!listed?.ok) return { replayed: [], review: [] };
|
|
487
|
+
return replayJournals({ records: listed.records, current: { context, state }, into });
|
|
488
|
+
}
|