@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
|
@@ -34,6 +34,15 @@ const boundaryRecord = (value) => {
|
|
|
34
34
|
|
|
35
35
|
const sameBoundary = (a, b) => a.blockId === b.blockId && a.kind === b.kind && a.recoveryOf === b.recoveryOf;
|
|
36
36
|
|
|
37
|
+
// The keys walkFlow reads boundary and alias positions from: any key that parses to
|
|
38
|
+
// ["boundary" | "alias", client, clock], whatever its value.
|
|
39
|
+
const markerKey = (key) => {
|
|
40
|
+
let parts;
|
|
41
|
+
try { parts = JSON.parse(key); } catch { return null; }
|
|
42
|
+
if (!Array.isArray(parts) || parts.length !== 3 || (parts[0] !== "boundary" && parts[0] !== "alias")) return null;
|
|
43
|
+
return isClock(parts[1]) && isClock(parts[2]) ? parts : null;
|
|
44
|
+
};
|
|
45
|
+
|
|
37
46
|
const recordKey = (key) => {
|
|
38
47
|
if (typeof key !== "string" || key.charCodeAt(0) !== 91) return null;
|
|
39
48
|
let parts;
|
|
@@ -46,6 +55,7 @@ const recordKey = (key) => {
|
|
|
46
55
|
export function createOriginIndex(origins) {
|
|
47
56
|
const spans = new Map();
|
|
48
57
|
const aliases = new Map();
|
|
58
|
+
const aliasClocks = new Map();
|
|
49
59
|
const boundaries = new Map();
|
|
50
60
|
|
|
51
61
|
const lower = (list, clock) => {
|
|
@@ -53,13 +63,56 @@ export function createOriginIndex(origins) {
|
|
|
53
63
|
while (lo < hi) { const mid = (lo + hi) >> 1; if (list[mid].clock < clock) lo = mid + 1; else hi = mid; }
|
|
54
64
|
return lo;
|
|
55
65
|
};
|
|
66
|
+
const lowerClock = (list, clock) => {
|
|
67
|
+
let lo = 0, hi = list.length;
|
|
68
|
+
while (lo < hi) { const mid = (lo + hi) >> 1; if (list[mid] < clock) lo = mid + 1; else hi = mid; }
|
|
69
|
+
return lo;
|
|
70
|
+
};
|
|
71
|
+
const markAlias = (client, clock, present) => {
|
|
72
|
+
const list = aliasClocks.get(client) ?? [];
|
|
73
|
+
const at = lowerClock(list, clock);
|
|
74
|
+
const there = list[at] === clock;
|
|
75
|
+
if (present && !there) list.splice(at, 0, clock);
|
|
76
|
+
else if (!present && there) list.splice(at, 1);
|
|
77
|
+
if (list.length) aliasClocks.set(client, list);
|
|
78
|
+
else aliasClocks.delete(client);
|
|
79
|
+
};
|
|
80
|
+
// Boundary and alias keys in the map's key order (Yjs keeps a key where it was first
|
|
81
|
+
// written), and each client's clocks that have one, sorted, for walkFlow.
|
|
82
|
+
const markerRecords = [];
|
|
83
|
+
const markerEntries = new Map();
|
|
84
|
+
const markerCounts = new Map();
|
|
85
|
+
const markerClocks = new Map();
|
|
86
|
+
const trackMarker = (key, live) => {
|
|
87
|
+
let entry = markerEntries.get(key);
|
|
88
|
+
if (entry === undefined) {
|
|
89
|
+
const parts = markerKey(key);
|
|
90
|
+
entry = parts ? { kind: parts[0], client: parts[1], clock: parts[2], live: false } : null;
|
|
91
|
+
markerEntries.set(key, entry);
|
|
92
|
+
if (entry) markerRecords.push(entry);
|
|
93
|
+
}
|
|
94
|
+
if (!entry || entry.live === live) return;
|
|
95
|
+
entry.live = live;
|
|
96
|
+
const counts = markerCounts.get(entry.client) ?? markerCounts.set(entry.client, new Map()).get(entry.client);
|
|
97
|
+
const list = markerClocks.get(entry.client) ?? markerClocks.set(entry.client, []).get(entry.client);
|
|
98
|
+
const n = (counts.get(entry.clock) ?? 0) + (live ? 1 : -1);
|
|
99
|
+
if (n) counts.set(entry.clock, n);
|
|
100
|
+
else counts.delete(entry.clock);
|
|
101
|
+
const at = lowerClock(list, entry.clock);
|
|
102
|
+
if (live && n === 1) list.splice(at, 0, entry.clock);
|
|
103
|
+
else if (!live && n === 0) list.splice(at, 1);
|
|
104
|
+
if (!list.length) { markerClocks.delete(entry.client); markerCounts.delete(entry.client); }
|
|
105
|
+
};
|
|
56
106
|
const apply = (key, value) => {
|
|
107
|
+
trackMarker(key, value !== undefined);
|
|
57
108
|
const parts = recordKey(key);
|
|
58
109
|
if (!parts) return;
|
|
59
110
|
const [kind, client, clock] = parts;
|
|
60
111
|
if (kind === "alias") {
|
|
61
|
-
|
|
112
|
+
const valid = Boolean(value && isClock(value.sourceClient) && isClock(value.sourceClock));
|
|
113
|
+
if (valid) aliases.set(idKey(client, clock), { client: value.sourceClient, clock: value.sourceClock });
|
|
62
114
|
else aliases.delete(idKey(client, clock));
|
|
115
|
+
markAlias(client, clock, valid);
|
|
63
116
|
return;
|
|
64
117
|
}
|
|
65
118
|
if (kind === "boundary") {
|
|
@@ -77,12 +130,17 @@ export function createOriginIndex(origins) {
|
|
|
77
130
|
if (list.length) spans.set(client, list);
|
|
78
131
|
else spans.delete(client);
|
|
79
132
|
};
|
|
80
|
-
for (const
|
|
133
|
+
for (const key of origins._map.keys()) apply(key, origins.get(key));
|
|
81
134
|
const observer = (event) => { for (const key of event.keysChanged) apply(key, origins.get(key)); };
|
|
82
135
|
origins.observe(observer);
|
|
83
136
|
|
|
137
|
+
const hasAlias = (client, clock) => {
|
|
138
|
+
const list = aliasClocks.get(client);
|
|
139
|
+
return list !== undefined && list[lowerClock(list, clock)] === clock;
|
|
140
|
+
};
|
|
141
|
+
|
|
84
142
|
const resolve = (id) => {
|
|
85
|
-
const alias = aliases.get(idKey(id.client, id.clock));
|
|
143
|
+
const alias = hasAlias(id.client, id.clock) ? aliases.get(idKey(id.client, id.clock)) : undefined;
|
|
86
144
|
if (alias) return { client: alias.client, clock: alias.clock };
|
|
87
145
|
const list = spans.get(id.client);
|
|
88
146
|
if (!list) return { client: id.client, clock: id.clock };
|
|
@@ -95,8 +153,45 @@ export function createOriginIndex(origins) {
|
|
|
95
153
|
return { client: record.sourceClient, clock: record.sourceClock + offset };
|
|
96
154
|
};
|
|
97
155
|
|
|
156
|
+
const resolveRange = (client, clock, length) => {
|
|
157
|
+
const out = [];
|
|
158
|
+
const push = (sourceClient, sourceClock, size) => {
|
|
159
|
+
const last = out[out.length - 1];
|
|
160
|
+
if (last && last.client === sourceClient && last.clock + last.length === sourceClock) last.length += size;
|
|
161
|
+
else out.push({ client: sourceClient, clock: sourceClock, length: size });
|
|
162
|
+
};
|
|
163
|
+
const list = spans.get(client) ?? [];
|
|
164
|
+
const marks = aliasClocks.get(client) ?? [];
|
|
165
|
+
const end = clock + length;
|
|
166
|
+
let s = lower(list, clock + 1) - 1, a = lowerClock(marks, clock), at = clock;
|
|
167
|
+
while (at < end) {
|
|
168
|
+
if (a < marks.length && marks[a] === at) {
|
|
169
|
+
const target = aliases.get(idKey(client, at));
|
|
170
|
+
push(target.client, target.clock, 1);
|
|
171
|
+
at += 1;
|
|
172
|
+
a += 1;
|
|
173
|
+
} else {
|
|
174
|
+
let stop = end;
|
|
175
|
+
if (a < marks.length && marks[a] < stop) stop = marks[a];
|
|
176
|
+
if (s + 1 < list.length && list[s + 1].clock < stop) stop = list[s + 1].clock;
|
|
177
|
+
const span = s >= 0 ? list[s] : null;
|
|
178
|
+
if (span && at < span.clock + span.length) {
|
|
179
|
+
stop = Math.min(stop, span.clock + span.length);
|
|
180
|
+
push(span.sourceClient, span.sourceClock + at - span.clock, stop - at);
|
|
181
|
+
} else push(client, at, stop - at);
|
|
182
|
+
at = stop;
|
|
183
|
+
}
|
|
184
|
+
while (s + 1 < list.length && list[s + 1].clock <= at) s += 1;
|
|
185
|
+
}
|
|
186
|
+
return out;
|
|
187
|
+
};
|
|
188
|
+
|
|
98
189
|
const boundaryOf = (id) => {
|
|
99
190
|
if (!id || !isClock(id.client) || !isClock(id.clock)) return null;
|
|
191
|
+
if (!hasAlias(id.client, id.clock)) {
|
|
192
|
+
const record = boundaries.get(idKey(id.client, id.clock));
|
|
193
|
+
return record ? { ...record } : null;
|
|
194
|
+
}
|
|
100
195
|
let key = idKey(id.client, id.clock);
|
|
101
196
|
const seen = new Set();
|
|
102
197
|
while (!seen.has(key)) {
|
|
@@ -164,8 +259,15 @@ export function createOriginIndex(origins) {
|
|
|
164
259
|
if (!previous || !sameBoundary(previous, record)) throw conflicting(key);
|
|
165
260
|
};
|
|
166
261
|
|
|
262
|
+
// null while a transaction that wrote origin records has not settled: the observer has
|
|
263
|
+
// not seen those keys yet, and walkFlow then reads the map's keys itself.
|
|
264
|
+
const markers = () => {
|
|
265
|
+
for (const t of origins.doc?._transactionCleanups ?? []) if (t.changed.has(origins)) return null;
|
|
266
|
+
return { records: markerRecords, clocks: markerClocks };
|
|
267
|
+
};
|
|
268
|
+
|
|
167
269
|
return {
|
|
168
|
-
origins, resolve, recordSpans, recordAlias, recordBoundary, boundaryOf,
|
|
270
|
+
origins, resolve, resolveRange, recordSpans, recordAlias, recordBoundary, boundaryOf, markers,
|
|
169
271
|
get size() { return origins.size; },
|
|
170
272
|
dispose() { origins.unobserve(observer); },
|
|
171
273
|
};
|
package/src/page/ybind/parts.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
export const PART_KEY = /^(.+)\.(\d+)\.(\d+)$/;
|
|
2
|
-
|
|
2
|
+
// A regexp group per quad keeps backtrack state per quad and overflows on a page of a few MB.
|
|
3
|
+
const NOT_BASE64 = /[^A-Za-z0-9+/]/;
|
|
4
|
+
export function isBase64(text) {
|
|
5
|
+
const pad = text.endsWith("==") ? 2 : text.endsWith("=") ? 1 : 0;
|
|
6
|
+
const body = text.length - pad;
|
|
7
|
+
if (NOT_BASE64.test(pad ? text.slice(0, body) : text)) return false;
|
|
8
|
+
return pad ? body % 4 + pad === 4 : body % 4 !== 1;
|
|
9
|
+
}
|
|
3
10
|
const PART_CHARS = /^[A-Za-z0-9+/=\t\n\f\r ]*$/;
|
|
4
11
|
|
|
5
12
|
export function entriesOf(y) {
|
|
@@ -2,8 +2,9 @@ import * as Y from "../../vendor/yjs.vendor.js";
|
|
|
2
2
|
import { toBase64, fromBase64 } from "../../vendor/yjs.vendor.js";
|
|
3
3
|
import { NET } from "./layout.js";
|
|
4
4
|
import { ORIGINS_ORIGIN } from "./origins.js";
|
|
5
|
-
import { PART_KEY,
|
|
5
|
+
import { PART_KEY, isBase64, entriesOf, captureYValue } from "./parts.js";
|
|
6
6
|
import { containsUpdate } from "./recovery-proof.js";
|
|
7
|
+
import { rangesOf, unite, within, docRanges } from "./containment.js";
|
|
7
8
|
|
|
8
9
|
export { entriesOf } from "./parts.js";
|
|
9
10
|
|
|
@@ -63,7 +64,7 @@ export function docFromValue(value, { strict = false } = {}) {
|
|
|
63
64
|
try {
|
|
64
65
|
for (const [key, value] of entries) {
|
|
65
66
|
const text = strict ? value.replace(/[\t\n\f\r ]/g, "") : value;
|
|
66
|
-
if (strict && !
|
|
67
|
+
if (strict && !isBase64(text)) throw new YDecodeError("invalid-update", `Invalid base64 in Yjs update: ${key}`);
|
|
67
68
|
Y.applyUpdateV2(d, dec(text), NET);
|
|
68
69
|
}
|
|
69
70
|
if (strict && (d.store.pendingStructs || d.store.pendingDs)) {
|
|
@@ -84,7 +85,7 @@ const utf8Bytes = (text) => (encoder ? encoder.encode(text).length : text.length
|
|
|
84
85
|
const sameDeletes = (a, b) => Y.equalDeleteSets(Y.createDeleteSetFromStructStore(a.store), Y.createDeleteSetFromStructStore(b.store));
|
|
85
86
|
const svEqual = (a, b) => { if (a.size !== b.size) return false; for (const [c, n] of a) if (b.get(c) !== n) return false; return true; };
|
|
86
87
|
|
|
87
|
-
export function createYProvider({ store, ydoc, compact: limits = {}, setTimer = setTimeout, clearTimer = clearTimeout, onStopped = () => {}, stats = { applied: 0, offered: 0, compactions: 0 }, journal = null }) {
|
|
88
|
+
export function createYProvider({ store, ydoc, compact: limits = {}, setTimer = setTimeout, clearTimer = clearTimeout, onStopped = () => {}, stats = { applied: 0, offered: 0, compactions: 0 }, journal = null, onLocalUpdate = () => {} }) {
|
|
88
89
|
const initial = entriesOf(store.value.y);
|
|
89
90
|
const known = new Set();
|
|
90
91
|
// Set when this tab's page can no longer reach the room: "lineage" when the room holds
|
|
@@ -99,8 +100,14 @@ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer =
|
|
|
99
100
|
let canCompact = !initial.some(([k]) => k[0] === "m");
|
|
100
101
|
let counter = 0;
|
|
101
102
|
const me = ydoc.clientID.toString(36);
|
|
102
|
-
const
|
|
103
|
+
const inst = Math.floor(Math.random() * 36 ** 6).toString(36);
|
|
104
|
+
let mode = store.room?.compaction?.v === 1 ? "host" : "legacy";
|
|
105
|
+
const nextKey = (p) => (mode === "host" ? `${p}${me}-${inst}-${(counter++).toString(36)}` : `${p}${me}-${(counter++).toString(36)}`);
|
|
103
106
|
let timer = null;
|
|
107
|
+
let outbox = [];
|
|
108
|
+
// A refusal took back writes of this tab's that later updates build on: until a reconcile
|
|
109
|
+
// puts them back, nothing newer goes out.
|
|
110
|
+
let refusedGap = false;
|
|
104
111
|
const generation = store.value.g;
|
|
105
112
|
// Keys this tab wrote itself: the updates and offers the room has not confirmed yet.
|
|
106
113
|
const own = new Set();
|
|
@@ -117,15 +124,50 @@ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer =
|
|
|
117
124
|
const parts = partsOf(key, enc(bytes));
|
|
118
125
|
store.handle.update((data) => { for (const [k, v] of parts) data.y[k] = v; });
|
|
119
126
|
};
|
|
127
|
+
const putWhole = (key, bytes) => {
|
|
128
|
+
known.add(key);
|
|
129
|
+
own.add(key);
|
|
130
|
+
const text = enc(bytes);
|
|
131
|
+
store.handle.update((data) => { data.y[key] = text; });
|
|
132
|
+
};
|
|
133
|
+
const isOwnYOp = (op) => {
|
|
134
|
+
const path = op.path;
|
|
135
|
+
if (!Array.isArray(path) || path.length !== 2 || path[0] !== "y") return true;
|
|
136
|
+
return own.has(path[1]);
|
|
137
|
+
};
|
|
138
|
+
const mineInFlight = () => store.pending.some((op) => isOwnYOp(op));
|
|
139
|
+
// Host mode orders this tab's writes only while the store is in the room: at save
|
|
140
|
+
// speed nothing confirms a write until the save lands, so updates go out at once.
|
|
141
|
+
const hosted = () => mode === "host" && store.handle.status?.mode !== "file";
|
|
142
|
+
const flushOutbox = () => {
|
|
143
|
+
if (!outbox.length) return;
|
|
144
|
+
const update = outbox.length === 1 ? outbox[0] : Y.mergeUpdatesV2(outbox);
|
|
145
|
+
outbox = [];
|
|
146
|
+
putWhole(nextKey("u"), update);
|
|
147
|
+
};
|
|
148
|
+
const offer = () => {
|
|
149
|
+
if (!hosted() || stopped || mineInFlight()) return;
|
|
150
|
+
if (refusedGap) {
|
|
151
|
+
refusedGap = false;
|
|
152
|
+
if (reconcile()) return;
|
|
153
|
+
}
|
|
154
|
+
if (!outbox.length) return;
|
|
155
|
+
const update = outbox.length === 1 ? outbox[0] : Y.mergeUpdatesV2(outbox);
|
|
156
|
+
outbox = [];
|
|
157
|
+
putWhole(nextKey("u"), update);
|
|
158
|
+
};
|
|
120
159
|
|
|
121
160
|
for (const [k] of initial) known.add(k);
|
|
122
161
|
|
|
123
162
|
// Ahead of the stop guards, and of the room's own delivery: work this tab can no longer
|
|
124
163
|
// offer, and work a refused offer took back, is still work a crash must not lose.
|
|
125
164
|
const onUpdate = (u, origin) => {
|
|
165
|
+
if (origin !== NET && origin !== ORIGINS_ORIGIN) onLocalUpdate(u);
|
|
126
166
|
if (origin !== NET && origin !== ORIGINS_ORIGIN && origin !== RECOVERY_ORIGIN) journal?.local(u);
|
|
127
167
|
if (stopped || origin === NET || origin === ORIGINS_ORIGIN) return;
|
|
128
|
-
|
|
168
|
+
if (hosted()) { outbox.push(u); offer(); }
|
|
169
|
+
else if (mode === "host") { outbox.push(u); flushOutbox(); }
|
|
170
|
+
else put(nextKey("u"), u);
|
|
129
171
|
};
|
|
130
172
|
ydoc.on("updateV2", onUpdate);
|
|
131
173
|
|
|
@@ -151,15 +193,24 @@ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer =
|
|
|
151
193
|
timer = null;
|
|
152
194
|
const full = wantReconcile;
|
|
153
195
|
if (full) reconcile();
|
|
154
|
-
maybeCompact();
|
|
196
|
+
if (mode === "legacy") maybeCompact();
|
|
155
197
|
}, 0);
|
|
156
198
|
};
|
|
157
199
|
|
|
158
|
-
const settle = () => { reconcile(); maybeCompact(); };
|
|
200
|
+
const settle = () => { reconcile(); if (mode === "legacy") maybeCompact(); };
|
|
201
|
+
|
|
202
|
+
// The room's entries already hold every item and deletion of this tab's document, read
|
|
203
|
+
// from each entry's cached ranges: nothing to offer, and no copy of the room is built.
|
|
204
|
+
const roomHolds = (y) => {
|
|
205
|
+
const mine = docRanges(ydoc);
|
|
206
|
+
return mine.complete && within(unite(entriesOf(y ?? {}).map(([k, v]) => rangesFor(k, v))), mine);
|
|
207
|
+
};
|
|
159
208
|
|
|
160
209
|
function reconcile() {
|
|
161
210
|
if (stopped) return false;
|
|
211
|
+
if (hosted() && mineInFlight()) return false;
|
|
162
212
|
const y = store.value.y;
|
|
213
|
+
if (roomHolds(y)) { wantReconcile = false; reconcilePending = false; return false; }
|
|
163
214
|
const scratch = new Y.Doc();
|
|
164
215
|
for (const [, v] of entriesOf(y)) Y.applyUpdateV2(scratch, dec(v), NET);
|
|
165
216
|
const sv = Y.encodeStateVector(scratch);
|
|
@@ -169,7 +220,8 @@ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer =
|
|
|
169
220
|
const diff = Y.encodeStateAsUpdateV2(ydoc, sv);
|
|
170
221
|
scratch.destroy();
|
|
171
222
|
stats.offered++;
|
|
172
|
-
|
|
223
|
+
if (mode === "host") { outbox = []; putWhole(nextKey("r"), diff); }
|
|
224
|
+
else put(nextKey("r"), diff);
|
|
173
225
|
wantReconcile = false;
|
|
174
226
|
reconcilePending = false;
|
|
175
227
|
return true;
|
|
@@ -182,24 +234,38 @@ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer =
|
|
|
182
234
|
let quietUntil = 0;
|
|
183
235
|
const COVER_MAX = 256;
|
|
184
236
|
const coverCache = new Map();
|
|
237
|
+
const META_MAX = 256;
|
|
238
|
+
const metas = new Map();
|
|
185
239
|
const drop = (keys) => store.handle.update((data) => { for (const k of keys) delete data.y[k]; });
|
|
186
240
|
const keysOf = (auth, bases) => Object.keys(auth).filter((k) => bases.has(baseOf(k)));
|
|
187
241
|
// A key is retired only while what this tab shows under it is still what the proof was
|
|
188
242
|
// made over: a late write under the same key keeps its own entry.
|
|
189
243
|
const retire = (auth, keys) => { const visible = store.value.y ?? {}; drop(keys.filter((k) => k in visible && visible[k] === auth[k])); };
|
|
190
244
|
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
245
|
+
// The items and deletions one entry holds, read once per value.
|
|
246
|
+
const rangesFor = (key, value) => {
|
|
247
|
+
const meta = metas.get(key);
|
|
248
|
+
if (meta && meta.value === value) return meta.ranges;
|
|
249
|
+
if (metas.size >= META_MAX) metas.delete(metas.keys().next().value);
|
|
250
|
+
const ranges = rangesOf(dec(value));
|
|
251
|
+
metas.set(key, { value, ranges });
|
|
252
|
+
return ranges;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// `known` and the entry memo keep live references only: the authoritative entries, by
|
|
256
|
+
// their whole keys, this tab's own unconfirmed writes and the fold in progress. Pruned
|
|
257
|
+
// after a rebase, a fold and a refusal. A key that is forgotten can be decoded again;
|
|
258
|
+
// Yjs is idempotent.
|
|
194
259
|
function prune() {
|
|
195
260
|
const auth = store.authoritative.y ?? {};
|
|
196
261
|
const visible = store.value.y ?? {};
|
|
197
262
|
for (const k of own) if (!(k in visible)) own.delete(k);
|
|
198
|
-
const held = fold ? [...fold.parts.map(([k]) => k), ...fold.inputs.keys()] : [];
|
|
199
|
-
const retained = new Set([...Object.keys(auth), ...own, ...held]);
|
|
200
|
-
for (const key of known) if (!retained.has(key)) known.delete(key);
|
|
201
263
|
const values = new Map(entriesOf(auth));
|
|
264
|
+
const held = fold ? [fold.key, ...fold.parts.map(([k]) => k), ...fold.inputs.keys()] : [];
|
|
265
|
+
const retained = new Set([...Object.keys(auth), ...values.keys(), ...own, ...held]);
|
|
266
|
+
for (const key of known) if (!retained.has(key)) known.delete(key);
|
|
202
267
|
for (const [key, proof] of coverCache) if (!retained.has(key) || values.get(key) !== proof.value) coverCache.delete(key);
|
|
268
|
+
for (const [key, meta] of metas) if (!retained.has(key) || (values.has(key) && values.get(key) !== meta.value)) metas.delete(key);
|
|
203
269
|
}
|
|
204
270
|
|
|
205
271
|
// Whether a fold's merged bytes already carry this input: merging it in again changes
|
|
@@ -240,7 +306,15 @@ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer =
|
|
|
240
306
|
prune();
|
|
241
307
|
return true;
|
|
242
308
|
}
|
|
243
|
-
if (store.pending.length
|
|
309
|
+
if (store.pending.length) return false;
|
|
310
|
+
if (!stillHeld(auth, active) || !coversInputs(active)) {
|
|
311
|
+
// A fold that can never speak for its inputs is re-planned, never held.
|
|
312
|
+
fold = null;
|
|
313
|
+
if (arrived.length) retire(auth, arrived.map(([k]) => k));
|
|
314
|
+
quietUntil = entriesOf(auth).length + compact.keys;
|
|
315
|
+
prune();
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
244
318
|
fold = null;
|
|
245
319
|
retire(auth, keysOf(auth, active.inputs));
|
|
246
320
|
prune();
|
|
@@ -306,7 +380,7 @@ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer =
|
|
|
306
380
|
// with this tab's document is a page rebuilt from another article while the tab was away.
|
|
307
381
|
const clientsOf = (y) => {
|
|
308
382
|
const clients = new Set();
|
|
309
|
-
for (const [, v] of entriesOf(y ?? {})) for (const c of
|
|
383
|
+
for (const [k, v] of entriesOf(y ?? {})) for (const [c, spans] of rangesFor(k, v).items) if (spans[0][0] === 0) clients.add(c);
|
|
310
384
|
return clients;
|
|
311
385
|
};
|
|
312
386
|
// A page whose article has no blocks seeds with no client at all; it still shares its
|
|
@@ -321,6 +395,18 @@ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer =
|
|
|
321
395
|
|
|
322
396
|
const off = store.onRebase((ev) => {
|
|
323
397
|
if (stopped) return;
|
|
398
|
+
if (ev.room) {
|
|
399
|
+
const was = mode;
|
|
400
|
+
mode = ev.room.compaction?.v === 1 ? "host" : "legacy";
|
|
401
|
+
// A room that stopped compacting confirms nothing in host order: what host mode
|
|
402
|
+
// was holding back goes out as ordinary updates.
|
|
403
|
+
if (was === "host" && mode === "legacy" && outbox.length) setTimer(() => {
|
|
404
|
+
if (stopped || mode !== "legacy" || !outbox.length) return;
|
|
405
|
+
const update = outbox.length === 1 ? outbox[0] : Y.mergeUpdatesV2(outbox);
|
|
406
|
+
outbox = [];
|
|
407
|
+
put(nextKey("u"), update);
|
|
408
|
+
}, 0);
|
|
409
|
+
}
|
|
324
410
|
const ops = ev.source === "remote" && ev.frame ? ev.frame.ops : null;
|
|
325
411
|
const plain = ops && ops.every((op) => op.path[0] === "y" && op.path.length === 2 && !PART_KEY.test(op.path[1]));
|
|
326
412
|
if (!plain && ev.cause !== "refused" && !sharesLineage(ev.next.y)) {
|
|
@@ -334,6 +420,7 @@ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer =
|
|
|
334
420
|
}
|
|
335
421
|
prune();
|
|
336
422
|
schedule(ev.source !== "remote" || !ops, ev.source !== "remote");
|
|
423
|
+
offer();
|
|
337
424
|
});
|
|
338
425
|
|
|
339
426
|
// A page update the room refused as too large is never offered again: the difference would
|
|
@@ -342,6 +429,12 @@ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer =
|
|
|
342
429
|
// hold whole, and the room's page stays one a checkpoint can render.
|
|
343
430
|
const offDelivery = store.onDelivery((event) => {
|
|
344
431
|
if (event.phase !== "refused") return;
|
|
432
|
+
if (mode === "host") {
|
|
433
|
+
if (event.code === "page-full" || event.code === "compaction-failed") { stop(event.code); return; }
|
|
434
|
+
refusedGap = true;
|
|
435
|
+
schedule(true, false);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
345
438
|
if (event.code !== "too-large" || stopped) { prune(); return; }
|
|
346
439
|
stop("too-large");
|
|
347
440
|
const auth = store.authoritative.y ?? {};
|
|
@@ -352,7 +445,10 @@ export function createYProvider({ store, ydoc, compact: limits = {}, setTimer =
|
|
|
352
445
|
prune();
|
|
353
446
|
});
|
|
354
447
|
|
|
448
|
+
const offConfirm = store.onConfirm(() => { offer(); });
|
|
449
|
+
|
|
355
450
|
return { known, stats, settle, reconcile, get reconcilePending() { return reconcilePending; }, get stopped() { return stopped; },
|
|
451
|
+
get pending() { return outbox.length > 0; },
|
|
356
452
|
// Test-only snapshot of the counters and cache sizes; no mutable set leaves the provider.
|
|
357
|
-
_diagnostics: () => ({ known: known.size, cover: coverCache.size, fold: fold ? fold.parts.length : 0, generation }), compact: maybeCompact, dispose() { off(); offDelivery(); ydoc.off("updateV2", onUpdate); if (timer) clearTimer(timer); } };
|
|
453
|
+
_diagnostics: () => ({ known: known.size, cover: coverCache.size, fold: fold ? fold.parts.length : 0, generation }), compact: maybeCompact, dispose() { off(); offDelivery(); offConfirm(); ydoc.off("updateV2", onUpdate); if (timer) clearTimer(timer); } };
|
|
358
454
|
}
|
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
* state and the receipt arrive in the proof itself.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { encodeStateAsUpdateV2
|
|
17
|
+
import { encodeStateAsUpdateV2 } from "../../vendor/yjs.vendor.js";
|
|
18
18
|
import { docFromValue } from "./provider.js";
|
|
19
|
+
import { indexOf, indexContains } from "./containment.js";
|
|
19
20
|
|
|
20
21
|
export class PageProofError extends Error {
|
|
21
22
|
constructor(code, message) {
|
|
@@ -40,17 +41,19 @@ const copyState = (state) => ({ v: state.v, g: state.g, y: { ...(state.y ?? {})
|
|
|
40
41
|
export const equalBytes = (a, b) => a.length === b.length && a.every((value, index) => value === b[index]);
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
|
-
* Whether
|
|
44
|
+
* Whether applying `update` on top of `checkpoint` adds nothing: no insertion, no
|
|
45
|
+
* deletion. Compared by structure (the items and deletions the checkpoint holds), since
|
|
46
|
+
* merged bytes are not stable across merge orders. An update whose dependencies the
|
|
47
|
+
* checkpoint lacks is not contained: Yjs would hold it pending rather than refuse it. The
|
|
48
|
+
* checkpoint is integrated once for as long as its bytes are held, and each update after
|
|
49
|
+
* it is read, not applied.
|
|
44
50
|
*
|
|
45
51
|
* @param {Uint8Array} checkpoint - canonical or merged Yjs V2 update bytes
|
|
46
52
|
* @param {Uint8Array} update - the bytes to look for
|
|
47
|
-
* @param {Object} [Yjs] - the vendored Yjs module
|
|
48
53
|
* @returns {boolean}
|
|
49
54
|
*/
|
|
50
|
-
export function containsUpdate(checkpoint, update
|
|
51
|
-
|
|
52
|
-
const canonical = merge([checkpoint]);
|
|
53
|
-
return equalBytes(canonical, merge([canonical, update]));
|
|
55
|
+
export function containsUpdate(checkpoint, update) {
|
|
56
|
+
return indexContains(indexOf(checkpoint), update);
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
export async function digestHex(bytes) {
|
|
@@ -58,27 +61,47 @@ export async function digestHex(bytes) {
|
|
|
58
61
|
return [...new Uint8Array(digest)].map((value) => value.toString(16).padStart(2, "0")).join("");
|
|
59
62
|
}
|
|
60
63
|
|
|
64
|
+
// The last few states decoded, by their entries: a proof, its validation, the file debt
|
|
65
|
+
// and the journal's rebase all ask for the same state's bytes after one accepted save.
|
|
66
|
+
const CANONICAL_MAX = 4;
|
|
67
|
+
const canonical = [];
|
|
68
|
+
const sameState = (memo, state) => {
|
|
69
|
+
if (memo.v !== state.v || memo.g !== state.g) return false;
|
|
70
|
+
const keys = Object.keys(state.y ?? {});
|
|
71
|
+
if (keys.length !== memo.y.size) return false;
|
|
72
|
+
for (const key of keys) if (memo.y.get(key) !== state.y[key]) return false;
|
|
73
|
+
return true;
|
|
74
|
+
};
|
|
75
|
+
|
|
61
76
|
/**
|
|
62
77
|
* The accepted page state as one canonical update, decoded strictly.
|
|
63
78
|
*
|
|
64
79
|
* 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.
|
|
80
|
+
* a checkpoint nobody can decode proves nothing about a journaled record. The same state
|
|
81
|
+
* answers with the same bytes, which no caller may change.
|
|
66
82
|
*
|
|
67
83
|
* @param {Object} state - a version 1 page value
|
|
68
84
|
* @returns {Uint8Array}
|
|
69
85
|
*/
|
|
70
86
|
export function canonicalPageBytes(state) {
|
|
87
|
+
const hit = state && typeof state === "object" && state.y && typeof state.y === "object" ? canonical.find((memo) => sameState(memo, state)) : null;
|
|
88
|
+
if (hit) return hit.bytes;
|
|
71
89
|
let doc;
|
|
72
90
|
try {
|
|
73
91
|
doc = docFromValue(state, { strict: true });
|
|
74
92
|
} catch (error) {
|
|
75
93
|
fail("invalid-state", `The accepted page state does not decode: ${error.message}`);
|
|
76
94
|
}
|
|
95
|
+
let bytes;
|
|
77
96
|
try {
|
|
78
|
-
|
|
97
|
+
bytes = encodeStateAsUpdateV2(doc);
|
|
98
|
+
indexOf(bytes, doc);
|
|
79
99
|
} finally {
|
|
80
100
|
doc.destroy();
|
|
81
101
|
}
|
|
102
|
+
if (canonical.length >= CANONICAL_MAX) canonical.shift();
|
|
103
|
+
canonical.push({ v: state.v, g: state.g, y: new Map(Object.entries(state.y)), bytes });
|
|
104
|
+
return bytes;
|
|
82
105
|
}
|
|
83
106
|
|
|
84
107
|
// The same answer `sameAuthority` gives in recovery/identity.js, written here because the
|