@panphora/clayjs 1.17.0 → 1.18.1
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/dist/all.js +1 -1
- package/dist/clay-dom.js +1 -1
- package/dist/clay-events.js +1 -1
- package/dist/clay-internals.js +1 -1
- package/dist/clay-options.js +1 -1
- package/dist/clay-router.js +1 -1
- package/dist/clay-ui.js +1 -1
- package/dist/clay-utils.js +1 -1
- package/dist/clay.js +1 -1
- package/dist/clay.standalone.js +72474 -62090
- package/dist/m/attrs/{save-freeze-MYNVXREN.js → save-freeze-LWGF6XUN.js} +1 -1
- package/dist/m/c-2GRUM77Q.js +30 -0
- package/dist/m/c-2KIEGNC4.js +1 -0
- package/dist/m/{c-AMSNPOCX.js → c-2KL3BD52.js} +1 -1
- package/dist/m/c-2ZZBC45O.js +1 -0
- package/dist/m/c-372N6H3B.js +1 -0
- package/dist/m/{c-4WKFKEKF.js → c-3G2DGZUP.js} +1 -1
- package/dist/m/c-3LDGIBMN.js +1 -0
- package/dist/m/c-3O7OBDCM.js +1 -0
- package/dist/m/c-4ASQPRQL.js +1 -0
- package/dist/m/c-4TXNLX4K.js +6 -0
- package/dist/m/c-4YU3BR4D.js +6 -0
- package/dist/m/c-5734MF2P.js +1 -0
- package/dist/m/c-57XDY6HC.js +1 -0
- package/dist/m/{c-KAU6E43D.js → c-5B4YBZXC.js} +1 -1
- package/dist/m/c-5ONTYIUI.js +1 -0
- package/dist/m/c-5OOVFCJM.js +1 -0
- package/dist/m/{c-3XIRD4NU.js → c-5T6NZHNM.js} +1 -1
- package/dist/m/{c-74MGYWZF.js → c-5ZQV7KC4.js} +1 -1
- package/dist/m/{c-7OLLJPWP.js → c-6DB2OLRJ.js} +1 -1
- package/dist/m/c-6DNRCPNM.js +1 -0
- package/dist/m/c-6UGWFMQC.js +2 -0
- package/dist/m/c-7K4SCQB7.js +1 -0
- package/dist/m/c-7MG5RU63.js +1 -0
- package/dist/m/c-7PRJCA3E.js +1 -0
- package/dist/m/c-A6L3FMUS.js +1 -0
- package/dist/m/c-AMU3BPPR.js +4 -0
- package/dist/m/{c-Y5HBBIQ4.js → c-AMYEFA2E.js} +1 -1
- package/dist/m/c-AQYCG7QW.js +1 -0
- package/dist/m/c-ATKAOH23.js +1 -0
- package/dist/m/c-B5CS55WR.js +1 -0
- package/dist/m/c-B5UJQT7W.js +1 -0
- package/dist/m/c-B7J4K2AE.js +3 -0
- package/dist/m/c-BHZYSHW4.js +1 -0
- package/dist/m/{c-VHM5XQHK.js → c-BIJALSRD.js} +1 -1
- package/dist/m/c-BJ7YQ5QZ.js +1 -0
- package/dist/m/{c-KIPQJX4B.js → c-BRLCTNMC.js} +1 -1
- package/dist/m/{c-VE6ZMYMC.js → c-BUAYFEV5.js} +1 -1
- package/dist/m/c-C6IPRG34.js +1 -0
- package/dist/m/{c-KU3ZFADW.js → c-CAS3NND4.js} +1 -1
- package/dist/m/{c-PS5ARVWR.js → c-CCQ25GFY.js} +1 -1
- package/dist/m/c-CJFDLD43.js +1 -0
- package/dist/m/c-COPTL5WB.js +1 -0
- package/dist/m/c-D3E4WJ4M.js +166 -0
- package/dist/m/c-D5IKEKXW.js +7 -0
- package/dist/m/c-DHBRA6DL.js +1 -0
- package/dist/m/c-DKLS74XB.js +72 -0
- package/dist/m/c-DVYGFTSK.js +3 -0
- package/dist/m/c-E3ZXARWA.js +1 -0
- package/dist/m/c-EK63G3CG.js +1 -0
- package/dist/m/c-ERQSNR6K.js +13 -0
- package/dist/m/c-EVYAISP3.js +1 -0
- package/dist/m/c-EXKHPOVC.js +3 -0
- package/dist/m/{c-QJ4OM3G6.js → c-FWKHM4E7.js} +1 -1
- package/dist/m/{c-4Q2XTQ2N.js → c-GBKB5WDL.js} +1 -1
- package/dist/m/c-GNDL3TKB.js +1 -0
- package/dist/m/c-GRWMD6O5.js +1 -0
- package/dist/m/c-GRXHHFPO.js +1 -0
- package/dist/m/c-HG2C77BM.js +4 -0
- package/dist/m/c-HHGJBU4D.js +1 -0
- package/dist/m/c-HMV6P4RX.js +1 -0
- package/dist/m/c-HMYWIHFG.js +1 -0
- package/dist/m/c-HT2DKBV2.js +1 -0
- package/dist/m/c-I2SCCUEW.js +1 -0
- package/dist/m/{c-TJ6WOJL3.js → c-ICJYHABF.js} +2 -2
- package/dist/m/c-IMZCWYHR.js +7 -0
- package/dist/m/c-IVMD3FEM.js +1 -0
- package/dist/m/{c-WDC5GVKU.js → c-IVTRVENZ.js} +1 -1
- package/dist/m/c-JKZJIUIP.js +1 -0
- package/dist/m/{c-UDUUJPFZ.js → c-JNLX3RJB.js} +1 -1
- package/dist/m/c-K3ADYNLU.js +1 -0
- package/dist/m/c-KGHVFRR3.js +1 -0
- package/dist/m/{c-5QWQ7A5L.js → c-KJJPV77R.js} +1 -1
- package/dist/m/{c-CZUSOB7V.js → c-KNPDOMDY.js} +1 -1
- package/dist/m/{c-EZU56NQV.js → c-KP64LT2A.js} +2 -2
- package/dist/m/c-KQ7E3PYY.js +1 -0
- package/dist/m/c-L3F5ZLIV.js +1 -0
- package/dist/m/{c-3RUU4TPX.js → c-L42OLXWM.js} +3 -2
- package/dist/m/c-L5VFATT6.js +1 -0
- package/dist/m/c-L7JL5TL3.js +3 -0
- package/dist/m/c-LKSL4AUJ.js +1 -0
- package/dist/m/c-M6FNQCTQ.js +1 -0
- package/dist/m/c-MCOOHCSG.js +1 -0
- package/dist/m/c-MHI3GYKF.js +1 -0
- package/dist/m/c-MWXFLW4J.js +1 -0
- package/dist/m/{c-5RJFTLSF.js → c-NEL7HQW3.js} +1 -1
- package/dist/m/{c-DCXOXBF3.js → c-NQJRVJ56.js} +1 -1
- package/dist/m/c-NWX3MFT4.js +1 -0
- package/dist/m/{c-HPURYVZS.js → c-OJYM642Q.js} +1 -1
- package/dist/m/c-OKXI52N7.js +1 -0
- package/dist/m/{c-R3L7ZJGB.js → c-OPLUZR3A.js} +1 -1
- package/dist/m/{c-GSFIGKOP.js → c-OTMKN2OO.js} +1 -1
- package/dist/m/{c-6NC5I2TV.js → c-OYRZ7XG2.js} +1 -1
- package/dist/m/{c-EVCQ3BKA.js → c-P3OFYUMA.js} +1 -1
- package/dist/m/c-PLN76GF3.js +6 -0
- package/dist/m/c-PLP4JKFC.js +1 -0
- package/dist/m/c-Q26MME6N.js +1 -0
- package/dist/m/{c-S7H2DRCB.js → c-Q3XOWZEV.js} +1 -1
- package/dist/m/{c-FRLHSY7E.js → c-QIKT6SKM.js} +1 -1
- package/dist/m/c-QIXT3Q7C.js +28 -0
- package/dist/m/c-QO4HNGCQ.js +1 -0
- package/dist/m/{c-JU3NK25H.js → c-R27DWQKX.js} +1 -1
- package/dist/m/{c-RSMOODDU.js → c-R2XUODVY.js} +1 -1
- package/dist/m/{c-QNKREOL6.js → c-R7LBFMF5.js} +1 -1
- package/dist/m/c-RND3G2S2.js +1 -0
- package/dist/m/{c-Q622NS2Y.js → c-S2U6NJU4.js} +1 -1
- package/dist/m/c-SPZPGD3R.js +1 -0
- package/dist/m/c-SW4YEG2D.js +8 -0
- package/dist/m/c-T637T7SN.js +1 -0
- package/dist/m/c-TJAXGIRV.js +1 -0
- package/dist/m/{c-RYUXUUBX.js → c-TNEM3RNM.js} +1 -1
- package/dist/m/c-TTB5EK4S.js +32 -0
- package/dist/m/c-U7SHNWDH.js +1 -0
- package/dist/m/c-UDHIVLYH.js +20 -0
- package/dist/m/{c-BBVGTQG7.js → c-UGNV5MFB.js} +1 -1
- package/dist/m/{c-QICP2YR5.js → c-UORXLSD3.js} +1 -1
- package/dist/m/c-UPD33ZPE.js +1 -0
- package/dist/m/c-URSMSFCB.js +1 -0
- package/dist/m/c-UY4QX42B.js +1 -0
- package/dist/m/c-V44KWI2L.js +4 -0
- package/dist/m/c-VILENZTD.js +1 -0
- package/dist/m/{c-HDDEEL77.js → c-VJYEVRYK.js} +1 -1
- package/dist/m/{c-SMBZG5X4.js → c-VR2BJ4AN.js} +1 -1
- package/dist/m/c-W4JCVLAB.js +1 -0
- package/dist/m/c-W6DQSHXI.js +135 -0
- package/dist/m/c-WBGWGOM7.js +1 -0
- package/dist/m/{c-4L3BTFRK.js → c-WCXLG3MV.js} +1 -1
- package/dist/m/c-WGW764WX.js +1 -0
- package/dist/m/c-WMRJKQ2V.js +5 -0
- package/dist/m/c-WNTYKCDG.js +1 -0
- package/dist/m/c-WQWIRWO3.js +72 -0
- package/dist/m/c-X5LNHET5.js +1 -0
- package/dist/m/{c-I2ZATQCD.js → c-X5TSXZYJ.js} +2 -2
- package/dist/m/c-XGH4GS2T.js +1 -0
- package/dist/m/c-Y6F2FORL.js +1 -0
- package/dist/m/c-YBKHUAK7.js +10 -0
- package/dist/m/c-YBSS55KU.js +20 -0
- package/dist/m/c-YCLU4UNT.js +2 -0
- package/dist/m/c-YVZ2L7CE.js +1 -0
- package/dist/m/c-ZK4GIZH2.js +1 -0
- package/dist/m/c-ZRXJD4G6.js +1 -0
- package/dist/m/c-ZWBI4HYK.js +1 -0
- package/dist/m/core/{admin-attrs-QHOKPJ7W.js → admin-attrs-YY7T5FGU.js} +1 -1
- package/dist/m/core/autosave-S6OMNDCK.js +1 -0
- package/dist/m/core/conflict-notice-IWHMHH43.js +1 -0
- package/dist/m/core/{host-permission-FNXVEFNE.js → host-permission-6EVJGNPL.js} +1 -1
- package/dist/m/core/is-edit-mode-GCX6767E.js +1 -0
- package/dist/m/core/{persist-JFYRHMRJ.js → persist-UKXV5KLA.js} +1 -1
- package/dist/m/core/recovery-DRFZRJED.js +1 -0
- package/dist/m/core/save-LYY7K6U7.js +1 -0
- package/dist/m/core/save-core-MKRPSEEO.js +1 -0
- package/dist/m/core/snapshot-NU3YM6GR.js +1 -0
- package/dist/m/core/unsaved-warning-7QTKEN7Z.js +1 -0
- package/dist/m/internals/{index-PVXGPUDA.js → index-TGLQINN3.js} +1 -1
- package/dist/m/loader-3BBNZZGM.js +1 -0
- package/dist/m/manifest-26C55578.js +1 -0
- package/dist/m/manifest-edit-4396A009.js +1 -0
- package/dist/m/manifest.json +2111 -1573
- package/dist/m/page/{editor-NNJA4OKD.js → editor-6LRB3GE4.js} +123 -21
- package/dist/m/plugins/ai-edit-UTV7YO2I.js +6 -0
- package/dist/m/plugins/{cms-HP3LSTPN.js → cms-LRAOZHT7.js} +9 -9
- package/dist/m/plugins/{demo-RM5IR6AO.js → demo-OYO5GCAO.js} +1 -1
- package/dist/m/plugins/{derive-UAAA4V4S.js → derive-FRE5JJQE.js} +1 -1
- package/dist/m/plugins/indicator-4VDXWYTD.js +1 -0
- package/dist/m/plugins/live-AD4H6XSL.js +1 -0
- package/dist/m/plugins/menu-M5WW7DBF.js +1 -0
- package/dist/m/plugins/page-MD3T2ZX3.js +2 -0
- package/dist/m/plugins/page-view-PAWVZNFY.js +1 -0
- package/dist/m/plugins/{people-SGPJKANK.js → people-JCHVPF7P.js} +1 -1
- package/dist/m/plugins/quickcrop-BQ5YYFCH.js +26 -0
- package/dist/m/plugins/{richclay-NRQDSWX6.js → richclay-KLGETK77.js} +2 -2
- package/dist/m/plugins/sortable-4S6SQC3D.js +1 -0
- package/dist/m/plugins/source-TJXOIOYY.js +1 -0
- package/dist/m/plugins/undo-NLPGCJQY.js +1 -0
- package/dist/m/plugins/{unsent-HJFLQJ2O.js → unsent-OA4RGBUT.js} +1 -1
- package/dist/m/plugins/upgrade-HSUNDNJD.js +1 -0
- package/dist/m/plugins/{upload-WLYKK6TL.js → upload-NHGJXZK2.js} +1 -1
- package/dist/m/plugins/visitor-drafts-BW43DGY3.js +20 -0
- package/dist/m/plugins/visitor-drafts-owner-7ODQG7JX.js +1 -0
- package/dist/m/plugins/{wire-4N6GYAF6.js → wire-VBWXEXU2.js} +1 -1
- package/dist/m/reader-core-4W2KYEWQ.js +77 -0
- package/dist/m/route/route-62CHXL5B.js +1 -0
- package/dist/m/sync/live-sync-MO5DH33W.js +1 -0
- package/dist/m/ui/index-IVWN2QC4.js +1 -0
- package/dist/m/vendor/sap-core.vendor-RN4ZVY4X.js +6 -0
- package/dist/offline-manifest.json +1367 -930
- package/dist/page/themes/light-1.css +8 -7
- package/dist/server/core.cjs +1440 -288
- package/dist/server/offline-runtime.cjs +63 -63
- package/dist/server/offline-runtime.mjs +63 -63
- package/dist/server/page-compaction-worker.cjs +3710 -0
- package/dist/server/rooms.mjs +5 -0
- package/docs/guide.md +653 -101
- package/entries/sap.js +5 -5
- package/package.json +1 -1
- package/src/core/admin-contenteditable.js +5 -2
- package/src/core/admin-inputs.js +1 -0
- package/src/core/admin-onclick.js +2 -1
- package/src/core/conflict-notice-ui.js +4 -7
- package/src/core/download.js +2 -2
- package/src/core/host-meta.js +38 -0
- package/src/core/is-edit-mode.js +17 -1
- package/src/core/mode-key.js +125 -19
- package/src/core/mode-support.js +5 -0
- package/src/core/mode-transition.js +20 -6
- package/src/core/preserve-authored.js +4 -4
- package/src/core/recovery.js +121 -33
- package/src/core/save-accepted.js +33 -0
- package/src/core/save-core.js +106 -31
- package/src/core/save-job.js +108 -0
- package/src/core/save-prepare.js +53 -0
- package/src/core/save-size-notice.js +3 -1
- package/src/core/save-slices.js +15 -0
- package/src/core/save.js +557 -127
- package/src/core/serialize-steps.js +109 -0
- package/src/core/snapshot.js +170 -39
- package/src/core/source-map.js +83 -38
- package/src/core/stale-host-notice.js +1 -0
- package/src/core/upgrade-run.js +59 -8
- package/src/dom/active-element.js +5 -0
- package/src/lib/assets.js +1 -0
- package/src/lib/slice.js +65 -0
- package/src/lib/uimotion.js +774 -0
- package/src/lib/unsaved-state.js +2 -2
- package/src/live/fast-lane.js +10 -3
- package/src/live/local-lane.js +202 -11
- package/src/live/ops.js +16 -0
- package/src/live/page-compaction.js +162 -0
- package/src/live/server-lane.js +1 -0
- package/src/live/store.js +11 -3
- package/src/loader-logic.js +3 -1
- package/src/loader.js +14 -4
- package/src/page/adopt-boot.js +2 -1
- package/src/page/awareness.js +33 -2
- package/src/page/blocks.js +89 -27
- package/src/page/boot-pack.js +1 -1
- package/src/page/chart-tables.js +21 -3
- package/src/page/chart.js +406 -63
- package/src/page/checklist.js +10 -135
- package/src/page/checkpoint-proof.js +7 -1
- package/src/page/chrome.js +250 -51
- package/src/page/clipboard-shape.js +126 -0
- package/src/page/drag.js +33 -3
- package/src/page/editor.js +136 -25
- package/src/page/export-assets.js +357 -0
- package/src/page/export-html.js +209 -0
- package/src/page/export-markdown.js +361 -0
- package/src/page/features/checklist.js +428 -0
- package/src/page/features/export-html.js +189 -0
- package/src/page/features/export-markdown.js +169 -0
- package/src/page/features/import-markdown.js +349 -0
- package/src/page/features/media.js +61 -5
- package/src/page/features/registry.js +18 -0
- package/src/page/features/table.js +373 -83
- package/src/page/features/title-cover.js +331 -0
- package/src/page/features/title-icon.js +213 -0
- package/src/page/features/toc.js +87 -0
- package/src/page/format-command.js +25 -1
- package/src/page/format-gate.js +68 -18
- package/src/page/format-host.js +17 -0
- package/src/page/format-native-commands.js +21 -5
- package/src/page/image-actions.js +64 -0
- package/src/page/import-markdown.js +262 -0
- package/src/page/items.js +135 -17
- package/src/page/markdown.js +6 -2
- package/src/page/media.js +35 -119
- package/src/page/placeholder.js +14 -1
- package/src/page/projection.js +98 -10
- package/src/page/recover.js +10 -12
- package/src/page/recovery-ui.js +464 -0
- package/src/page/rich/chart.js +63 -7
- package/src/page/save-projection.js +28 -0
- package/src/page/save-states.js +13 -3
- package/src/page/schema.js +19 -3
- package/src/page/table-paste.js +3 -1
- package/src/page/table-style.js +4 -4
- package/src/page/table.js +44 -2
- package/src/page/themes/light-1.css +8 -7
- package/src/page/themes/light-1.source.css +8 -7
- package/src/page/toc-link.js +31 -0
- package/src/page/toc.js +5 -0
- package/src/page/view-hooks.js +38 -12
- package/src/page/view.js +13 -0
- package/src/page/ybind/binding.js +88 -5
- package/src/page/ybind/bridge.js +187 -20
- package/src/page/ybind/containment.js +162 -0
- package/src/page/ybind/doc.js +17 -9
- package/src/page/ybind/file-debt.js +63 -0
- package/src/page/ybind/layout.js +7 -2
- package/src/page/ybind/marks.js +1 -1
- package/src/page/ybind/moves.js +5 -4
- package/src/page/ybind/origins.js +106 -4
- package/src/page/ybind/parts.js +8 -1
- package/src/page/ybind/provider.js +113 -17
- package/src/page/ybind/recovery-proof.js +32 -9
- package/src/page/ybind/recovery-replay.js +558 -15
- package/src/page/ybind/recovery.js +174 -45
- package/src/page/ybind/table.js +67 -11
- package/src/page/ybind/walk-delta.js +258 -0
- package/src/page/ybind/walk.js +54 -29
- package/src/page/ybind/writer.js +1 -1
- package/src/plugins/ai-edit.js +13 -9
- package/src/plugins/indicator-chip.js +91 -0
- package/src/plugins/indicator.js +13 -80
- package/src/plugins/live.js +89 -14
- package/src/plugins/menu/actions.js +196 -0
- package/src/plugins/menu/people.js +279 -0
- package/src/plugins/menu/placement.js +121 -0
- package/src/plugins/menu/preferences.js +9 -0
- package/src/plugins/menu/seo.js +316 -397
- package/src/plugins/menu/style.js +171 -0
- package/src/plugins/menu/variants.js +296 -0
- package/src/plugins/menu.js +186 -183
- package/src/plugins/page-view.js +3 -2
- package/src/plugins/page.js +249 -44
- package/src/plugins/sortable.js +8 -1
- package/src/plugins/source.js +6 -0
- package/src/plugins/upgrade.js +51 -17
- package/src/plugins/upload-ui.js +12 -17
- package/src/plugins/visitor-drafts/bottom-ui.js +12 -4
- package/src/plugins/visitor-drafts/intro-style.js +7 -2
- package/src/plugins/visitor-drafts/intro.js +5 -3
- package/src/plugins/visitor-drafts/owner-chip.js +8 -12
- package/src/plugins/visitor-drafts/prompt.js +11 -9
- package/src/plugins/visitor-drafts/source.js +4 -3
- package/src/plugins/visitor-drafts/store.js +138 -9
- package/src/plugins/visitor-drafts/ui-memory.js +2 -2
- package/src/plugins/visitor-drafts-owner.js +4 -6
- package/src/plugins/visitor-drafts.js +5 -3
- package/src/recovery/store.js +1 -1
- package/src/route/route.js +4 -0
- package/src/server/rooms/index.js +5 -1
- package/src/server/rooms/live-data-rooms.js +302 -50
- package/src/server/rooms/page-compaction-worker.js +12 -0
- package/src/server/rooms/page-compactor.js +92 -0
- package/src/server/rooms/page-room-conformance.js +461 -0
- package/src/source/parse-steps.js +94 -0
- package/src/source/session.js +233 -57
- package/src/sync/editing-sender.js +12 -0
- package/src/sync/live-sync-edit.js +22 -2
- package/src/sync/live-sync.js +150 -23
- package/src/sync/page-relay.js +52 -0
- package/src/sync/presence-store.js +44 -4
- package/src/sync/presence.js +6 -3
- package/src/sync/saved-stream.js +11 -6
- package/src/ui/bevel-controls.js +1 -0
- package/src/ui/bevel-dialog.js +5 -2
- package/src/ui/editor-ui.js +77 -0
- package/src/ui/page-colour.js +281 -0
- package/src/ui/toast.js +16 -7
- package/src/vendor/hyper-html-api-upgrade.vendor.js +2 -2
- package/src/vendor/hyper-undo.vendor.js +1 -1
- package/src/vendor/hypercms.vendor.js +9 -9
- package/src/vendor/parse5.vendor.js +2 -2
- package/src/vendor/sap-core.vendor.js +5 -5
- package/dist/m/c-2SPVQSHC.js +0 -1
- package/dist/m/c-336SGU5W.js +0 -1
- package/dist/m/c-3H6BZDNJ.js +0 -6
- package/dist/m/c-4DFEJ2PO.js +0 -3
- package/dist/m/c-56QRZTZH.js +0 -1
- package/dist/m/c-5J4E7O4Y.js +0 -4
- package/dist/m/c-5LKDWQTL.js +0 -19
- package/dist/m/c-5V3MOVJT.js +0 -1
- package/dist/m/c-6AZOGAPE.js +0 -1
- package/dist/m/c-6CRUUXOX.js +0 -5
- package/dist/m/c-6HMB24EV.js +0 -1
- package/dist/m/c-6KEVEQ7L.js +0 -1
- package/dist/m/c-A3E3NIMC.js +0 -1
- package/dist/m/c-BQ6LOBEE.js +0 -1
- package/dist/m/c-BTDI4Y23.js +0 -1
- package/dist/m/c-CJ3A7ZKE.js +0 -1
- package/dist/m/c-CQHPGKX2.js +0 -1
- package/dist/m/c-DFS4XZJV.js +0 -2
- package/dist/m/c-DNENWY4O.js +0 -1
- package/dist/m/c-E4LIR5X5.js +0 -1
- package/dist/m/c-FDNHHZY5.js +0 -1
- package/dist/m/c-FROJUZMV.js +0 -1
- package/dist/m/c-G2JPKCTP.js +0 -1
- package/dist/m/c-GB6TU7WV.js +0 -1
- package/dist/m/c-GFJJEBTU.js +0 -1
- package/dist/m/c-GJAEGGXY.js +0 -1
- package/dist/m/c-GOWBDM6J.js +0 -1
- package/dist/m/c-HFKUVACH.js +0 -1
- package/dist/m/c-HHDFCNHV.js +0 -1
- package/dist/m/c-IEM4NTCS.js +0 -1
- package/dist/m/c-JDCX26UP.js +0 -25
- package/dist/m/c-KGVGROZT.js +0 -1
- package/dist/m/c-KQ6TYJAP.js +0 -135
- package/dist/m/c-LFPHCXY3.js +0 -1
- package/dist/m/c-LRPUO2MX.js +0 -1
- package/dist/m/c-LWLBHLVC.js +0 -3
- package/dist/m/c-M2L47S2X.js +0 -7
- package/dist/m/c-MFP2ZWD4.js +0 -1
- package/dist/m/c-MKSQ4Y57.js +0 -1
- package/dist/m/c-NUFFJVVJ.js +0 -1
- package/dist/m/c-NUHSPT5P.js +0 -1
- package/dist/m/c-O72HVYSA.js +0 -1
- package/dist/m/c-OIBUTDXI.js +0 -1
- package/dist/m/c-OW5QIGG3.js +0 -1
- package/dist/m/c-PGRCK73F.js +0 -1
- package/dist/m/c-Q7NQD65V.js +0 -1
- package/dist/m/c-QH63LQOS.js +0 -1
- package/dist/m/c-R7IWVINQ.js +0 -1
- package/dist/m/c-RM7J7BV2.js +0 -1
- package/dist/m/c-S4ZEH46B.js +0 -9
- package/dist/m/c-SDAXDM2D.js +0 -1
- package/dist/m/c-SMGU7RT3.js +0 -235
- package/dist/m/c-SPIORRYS.js +0 -1
- package/dist/m/c-SWLRT66R.js +0 -1
- package/dist/m/c-TCJTVAFN.js +0 -1
- package/dist/m/c-TQEMVJCS.js +0 -1
- package/dist/m/c-TYLXX4NF.js +0 -21
- package/dist/m/c-U62OB4BT.js +0 -1
- package/dist/m/c-U6YWUAR3.js +0 -8
- package/dist/m/c-UDCSU5G5.js +0 -2
- package/dist/m/c-VI5RMSVY.js +0 -1
- package/dist/m/c-VRZN56XY.js +0 -1
- package/dist/m/c-W5MCHEGA.js +0 -1
- package/dist/m/c-WNYBLIOZ.js +0 -1
- package/dist/m/c-WWXIDH4O.js +0 -28
- package/dist/m/c-XCNTK76U.js +0 -1
- package/dist/m/c-XMS6DPBR.js +0 -1
- package/dist/m/c-ZUCOKOHT.js +0 -1
- package/dist/m/c-ZVPVXTJF.js +0 -4
- package/dist/m/c-ZVRKXKM7.js +0 -1
- package/dist/m/core/autosave-HRYGUWV6.js +0 -1
- package/dist/m/core/conflict-notice-6MROJN2L.js +0 -1
- package/dist/m/core/is-edit-mode-KTWA3O5N.js +0 -1
- package/dist/m/core/recovery-FLTCC2OC.js +0 -1
- package/dist/m/core/save-HQZ4S667.js +0 -1
- package/dist/m/core/save-core-KGQWFUNH.js +0 -1
- package/dist/m/core/snapshot-EGQZNT45.js +0 -1
- package/dist/m/core/unsaved-warning-IDJLD66H.js +0 -1
- package/dist/m/loader-S4I7GKXR.js +0 -1
- package/dist/m/manifest-4C419D67.js +0 -1
- package/dist/m/manifest-edit-B077F288.js +0 -1
- package/dist/m/plugins/ai-edit-QN4PMMNL.js +0 -6
- package/dist/m/plugins/indicator-URDBBOQO.js +0 -1
- package/dist/m/plugins/live-HFWD4OIH.js +0 -1
- package/dist/m/plugins/menu-3HIEQSI6.js +0 -138
- package/dist/m/plugins/page-JOKB6XPR.js +0 -2
- package/dist/m/plugins/page-view-NXOJHNW6.js +0 -1
- package/dist/m/plugins/quickcrop-QNY3PKLD.js +0 -26
- package/dist/m/plugins/sortable-GWT4PWDV.js +0 -1
- package/dist/m/plugins/source-4ZRSAIYM.js +0 -1
- package/dist/m/plugins/undo-VDA5P7FH.js +0 -1
- package/dist/m/plugins/upgrade-W7FG5VFG.js +0 -1
- package/dist/m/plugins/visitor-drafts-LIDY7NH6.js +0 -21
- package/dist/m/plugins/visitor-drafts-owner-ZXCKBZE7.js +0 -1
- package/dist/m/reader-core-DXYCURFF.js +0 -68
- package/dist/m/route/route-KOCZSEGN.js +0 -1
- package/dist/m/sync/live-sync-J7VMYTKS.js +0 -1
- package/dist/m/ui/index-3JLVI27O.js +0 -1
- package/dist/m/vendor/sap-core.vendor-A7BMUYZT.js +0 -6
- /package/dist/m/{c-FILLTJA7.js → c-QTTJYJMA.js} +0 -0
- /package/dist/m/plugins/{quickcrop-QNY3PKLD.js.LEGAL.txt → quickcrop-BQ5YYFCH.js.LEGAL.txt} +0 -0
- /package/dist/m/plugins/{richclay-NRQDSWX6.js.LEGAL.txt → richclay-KLGETK77.js.LEGAL.txt} +0 -0
|
@@ -20,6 +20,7 @@ import { sameAuthority, hostIdentityOf, pathIdentityOf } from "../../recovery/id
|
|
|
20
20
|
import { sameLineage, sharesAncestry, lineageOf } from "./lineage.js";
|
|
21
21
|
import { canonicalPageBytes, containsUpdate, digestHex, validatePageCheckpoint } from "./recovery-proof.js";
|
|
22
22
|
import { RECOVERY_ORIGIN } from "./provider.js";
|
|
23
|
+
import { registerUnsavedState } from "../../lib/unsaved-state.js";
|
|
23
24
|
|
|
24
25
|
/** The channel this adapter journals and replays: one name, one version, everywhere. */
|
|
25
26
|
export const PAGE_CHANNEL = "page.yjs";
|
|
@@ -193,11 +194,22 @@ export function applyCapsule(doc, parts) {
|
|
|
193
194
|
*
|
|
194
195
|
* Nothing is applied for a capsule that fails admission: a changed authority, a changed
|
|
195
196
|
* 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
|
+
* merge. A capsule with no owned work has nothing to restore and is skipped silently, and
|
|
198
|
+
* so is one from the generation before a save that already holds all its work: that save
|
|
199
|
+
* reached the file after its tab stopped listening for the answer.
|
|
197
200
|
*
|
|
198
201
|
* @param {{records?: Array<Object>, current: {context: Object, state: Object}, into: Object}} options
|
|
199
202
|
* @returns {{replayed: Array<Object>, review: Array<Object>}}
|
|
200
203
|
*/
|
|
204
|
+
function alreadyHeld(entry, current) {
|
|
205
|
+
try {
|
|
206
|
+
const { owned } = accumulateCapsule({ capsule: entry.capsule, batches: entry.batches });
|
|
207
|
+
return owned.length > 0 && containsUpdate(canonicalPageBytes(current.state), owned);
|
|
208
|
+
} catch {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
201
213
|
export function replayJournals({ records = [], current, into } = {}) {
|
|
202
214
|
const replayed = [];
|
|
203
215
|
const review = [];
|
|
@@ -206,8 +218,8 @@ export function replayJournals({ records = [], current, into } = {}) {
|
|
|
206
218
|
try {
|
|
207
219
|
parts = admitCapsule({ capsule: entry.capsule, batches: entry.batches, current });
|
|
208
220
|
} 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" });
|
|
221
|
+
if (error?.code === "nothing-owned" || (error?.code === "foreign-generation" && alreadyHeld(entry, current))) continue;
|
|
222
|
+
review.push({ capsule: entry.capsule, batches: entry.batches, code: error?.code ?? "invalid-capsule", message: error?.message ?? "This journal cannot be replayed" });
|
|
211
223
|
continue;
|
|
212
224
|
}
|
|
213
225
|
applyCapsule(into, parts);
|
|
@@ -244,25 +256,49 @@ export async function pageJournalContext({ authority, generation = null, base =
|
|
|
244
256
|
* host with no people support gives its document to whoever holds it, so the document's
|
|
245
257
|
* own host identity is the principal there.
|
|
246
258
|
*
|
|
259
|
+
* `scope` records how the document was named. A host that gives the document an identity
|
|
260
|
+
* of its own is `"host"`; a host that gives it none, a local file among them, is `"path"`,
|
|
261
|
+
* and then the exact pathname is the whole of what this page's identity is.
|
|
262
|
+
*
|
|
247
263
|
* @param {Object} [options]
|
|
248
264
|
* @param {?Object} [options.meta] - the host's discovery answer
|
|
249
265
|
* @param {?Location} [options.location]
|
|
250
266
|
* @param {?Element} [options.root]
|
|
251
|
-
* @returns {?{host: string, documentId: string, principalId: string}}
|
|
267
|
+
* @returns {?{host: string, documentId: string, principalId: string, scope: "host"|"path"}}
|
|
252
268
|
*/
|
|
253
269
|
export function pageAuthority({ meta = null, location: where = globalThis.location, root = globalThis.document?.documentElement } = {}) {
|
|
254
270
|
const host = where?.origin ?? "";
|
|
255
271
|
if (!host) return null;
|
|
256
|
-
const
|
|
272
|
+
const hostIdentity = hostIdentityOf(root);
|
|
273
|
+
const scope = hostIdentity ? "host" : "path";
|
|
274
|
+
const documentId = hostIdentity?.identity ?? pathIdentityOf(where?.pathname ?? "/").identity;
|
|
257
275
|
if (!documentId) return null;
|
|
258
276
|
const people = meta?.document?.people;
|
|
259
277
|
const principalId = people && typeof people === "object" ? people.me?.id ?? null : documentId;
|
|
260
278
|
if (!principalId) return null;
|
|
261
|
-
return { host, documentId, principalId };
|
|
279
|
+
return { host, documentId, principalId, scope };
|
|
262
280
|
}
|
|
263
281
|
|
|
264
282
|
const journalKey = (id) => (typeof id === "string" ? id : JSON.stringify(id));
|
|
265
283
|
|
|
284
|
+
// Is `next` a later accepted state than `base`? A state vector only grows, so one that
|
|
285
|
+
// is behind the base anywhere is an older save that finished late.
|
|
286
|
+
function newer(next, base) {
|
|
287
|
+
if (!base.length) return true;
|
|
288
|
+
const ahead = Y.decodeStateVector(Y.encodeStateVectorFromUpdateV2(base));
|
|
289
|
+
const behind = Y.decodeStateVector(Y.encodeStateVectorFromUpdateV2(next));
|
|
290
|
+
for (const [client, clock] of ahead) if ((behind.get(client) ?? 0) < clock) return false;
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// The part of a capsule the new base does not already hold: work a compaction merged in
|
|
295
|
+
// before the save that covered it reads as nothing once that save is the base.
|
|
296
|
+
function unsaved(base, part) {
|
|
297
|
+
const bytes = bytesOf(part);
|
|
298
|
+
if (!bytes || !bytes.length) return new Uint8Array(0);
|
|
299
|
+
return containsUpdate(base, bytes) ? new Uint8Array(0) : bytes;
|
|
300
|
+
}
|
|
301
|
+
|
|
266
302
|
/**
|
|
267
303
|
* The accepted page state an episode starts from, as the bytes its capsule keeps.
|
|
268
304
|
*
|
|
@@ -315,13 +351,17 @@ export function pageChannel(service) {
|
|
|
315
351
|
export function createPageJournal({ channel, key, context, base = null, onChange = () => {} }) {
|
|
316
352
|
const records = new Map();
|
|
317
353
|
const waiting = [];
|
|
354
|
+
const awaiting = [];
|
|
318
355
|
const inflight = new Set();
|
|
319
|
-
|
|
356
|
+
let bytesOfBase = bytesOf(base) ?? new Uint8Array(0);
|
|
320
357
|
let resolved = null;
|
|
321
358
|
let failure = null;
|
|
322
359
|
let batches = 0;
|
|
323
360
|
let batchBytes = 0;
|
|
324
361
|
let episode = false;
|
|
362
|
+
let carryBase = true;
|
|
363
|
+
let holding = false;
|
|
364
|
+
let rebasing = Promise.resolve(null);
|
|
325
365
|
let compacting = null;
|
|
326
366
|
let closed = false;
|
|
327
367
|
|
|
@@ -329,16 +369,38 @@ export function createPageJournal({ channel, key, context, base = null, onChange
|
|
|
329
369
|
channel,
|
|
330
370
|
key,
|
|
331
371
|
onChange,
|
|
332
|
-
context:
|
|
372
|
+
// The context of the base the journal holds now: an accepted save moves it.
|
|
373
|
+
context: () => ready.then(() => resolved),
|
|
333
374
|
local: (bytes) => send(bytes, "local"),
|
|
334
375
|
// Only while an episode is open: a peer's updates on their own are not this tab's work
|
|
335
376
|
// to keep, and a page that has journaled nothing owns nothing.
|
|
336
377
|
dependency: (bytes) => { if (episode) send(bytes, "dependency"); },
|
|
337
378
|
records: () => [...records.values()],
|
|
338
379
|
names,
|
|
339
|
-
|
|
380
|
+
// The next record this journal commits, or null when the store refuses one or the
|
|
381
|
+
// session closes: the destination a copy names is only real once it is on this device.
|
|
382
|
+
next: () => new Promise((resolve) => { awaiting.push(resolve); }),
|
|
383
|
+
// Does the work this journal holds carry exactly this block's text? The episode's own
|
|
384
|
+
// base is here, so the answer needs no store and no page.
|
|
385
|
+
covers: (blockId, text) => {
|
|
386
|
+
const owned = [...records.values()].map((record) => record.update).filter((update) => update && update.byteLength);
|
|
387
|
+
const capsule = {
|
|
388
|
+
kind: "binary",
|
|
389
|
+
version: 1,
|
|
390
|
+
id: "journal",
|
|
391
|
+
revision: 0,
|
|
392
|
+
payload: {
|
|
393
|
+
base: bytesOfBase,
|
|
394
|
+
dependencies: new Uint8Array(0),
|
|
395
|
+
owned: owned.length ? Y.mergeUpdatesV2(owned) : new Uint8Array(0),
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
return coversText([capsule], blockId, text);
|
|
399
|
+
},
|
|
400
|
+
pending: () => inflight.size + (holding ? waiting.length : 0) + (failure ? 1 : 0),
|
|
340
401
|
get failure() { return failure; },
|
|
341
402
|
retire,
|
|
403
|
+
rebase,
|
|
342
404
|
compact,
|
|
343
405
|
// An acknowledgement is metadata: it says a room holds the bytes and retires nothing.
|
|
344
406
|
acknowledged: () => {
|
|
@@ -348,6 +410,7 @@ export function createPageJournal({ channel, key, context, base = null, onChange
|
|
|
348
410
|
close: () => {
|
|
349
411
|
closed = true;
|
|
350
412
|
waiting.length = 0;
|
|
413
|
+
settle(null);
|
|
351
414
|
},
|
|
352
415
|
};
|
|
353
416
|
|
|
@@ -367,8 +430,61 @@ export function createPageJournal({ channel, key, context, base = null, onChange
|
|
|
367
430
|
return resolved;
|
|
368
431
|
});
|
|
369
432
|
|
|
433
|
+
function settle(value) {
|
|
434
|
+
for (const resolve of awaiting.splice(0)) resolve(value);
|
|
435
|
+
}
|
|
436
|
+
|
|
370
437
|
function retire(list) {
|
|
371
|
-
for (const item of list || [])
|
|
438
|
+
for (const item of list || []) {
|
|
439
|
+
const key = journalKey(item?.id);
|
|
440
|
+
const record = records.get(key);
|
|
441
|
+
if (!record) continue;
|
|
442
|
+
records.delete(key);
|
|
443
|
+
batches = Math.max(0, batches - 1);
|
|
444
|
+
batchBytes = Math.max(0, batchBytes - (record.update?.byteLength ?? 0));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// An accepted save is the base the next replay starts from: the page reopens on it,
|
|
449
|
+
// and the batches it retired are no longer there to build on. Rebases run one at a
|
|
450
|
+
// time, appends wait for them, and a save older than the base already held is ignored.
|
|
451
|
+
function rebase(state) {
|
|
452
|
+
rebasing = rebasing.then(() => moveBase(bytesOf(state))).catch(() => null);
|
|
453
|
+
return rebasing;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function moveBase(next) {
|
|
457
|
+
if (closed || !resolved || !next || !next.length || !newer(next, bytesOfBase)) return null;
|
|
458
|
+
holding = true;
|
|
459
|
+
try {
|
|
460
|
+
while (inflight.size && !closed) await journal.next();
|
|
461
|
+
if (closed) return null;
|
|
462
|
+
const moved = typeof context === "function" ? await context(next) : context;
|
|
463
|
+
if (!moved || !moved.authority) return null;
|
|
464
|
+
if (episode) {
|
|
465
|
+
const answer = typeof channel.rebase === "function"
|
|
466
|
+
? await channel.rebase({
|
|
467
|
+
key,
|
|
468
|
+
context: moved,
|
|
469
|
+
rebuild: (payload) => ({
|
|
470
|
+
base: next,
|
|
471
|
+
dependencies: unsaved(next, payload.dependencies),
|
|
472
|
+
owned: unsaved(next, payload.owned),
|
|
473
|
+
}),
|
|
474
|
+
})
|
|
475
|
+
: null;
|
|
476
|
+
// A writer whose capsule never reached the store starts its next one on this base.
|
|
477
|
+
if (answer?.code === "missing") carryBase = true;
|
|
478
|
+
else if (!answer?.ok) return null;
|
|
479
|
+
}
|
|
480
|
+
bytesOfBase = next;
|
|
481
|
+
resolved = moved;
|
|
482
|
+
for (const record of records.values()) record.context = moved;
|
|
483
|
+
return moved;
|
|
484
|
+
} finally {
|
|
485
|
+
holding = false;
|
|
486
|
+
if (resolved) for (const run of waiting.splice(0)) run(resolved);
|
|
487
|
+
}
|
|
372
488
|
}
|
|
373
489
|
|
|
374
490
|
function names() {
|
|
@@ -383,8 +499,6 @@ export function createPageJournal({ channel, key, context, base = null, onChange
|
|
|
383
499
|
.then((receipt) => {
|
|
384
500
|
if (!receipt?.ok) return receipt;
|
|
385
501
|
retire(receipt.retired);
|
|
386
|
-
batches = Math.max(0, batches - (receipt.retired?.length ?? 0));
|
|
387
|
-
if (!batches) batchBytes = 0;
|
|
388
502
|
return receipt;
|
|
389
503
|
})
|
|
390
504
|
.catch(() => null)
|
|
@@ -402,6 +516,7 @@ export function createPageJournal({ channel, key, context, base = null, onChange
|
|
|
402
516
|
answer = channel.append({ key, context, update: bytes, origin, base: first ? bytesOfBase : null });
|
|
403
517
|
} catch (_) {
|
|
404
518
|
failure = "error";
|
|
519
|
+
settle(null);
|
|
405
520
|
journal.onChange();
|
|
406
521
|
return;
|
|
407
522
|
}
|
|
@@ -412,6 +527,7 @@ export function createPageJournal({ channel, key, context, base = null, onChange
|
|
|
412
527
|
inflight.delete(token);
|
|
413
528
|
if (!receipt || receipt.ok !== true) {
|
|
414
529
|
failure = receipt?.code ?? "error";
|
|
530
|
+
settle(null);
|
|
415
531
|
journal.onChange();
|
|
416
532
|
return;
|
|
417
533
|
}
|
|
@@ -422,22 +538,29 @@ export function createPageJournal({ channel, key, context, base = null, onChange
|
|
|
422
538
|
batchBytes += bytes.byteLength;
|
|
423
539
|
}
|
|
424
540
|
journal.onChange();
|
|
541
|
+
settle({ id: receipt.id, revision: receipt.revision });
|
|
425
542
|
if (batches >= COMPACT_BATCHES || batchBytes >= COMPACT_BYTES) compact();
|
|
426
543
|
},
|
|
427
544
|
() => {
|
|
428
545
|
inflight.delete(token);
|
|
429
546
|
failure = "error";
|
|
547
|
+
settle(null);
|
|
430
548
|
journal.onChange();
|
|
431
549
|
},
|
|
432
550
|
);
|
|
433
551
|
}
|
|
434
552
|
|
|
553
|
+
function dispatch(context, bytes, origin) {
|
|
554
|
+
const first = carryBase;
|
|
555
|
+
carryBase = false;
|
|
556
|
+
write(context, bytes, origin, first);
|
|
557
|
+
}
|
|
558
|
+
|
|
435
559
|
function send(bytes, origin) {
|
|
436
560
|
if (closed || !bytes || !bytes.byteLength || typeof channel.append !== "function") return;
|
|
437
|
-
const first = !episode;
|
|
438
561
|
episode = true;
|
|
439
|
-
if (resolved)
|
|
440
|
-
else waiting.push((context) =>
|
|
562
|
+
if (resolved && !holding) dispatch(resolved, bytes, origin);
|
|
563
|
+
else waiting.push((context) => dispatch(context, bytes, origin));
|
|
441
564
|
}
|
|
442
565
|
|
|
443
566
|
return journal;
|
|
@@ -472,6 +595,90 @@ export function openPageJournal({ meta = null, service = null, base = null, onCh
|
|
|
472
595
|
});
|
|
473
596
|
}
|
|
474
597
|
|
|
598
|
+
/**
|
|
599
|
+
* Where this tab's typing lives, and the wait that lets an exit leave edit.
|
|
600
|
+
*
|
|
601
|
+
* The answer is only ever as strong as the evidence behind it: a write whose transaction
|
|
602
|
+
* has not committed is Pending, committed records are Stored, a room's confirmation makes
|
|
603
|
+
* them Shared, and only a checkpoint that covers them makes the page Saved. The same
|
|
604
|
+
* handle is the exit's barrier for the journal: every append already handed over commits,
|
|
605
|
+
* and the store's own writes are down, before it answers. A journal that cannot write at
|
|
606
|
+
* all holds nothing and says so once, as Unavailable, so a person is never stranded in
|
|
607
|
+
* edit by a device that has stopped taking bytes.
|
|
608
|
+
*
|
|
609
|
+
* @param {Object} options
|
|
610
|
+
* @param {Object} options.journal - this session's page journal
|
|
611
|
+
* @param {?Object} [options.store] - the live page store, for its confirmations
|
|
612
|
+
* @param {Function} [options.reduce] - the precedence reducer, (facts) => state or null
|
|
613
|
+
* @param {Function} [options.onState] - (state) for every state the facts change to
|
|
614
|
+
* @param {Function} [options.onUnavailable] - (code) when the journal cannot write
|
|
615
|
+
* @returns {{checkpoint: Function, state: Function, dispose: Function}}
|
|
616
|
+
*/
|
|
617
|
+
export function watchJournal({ journal, store = null, reduce = () => null, onState = () => {}, onUnavailable = () => {} } = {}) {
|
|
618
|
+
if (!journal) return { checkpoint() {}, state: () => null, dispose() {} };
|
|
619
|
+
const seen = { acknowledged: false, checkpointed: false };
|
|
620
|
+
let last = null;
|
|
621
|
+
const state = () => {
|
|
622
|
+
const failure = journal.failure ?? null;
|
|
623
|
+
const records = journal.records().length;
|
|
624
|
+
return reduce({
|
|
625
|
+
unavailable: Boolean(failure),
|
|
626
|
+
pending: failure ? 0 : journal.pending(),
|
|
627
|
+
journaled: seen.acknowledged ? 0 : records,
|
|
628
|
+
acknowledged: seen.acknowledged ? records : 0,
|
|
629
|
+
checkpointed: seen.checkpointed && !records,
|
|
630
|
+
});
|
|
631
|
+
};
|
|
632
|
+
const publish = () => {
|
|
633
|
+
const next = state();
|
|
634
|
+
if (!next || next === last) return last;
|
|
635
|
+
last = next;
|
|
636
|
+
if (next === "unavailable") onUnavailable(journal.failure);
|
|
637
|
+
else onState(next);
|
|
638
|
+
return next;
|
|
639
|
+
};
|
|
640
|
+
// The binding owns this callback and hears every append and refusal through it.
|
|
641
|
+
const inner = journal.onChange;
|
|
642
|
+
journal.onChange = () => { inner?.(); publish(); };
|
|
643
|
+
// Work handed over now is work no room has seen and no checkpoint covers.
|
|
644
|
+
const offLocal = store?.onLocal?.(() => {
|
|
645
|
+
seen.acknowledged = false;
|
|
646
|
+
seen.checkpointed = false;
|
|
647
|
+
publish();
|
|
648
|
+
});
|
|
649
|
+
const offConfirm = store?.onConfirm?.(() => {
|
|
650
|
+
seen.acknowledged = true;
|
|
651
|
+
publish();
|
|
652
|
+
});
|
|
653
|
+
const blocked = () => Boolean(journal.failure);
|
|
654
|
+
const source = registerUnsavedState({
|
|
655
|
+
id: "page:journal",
|
|
656
|
+
isPending: () => !blocked() && journal.pending() > 0,
|
|
657
|
+
flush: async () => {
|
|
658
|
+
if (blocked()) return { ok: true };
|
|
659
|
+
while (!blocked() && journal.pending() > 0) {
|
|
660
|
+
if (!(await journal.next())) break;
|
|
661
|
+
}
|
|
662
|
+
await Promise.resolve(journal.channel?.flush?.()).catch(() => null);
|
|
663
|
+
return { ok: blocked() || journal.pending() === 0 };
|
|
664
|
+
},
|
|
665
|
+
});
|
|
666
|
+
publish();
|
|
667
|
+
return {
|
|
668
|
+
checkpoint() {
|
|
669
|
+
seen.checkpointed = true;
|
|
670
|
+
publish();
|
|
671
|
+
},
|
|
672
|
+
state,
|
|
673
|
+
dispose() {
|
|
674
|
+
journal.onChange = inner;
|
|
675
|
+
offLocal?.();
|
|
676
|
+
offConfirm?.();
|
|
677
|
+
source.dispose();
|
|
678
|
+
},
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
|
|
475
682
|
/**
|
|
476
683
|
* Replay this page's journal once, after the authority snapshot is adopted.
|
|
477
684
|
*
|
|
@@ -486,3 +693,339 @@ export async function replayInto({ journal, state, into } = {}) {
|
|
|
486
693
|
if (!listed?.ok) return { replayed: [], review: [] };
|
|
487
694
|
return replayJournals({ records: listed.records, current: { context, state }, into });
|
|
488
695
|
}
|
|
696
|
+
|
|
697
|
+
const posOf = (block) => (typeof block?.pos === "string" ? block.pos : "");
|
|
698
|
+
const idsInOrder = (blocks) =>
|
|
699
|
+
Object.keys(blocks).sort((a, b) => {
|
|
700
|
+
const left = posOf(blocks[a]);
|
|
701
|
+
const right = posOf(blocks[b]);
|
|
702
|
+
if (left !== right) return left < right ? -1 : 1;
|
|
703
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
// The text of one block, cells included: what a person reads in a review, and what an
|
|
707
|
+
// insert-as-text offers back.
|
|
708
|
+
function blockText(block) {
|
|
709
|
+
if (typeof block?.c === "string") return block.c;
|
|
710
|
+
if (!block?.rows) return "";
|
|
711
|
+
const parts = [];
|
|
712
|
+
for (const row of Object.values(block.rows)) {
|
|
713
|
+
for (const cell of Object.values(row?.cells ?? {})) {
|
|
714
|
+
const text = typeof cell === "string" ? cell : typeof cell?.c === "string" ? cell.c : "";
|
|
715
|
+
if (text) parts.push(text);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return parts.join(" ");
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* The page a set of records reconstructs, block by block.
|
|
723
|
+
*
|
|
724
|
+
* Records arrive in the store's shape — capsules with their batches — or as the updates
|
|
725
|
+
* a live journal holds, and both read back as one page: they share a lineage, and Yjs
|
|
726
|
+
* merges them by operation identity.
|
|
727
|
+
*
|
|
728
|
+
* @param {Array<Object>} [records]
|
|
729
|
+
* @returns {?Object} the block model, by block id, or null when it cannot be decoded
|
|
730
|
+
*/
|
|
731
|
+
function pageOf(records = []) {
|
|
732
|
+
const doc = new Y.Doc();
|
|
733
|
+
try {
|
|
734
|
+
for (const entry of capsulesOf(records)) {
|
|
735
|
+
let parts;
|
|
736
|
+
try {
|
|
737
|
+
parts = accumulateCapsule(entry);
|
|
738
|
+
} catch (_) {
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
for (const bytes of [parts.base, parts.dependencies, parts.owned]) if (bytes.length) Y.applyUpdateV2(doc, bytes, NET);
|
|
742
|
+
}
|
|
743
|
+
for (const record of records || []) {
|
|
744
|
+
const update = bytesOf(record?.update);
|
|
745
|
+
if (update && update.length) Y.applyUpdateV2(doc, update, NET);
|
|
746
|
+
}
|
|
747
|
+
return blocksFromYValue({ v: 1, g: 0, y: { seed: toBase64(Y.encodeStateAsUpdateV2(doc)) } });
|
|
748
|
+
} catch (_) {
|
|
749
|
+
return null;
|
|
750
|
+
} finally {
|
|
751
|
+
doc.destroy();
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* The readable page a capsule reconstructs: one line per block, in the page's own order.
|
|
757
|
+
*
|
|
758
|
+
* A capsule that cannot be decoded at all reads as no text rather than as an error: a
|
|
759
|
+
* review still says what it can, and nothing here decides whether it may be replayed.
|
|
760
|
+
*
|
|
761
|
+
* @param {{capsule: Object, batches?: Array<Object>}} options
|
|
762
|
+
* @returns {string}
|
|
763
|
+
*/
|
|
764
|
+
export function reconstructText({ capsule, batches = [] } = {}) {
|
|
765
|
+
let blocks;
|
|
766
|
+
try {
|
|
767
|
+
blocks = reconstructPage({ capsule, batches });
|
|
768
|
+
} catch (_) {
|
|
769
|
+
return "";
|
|
770
|
+
}
|
|
771
|
+
const lines = [];
|
|
772
|
+
for (const id of idsInOrder(blocks)) {
|
|
773
|
+
const text = blockText(blocks[id]);
|
|
774
|
+
if (text) lines.push(text);
|
|
775
|
+
}
|
|
776
|
+
return lines.join("\n");
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* The review rows a page draws for the journals it may not replay.
|
|
781
|
+
*
|
|
782
|
+
* Each row keeps the capsule it came from, so the person can read it, download it or
|
|
783
|
+
* copy its content, and nothing in a row is applied to the page.
|
|
784
|
+
*
|
|
785
|
+
* @param {Array<{capsule: Object, batches?: Array<Object>, code: string, message: string}>} [review]
|
|
786
|
+
* @returns {Array<Object>}
|
|
787
|
+
*/
|
|
788
|
+
export function describeReview(review = []) {
|
|
789
|
+
return (review || []).map((entry) => ({
|
|
790
|
+
id: entry?.capsule?.id,
|
|
791
|
+
revision: entry?.capsule?.revision ?? 0,
|
|
792
|
+
code: entry?.code ?? "invalid-capsule",
|
|
793
|
+
message: entry?.message ?? "This journal cannot be replayed",
|
|
794
|
+
text: reconstructText({ capsule: entry?.capsule, batches: entry?.batches ?? [] }),
|
|
795
|
+
capsule: entry?.capsule ?? null,
|
|
796
|
+
batches: entry?.batches ?? [],
|
|
797
|
+
}));
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Whether a capsule's own blocks reconstruct to exactly this text.
|
|
802
|
+
*
|
|
803
|
+
* Composition end asks this before retiring a shadow: an update the journal holds is a
|
|
804
|
+
* transaction, and only a transaction carrying the same visible text means the typing
|
|
805
|
+
* reached the document.
|
|
806
|
+
*
|
|
807
|
+
* @param {Array<Object>} records - the journal's records
|
|
808
|
+
* @param {?string} blockId
|
|
809
|
+
* @param {string} text
|
|
810
|
+
* @returns {boolean}
|
|
811
|
+
*/
|
|
812
|
+
export function coversText(records = [], blockId = null, text = "") {
|
|
813
|
+
const id = blockId == null ? "" : String(blockId);
|
|
814
|
+
const want = typeof text === "string" ? text : "";
|
|
815
|
+
if (!id || !want) return false;
|
|
816
|
+
const blocks = pageOf(records);
|
|
817
|
+
return Boolean(blocks && blocks[id] && blockText(blocks[id]) === want);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/** The channel that records where a reviewed capsule was copied to: one name, one version. */
|
|
821
|
+
export const COPY_CHANNEL = "page.copy";
|
|
822
|
+
export const COPY_VERSION = 1;
|
|
823
|
+
|
|
824
|
+
// One channel per service, registered the first time something is actually copied.
|
|
825
|
+
const copyChannels = new WeakMap();
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* The copy-marker channel of one recovery service.
|
|
829
|
+
*
|
|
830
|
+
* @param {Object} service - `clay.recovery`
|
|
831
|
+
* @returns {?Object} the channel api
|
|
832
|
+
*/
|
|
833
|
+
export function pageCopyChannel(service) {
|
|
834
|
+
if (!service) return null;
|
|
835
|
+
const existing = copyChannels.get(service);
|
|
836
|
+
if (existing) return existing;
|
|
837
|
+
const channel = service.register({ channel: COPY_CHANNEL, version: COPY_VERSION });
|
|
838
|
+
copyChannels.set(service, channel);
|
|
839
|
+
return channel;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const copyKey = (source) => `copy:${source?.key ?? ""}:${identityKey(source?.id)}`;
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* The transfer marker naming one capsule's copy, if this store has one.
|
|
846
|
+
*
|
|
847
|
+
* @param {{records?: Array<Object>, source: {key: string, id: *}}} options
|
|
848
|
+
* @returns {?{record: Object, source: Object, destination: Object}}
|
|
849
|
+
*/
|
|
850
|
+
export function markerFor({ records = [], source = null } = {}) {
|
|
851
|
+
if (!source || source.id === undefined) return null;
|
|
852
|
+
for (const record of records || []) {
|
|
853
|
+
const payload = record?.payload;
|
|
854
|
+
if (!payload?.source || !payload?.destination) continue;
|
|
855
|
+
if (payload.source.key !== source.key) continue;
|
|
856
|
+
if (identityKey(payload.source.id) !== identityKey(source.id)) continue;
|
|
857
|
+
return { record, source: payload.source, destination: payload.destination };
|
|
858
|
+
}
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* What one marker says about the copy it names.
|
|
864
|
+
*
|
|
865
|
+
* "copied" while the destination record is still in the journal: the copy is on this
|
|
866
|
+
* device and nothing has proved it saved. "covered" once the destination is gone from
|
|
867
|
+
* the journal, which only a checkpoint (or a dismissed destination) can do.
|
|
868
|
+
*
|
|
869
|
+
* @param {{marker: ?Object, records?: Array<Object>}} options
|
|
870
|
+
* @returns {string} "open", "copied" or "covered"
|
|
871
|
+
*/
|
|
872
|
+
export function copyState({ marker = null, records = [] } = {}) {
|
|
873
|
+
if (!marker?.destination) return "open";
|
|
874
|
+
const id = marker.destination.id;
|
|
875
|
+
const kept = (records || []).some((record) => record && identityKey(record.id) === identityKey(id));
|
|
876
|
+
return kept ? "copied" : "covered";
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Insert a reviewed capsule's content as a copy, with fresh block ids.
|
|
881
|
+
*
|
|
882
|
+
* The content goes in through `insert`, which is the page's own document command and
|
|
883
|
+
* names the destination record it committed; nothing of the capsule's Yjs operations is
|
|
884
|
+
* merged into this page, so a changed lineage is never merged blindly. The source
|
|
885
|
+
* capsule is not retired here: it survives until a checkpoint covers the destination,
|
|
886
|
+
* and the marker written last says where that destination is. A copy already on this
|
|
887
|
+
* device is never made twice, and a refused or failed insert leaves the source exactly
|
|
888
|
+
* where it was.
|
|
889
|
+
*
|
|
890
|
+
* @param {Object} options
|
|
891
|
+
* @param {Object} options.channel - the `page.yjs` channel api
|
|
892
|
+
* @param {Object} options.copies - the copy-marker channel api
|
|
893
|
+
* @param {{key: string, id: *, revision: number}} options.source
|
|
894
|
+
* @param {Function} options.insert - () => Promise<{key: string, id: *, revision: number}>
|
|
895
|
+
* @returns {Promise<{ok: boolean, code?: string, destination?: Object, marker?: Object}>}
|
|
896
|
+
*/
|
|
897
|
+
export async function recoverAsCopy({ channel, copies, source, insert } = {}) {
|
|
898
|
+
if (!channel || !copies || !source || typeof insert !== "function") return { ok: false, code: "unavailable" };
|
|
899
|
+
let list;
|
|
900
|
+
let marks;
|
|
901
|
+
try {
|
|
902
|
+
[list, marks] = await Promise.all([channel.list(), copies.list()]);
|
|
903
|
+
} catch (_) {
|
|
904
|
+
return { ok: false, code: "unavailable" };
|
|
905
|
+
}
|
|
906
|
+
const marker = markerFor({ records: marks?.ok ? marks.records : [], source });
|
|
907
|
+
if (marker) {
|
|
908
|
+
const state = copyState({ marker, records: list?.ok ? list.records : [] });
|
|
909
|
+
if (state === "copied") return { ok: false, code: "already-copied", marker };
|
|
910
|
+
if (state === "covered") return { ok: false, code: "covered", marker };
|
|
911
|
+
}
|
|
912
|
+
let destination;
|
|
913
|
+
try {
|
|
914
|
+
destination = await insert();
|
|
915
|
+
} catch (_) {
|
|
916
|
+
return { ok: false, code: "insert-failed" };
|
|
917
|
+
}
|
|
918
|
+
if (!destination || destination.revision === undefined || destination.id === undefined) return { ok: false, code: "insert-failed" };
|
|
919
|
+
copies.stage({
|
|
920
|
+
key: copyKey(source),
|
|
921
|
+
kind: "text",
|
|
922
|
+
payload: {
|
|
923
|
+
source: { key: source.key ?? "", id: source.id, revision: source.revision ?? 0 },
|
|
924
|
+
destination: { key: destination.key ?? "", id: destination.id, revision: destination.revision },
|
|
925
|
+
},
|
|
926
|
+
});
|
|
927
|
+
const written = await copies.flush();
|
|
928
|
+
if (!written?.ok) return { ok: false, code: written?.code || "error", destination };
|
|
929
|
+
return { ok: true, destination };
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/** The channel a composition's visible text is kept on while it is not a transaction. */
|
|
933
|
+
export const IME_CHANNEL = "page.ime";
|
|
934
|
+
export const IME_VERSION = 1;
|
|
935
|
+
|
|
936
|
+
const IME_PREFIX = "composing:";
|
|
937
|
+
|
|
938
|
+
const imeChannels = new WeakMap();
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* The composition-shadow channel of one recovery service.
|
|
942
|
+
*
|
|
943
|
+
* @param {Object} service - `clay.recovery`
|
|
944
|
+
* @returns {?Object} the channel api
|
|
945
|
+
*/
|
|
946
|
+
export function pageImeChannel(service) {
|
|
947
|
+
if (!service) return null;
|
|
948
|
+
const existing = imeChannels.get(service);
|
|
949
|
+
if (existing) return existing;
|
|
950
|
+
const channel = service.register({ channel: IME_CHANNEL, version: IME_VERSION });
|
|
951
|
+
imeChannels.set(service, channel);
|
|
952
|
+
return channel;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* The shadow one composing block's visible text is kept under.
|
|
957
|
+
*
|
|
958
|
+
* Composition is never forced into a transaction: while it is active the block's text is
|
|
959
|
+
* staged on a text channel, so a crash offers it as text. Composition end retires the
|
|
960
|
+
* shadow only when a journaled update carries the same text — `covered`, or answered
|
|
961
|
+
* from the records given — and anything else is kept and offered, because that typing
|
|
962
|
+
* never reached the document.
|
|
963
|
+
*
|
|
964
|
+
* @param {{channel: ?Object, key: string}} options
|
|
965
|
+
* @returns {?Object}
|
|
966
|
+
*/
|
|
967
|
+
export function openImeShadow({ channel = null, key = "" } = {}) {
|
|
968
|
+
if (!channel) return null;
|
|
969
|
+
|
|
970
|
+
async function drop(blockId) {
|
|
971
|
+
let listed;
|
|
972
|
+
try {
|
|
973
|
+
listed = await channel.list();
|
|
974
|
+
} catch (_) {
|
|
975
|
+
return 0;
|
|
976
|
+
}
|
|
977
|
+
if (!listed?.ok) return 0;
|
|
978
|
+
let retired = 0;
|
|
979
|
+
for (const record of listed.records) {
|
|
980
|
+
if (record?.payload?.blockId !== blockId) continue;
|
|
981
|
+
try {
|
|
982
|
+
const receipt = await channel.dismiss(record.id, { revision: record.revision });
|
|
983
|
+
if (receipt?.ok) retired += 1;
|
|
984
|
+
} catch (_) {}
|
|
985
|
+
}
|
|
986
|
+
return retired;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
return {
|
|
990
|
+
key,
|
|
991
|
+
channel,
|
|
992
|
+
keep({ blockId = null, text = "" } = {}) {
|
|
993
|
+
const id = blockId == null ? "" : String(blockId);
|
|
994
|
+
const value = typeof text === "string" ? text : "";
|
|
995
|
+
if (!id || !value) return null;
|
|
996
|
+
return channel.stage({ key: `${IME_PREFIX}${id}`, kind: "text", payload: { blockId: id, text: value } });
|
|
997
|
+
},
|
|
998
|
+
async clear({ blockId = null, text = "", covered = null, records = [] } = {}) {
|
|
999
|
+
const id = blockId == null ? "" : String(blockId);
|
|
1000
|
+
if (!id) return { ok: false, code: "invalid" };
|
|
1001
|
+
const carried = typeof covered === "boolean" ? covered : coversText(records, id, text);
|
|
1002
|
+
if (!carried) return { ok: false, code: "not-covered" };
|
|
1003
|
+
const retired = await drop(id);
|
|
1004
|
+
return retired ? { ok: true, retired } : { ok: false, code: "not-covered" };
|
|
1005
|
+
},
|
|
1006
|
+
async list() {
|
|
1007
|
+
return channel.list();
|
|
1008
|
+
},
|
|
1009
|
+
dispose() {
|
|
1010
|
+
return channel.flush();
|
|
1011
|
+
},
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* The composition shadows a page's store holds as reviewable text.
|
|
1017
|
+
*
|
|
1018
|
+
* @param {Array<Object>} [records] - the composition channel's records
|
|
1019
|
+
* @returns {Array<{id: *, revision: number, blockId: string, text: string}>}
|
|
1020
|
+
*/
|
|
1021
|
+
export function imeEntries(records = []) {
|
|
1022
|
+
const out = [];
|
|
1023
|
+
for (const record of records || []) {
|
|
1024
|
+
if (!record || record.supported === false) continue;
|
|
1025
|
+
const payload = record.payload;
|
|
1026
|
+
if (!payload || typeof payload.text !== "string" || !payload.text) continue;
|
|
1027
|
+
if (typeof payload.blockId !== "string" || !payload.blockId) continue;
|
|
1028
|
+
out.push({ id: record.id, revision: record.revision, blockId: payload.blockId, text: payload.text });
|
|
1029
|
+
}
|
|
1030
|
+
return out;
|
|
1031
|
+
}
|