@panphora/clayjs 1.17.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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-F449FF98.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
package/src/lib/unsaved-state.js
CHANGED
|
@@ -52,9 +52,9 @@ export function hasUnsavedState() {
|
|
|
52
52
|
// Ask every pending source that can finish on its own (a checkpoint, a queued
|
|
53
53
|
// upload) to do so, and wait. Sources without a flush (an open composition)
|
|
54
54
|
// stay pending; they are listed in the answer, never waited on.
|
|
55
|
-
export async function flushUnsaved() {
|
|
55
|
+
export async function flushUnsaved(context = {}) {
|
|
56
56
|
const flushes = [...sources.values()].filter((s) => typeof s.flush === 'function' && isPending(s));
|
|
57
|
-
const results = await Promise.all(flushes.map((s) => Promise.resolve().then(s.flush).catch(() => ({ ok: false }))));
|
|
57
|
+
const results = await Promise.all(flushes.map((s) => Promise.resolve().then(() => s.flush(context)).catch(() => ({ ok: false }))));
|
|
58
58
|
return { ok: results.every((r) => r?.ok !== false), pending: pendingIds() };
|
|
59
59
|
}
|
|
60
60
|
|
package/src/live/fast-lane.js
CHANGED
|
@@ -231,7 +231,7 @@ export function createFastLane({
|
|
|
231
231
|
lastSentAt = now();
|
|
232
232
|
let result;
|
|
233
233
|
try {
|
|
234
|
-
result = await transport.send(clientId, { name, n: frame.n, ops: frame.ops, epoch: sentEpoch, ...(conn !== undefined && { conn }), ...(frame.receiptGeneration !== null && { receiptGeneration: frame.receiptGeneration }), ...(frame.pageGeneration !== null && PAGE_STORE.test(name) && { pageGeneration: frame.pageGeneration }), ...(frame.adopted && { adopted: frame.adopted }) });
|
|
234
|
+
result = await transport.send(clientId, { name, n: frame.n, ops: frame.ops, epoch: sentEpoch, ...(conn !== undefined && { conn }), ...(frame.receiptGeneration !== null && { receiptGeneration: frame.receiptGeneration }), ...(frame.pageGeneration !== null && PAGE_STORE.test(name) && { pageGeneration: frame.pageGeneration }), ...(frame.adopted && { adopted: frame.adopted }), ...(PAGE_STORE.test(name) && lane.compaction?.v === 1 && { compaction: 1 }) });
|
|
235
235
|
} catch {
|
|
236
236
|
result = null;
|
|
237
237
|
}
|
|
@@ -386,6 +386,12 @@ export function createFastLane({
|
|
|
386
386
|
|
|
387
387
|
function apply(frame) {
|
|
388
388
|
seq = frame.seq;
|
|
389
|
+
if (frame.from === "compactor") {
|
|
390
|
+
holdUntil = 0;
|
|
391
|
+
if (timer !== null) clearTimer(timer);
|
|
392
|
+
timer = null;
|
|
393
|
+
schedule();
|
|
394
|
+
}
|
|
389
395
|
const store = entries.get(frame.name)?.store;
|
|
390
396
|
if (!store || abandoned.has(frame.name)) return;
|
|
391
397
|
if (!roomNames.has(frame.name)) {
|
|
@@ -510,6 +516,7 @@ export function createFastLane({
|
|
|
510
516
|
prune(name, lane);
|
|
511
517
|
const held = lane.room;
|
|
512
518
|
lane.room = !!room;
|
|
519
|
+
lane.compaction = room?.compaction ?? null;
|
|
513
520
|
if (!room) {
|
|
514
521
|
// Not in the room (too large, too many stores, or opened after it):
|
|
515
522
|
// there are no frames of it to miss. Already out: its value still
|
|
@@ -569,7 +576,7 @@ export function createFastLane({
|
|
|
569
576
|
frame.pageGeneration = pageGeneration;
|
|
570
577
|
}
|
|
571
578
|
lane.awaiting = resend;
|
|
572
|
-
entry.store.rebase(room.value, { source: "state", dropIds, room: { seq, fresh: true, moves: state.moves?.[name] ?? null, floor: Number.isInteger(room.floor) ? room.floor : null } });
|
|
579
|
+
entry.store.rebase(room.value, { source: "state", dropIds, room: { seq, fresh: true, moves: state.moves?.[name] ?? null, floor: Number.isInteger(room.floor) ? room.floor : null, compaction: lane.compaction } });
|
|
573
580
|
lane.unsent = unframed(name, lane);
|
|
574
581
|
} else {
|
|
575
582
|
const seen = room.seen ? room.seen[clientId] : undefined;
|
|
@@ -599,7 +606,7 @@ export function createFastLane({
|
|
|
599
606
|
}
|
|
600
607
|
}
|
|
601
608
|
lane.awaiting = next;
|
|
602
|
-
entry.store.rebase(room.value, { source: "state", dropIds, room: { seq, fresh: false, moves: state.moves?.[name] ?? null, floor: Number.isInteger(room.floor) ? room.floor : null } });
|
|
609
|
+
entry.store.rebase(room.value, { source: "state", dropIds, room: { seq, fresh: false, moves: state.moves?.[name] ?? null, floor: Number.isInteger(room.floor) ? room.floor : null, compaction: lane.compaction } });
|
|
603
610
|
// Back in the room after a spell at save speed: frame what was made meanwhile.
|
|
604
611
|
if (rejoined) {
|
|
605
612
|
lane.unsent = unframed(name, lane);
|
package/src/live/local-lane.js
CHANGED
|
@@ -17,13 +17,25 @@
|
|
|
17
17
|
// stamp (or, without one, the last version known to be in the file) picks
|
|
18
18
|
// where in the log it starts, the moves after that go back on top, and the
|
|
19
19
|
// merge itself is logged as a diff so a later file stamped before it keeps it.
|
|
20
|
+
//
|
|
21
|
+
// In the room scope the page store is a host-mode room, compacted by the tab that
|
|
22
|
+
// sequences it: an update is one whole client key, a refusal answers with the
|
|
23
|
+
// host's code, and a due room folds its keys in an idle task (a tail merge or a
|
|
24
|
+
// full, each proven before it is written) and sends the commit as a move of its
|
|
25
|
+
// own. A save merges as a union of keys, under names this room makes. The scope is
|
|
26
|
+
// part of the lane key, so a tab of the lane before it never shares a sequencer.
|
|
20
27
|
|
|
21
28
|
import { localChannel } from "./local-channel.js";
|
|
22
29
|
import { applyOp, applyTracked, checkOp, cloneJson, undoTracked, validateOp, serializeLive, textHash, diffOps } from "./ops.js";
|
|
23
30
|
import { parseJsonRelaxed } from "../vendor/hyper-morph-json.vendor.js";
|
|
24
31
|
import { movesSince } from "./moves.js";
|
|
32
|
+
import { PART_KEY, entriesOf } from "../page/ybind/parts.js";
|
|
33
|
+
import { COMPACTION, ROOM_ERROR_STATUS, admitPageOps, planCompaction, buildCandidate, checkUpdate, pageShape, createRangeCache, unionFileEntries } from "./page-compaction.js";
|
|
25
34
|
|
|
26
|
-
|
|
35
|
+
// The lane's own message version, carried by every message it posts: a tab of
|
|
36
|
+
// another version drops its frames even if a scope were ever shared. The shared
|
|
37
|
+
// channel keeps its wire version, which presence rides beside.
|
|
38
|
+
const VERSION = 5;
|
|
27
39
|
export const REQUEST_MS = 2000;
|
|
28
40
|
export const CLOSE_WAIT_MS = 500;
|
|
29
41
|
export const LOG_ENTRIES = 1024;
|
|
@@ -58,11 +70,22 @@ const encoder = new TextEncoder();
|
|
|
58
70
|
// host floor. The message protocol inside the scope is unchanged.
|
|
59
71
|
export const FORMAT_SCOPE = "page-format:1";
|
|
60
72
|
|
|
73
|
+
// The room scope: this lane's sequencer is a host-mode room, so a tab of the lane
|
|
74
|
+
// before it, which cannot read that room, is never on the same channel or lock.
|
|
75
|
+
export const ROOM_SCOPE = "page-rooms:2";
|
|
76
|
+
|
|
61
77
|
export function localLaneKey(floor = 0, path = window.location.pathname) {
|
|
62
78
|
const base = "clay-live:" + path;
|
|
63
|
-
return floor >= 1 ? `${base}:${FORMAT_SCOPE}` : base
|
|
79
|
+
return `${floor >= 1 ? `${base}:${FORMAT_SCOPE}` : base}:${ROOM_SCOPE}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function idle(fn) {
|
|
83
|
+
if (typeof requestIdleCallback === "function") requestIdleCallback(fn, { timeout: 1000 });
|
|
84
|
+
else setTimeout(fn, 0);
|
|
64
85
|
}
|
|
65
86
|
|
|
87
|
+
const isYValue = (value) => !!value && typeof value === "object" && typeof value.y === "object" && value.y !== null && !Array.isArray(value.y);
|
|
88
|
+
|
|
66
89
|
export function localLaneSupported() {
|
|
67
90
|
return typeof BroadcastChannel === "function" && typeof navigator !== "undefined" && typeof navigator.locks?.request === "function";
|
|
68
91
|
}
|
|
@@ -79,12 +102,16 @@ function randomId() {
|
|
|
79
102
|
* @param {string} [options.key]
|
|
80
103
|
* @param {object} [options.channel] - the document's channel, when a presence
|
|
81
104
|
* service in this tab already owns one: the two share it rather than open a second
|
|
105
|
+
* @param {boolean} [options.rooms] - the page's room scope: the channel and the
|
|
106
|
+
* lock of the host-mode lane, which a tab of the lane before it never joins
|
|
82
107
|
*/
|
|
83
|
-
// `key` is the room's channel without a floor suffix; `floor` scopes it
|
|
84
|
-
|
|
108
|
+
// `key` is the room's channel without a floor suffix; `floor` scopes it, and
|
|
109
|
+
// `rooms` puts it in the room scope.
|
|
110
|
+
export function localTransport({ seed, key = "clay-live:" + window.location.pathname, channel: shared = null, floor = 0, rooms = false }) {
|
|
85
111
|
const clientId = randomId();
|
|
86
112
|
let scopeFloor = floor >= 1 ? 1 : 0;
|
|
87
|
-
|
|
113
|
+
const scopeOf = (f) => `${key}${f >= 1 ? `:${FORMAT_SCOPE}` : ""}${rooms ? `:${ROOM_SCOPE}` : ""}`;
|
|
114
|
+
let laneKey = scopeOf(scopeFloor);
|
|
88
115
|
let channel = null;
|
|
89
116
|
let unsubscribe = null;
|
|
90
117
|
let handlers = null;
|
|
@@ -118,7 +145,7 @@ export function localTransport({ seed, key = "clay-live:" + window.location.path
|
|
|
118
145
|
let mirror = { epoch: null, seq: 0, sequencer: null, closedBy: null, stale: false, seen: new Map(), stores: new Map() };
|
|
119
146
|
|
|
120
147
|
function post(message) {
|
|
121
|
-
channel?.post(message);
|
|
148
|
+
channel?.post({ ...message, version: VERSION });
|
|
122
149
|
}
|
|
123
150
|
|
|
124
151
|
// Over either cap the oldest entries go, and a file older than them can no
|
|
@@ -166,6 +193,7 @@ export function localTransport({ seed, key = "clay-live:" + window.location.path
|
|
|
166
193
|
const meta = {};
|
|
167
194
|
for (const [name, m] of mirror.stores) {
|
|
168
195
|
stores[name] = { value: cloneJson(m.value), ephemeral: m.ephemeral, seen: receiptsFor(name), last: m.last, landed: landedIn(m) };
|
|
196
|
+
if (name === "page" && isYValue(m.value)) stores[name].compaction = { v: COMPACTION.version, loose: COMPACTION.loose, activeBytes: COMPACTION.activeBytes };
|
|
169
197
|
meta[name] = { log: m.log, last: m.last, landed: m.landed, floor: m.floor, behind: m.behind, file: m.file, inFile: m.inFile };
|
|
170
198
|
}
|
|
171
199
|
const state = { epoch: room.epoch, seq: room.seq, writer: clientId, stores, mirror: { seen: [...mirror.seen], stores: meta } };
|
|
@@ -272,7 +300,7 @@ export function localTransport({ seed, key = "clay-live:" + window.location.path
|
|
|
272
300
|
mirror.closedBy = null;
|
|
273
301
|
const lastN = new Map();
|
|
274
302
|
for (const [nKey, n] of mirror.seen) lastN.set(nKey, { n, seq: 0 });
|
|
275
|
-
room = { epoch: mirror.epoch, seq: mirror.seq, lastN };
|
|
303
|
+
room = { epoch: mirror.epoch, seq: mirror.seq, lastN, host: pageHost() };
|
|
276
304
|
for (const [name, frame] of kept) {
|
|
277
305
|
const m = mirror.stores.get(name);
|
|
278
306
|
if (!m || m.ephemeral) continue;
|
|
@@ -351,6 +379,111 @@ export function localTransport({ seed, key = "clay-live:" + window.location.path
|
|
|
351
379
|
});
|
|
352
380
|
}
|
|
353
381
|
|
|
382
|
+
// A room's host mode: the sources it retired, the last full fold and the
|
|
383
|
+
// deletions since it (which tell a room that is out of room from one that can
|
|
384
|
+
// still be compacted), a build failure that stops it, the range cache the union
|
|
385
|
+
// merge reads, and the idle task the room has asked for.
|
|
386
|
+
const pageHost = () => ({ retired: new Set(), lastFull: null, deletesSince: 0, failed: null, ranges: createRangeCache(), task: null, overflow: false });
|
|
387
|
+
|
|
388
|
+
const pageStore = () => {
|
|
389
|
+
const m = mirror.stores.get("page");
|
|
390
|
+
return m && !m.ephemeral && isYValue(m.value) ? m : null;
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const pageBytes = (value) => encoder.encode(serializeLive(value)).length;
|
|
394
|
+
|
|
395
|
+
// A host-mode refusal answers as the server's does: the status its code has and
|
|
396
|
+
// the code itself, which is what a tab acts on.
|
|
397
|
+
const refused = (error, retryAfter = 0) => ({ status: ROOM_ERROR_STATUS[error] ?? 409, body: { code: error }, retryAfter });
|
|
398
|
+
|
|
399
|
+
// The room is due a compaction: one idle task, in this tab. The plan, the build
|
|
400
|
+
// and the commit all run inside it without yielding, so no job outlives the
|
|
401
|
+
// sequencer that made it and a hand-off cannot carry one to its successor.
|
|
402
|
+
function due(overflow = false) {
|
|
403
|
+
if (!room) return;
|
|
404
|
+
const host = room.host;
|
|
405
|
+
host.overflow ||= overflow;
|
|
406
|
+
if (host.task) return;
|
|
407
|
+
const target = room;
|
|
408
|
+
host.task = idle(() => {
|
|
409
|
+
host.task = null;
|
|
410
|
+
const over = host.overflow;
|
|
411
|
+
host.overflow = false;
|
|
412
|
+
if (stopped || room !== target || host.failed) return;
|
|
413
|
+
compact(host, { overflow: over });
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function compact(host, { overflow }) {
|
|
418
|
+
const page = pageStore();
|
|
419
|
+
if (!page) return;
|
|
420
|
+
const plan = planCompaction(page.value.y, { overflow }, COMPACTION);
|
|
421
|
+
if (!plan) return;
|
|
422
|
+
const built = buildCandidate({ kind: plan.kind, sources: plan.sources });
|
|
423
|
+
if (typeof built.update !== "string") {
|
|
424
|
+
host.failed = { reason: built.failure };
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
commitPage(host, page, plan, built.update);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Every source still there with the identical string, and the candidate proven:
|
|
431
|
+
// then the room retires the sources for one checkpoint key of its own, named for
|
|
432
|
+
// this epoch and position, and the commit goes out as a move like any other.
|
|
433
|
+
function commitPage(host, m, plan, update) {
|
|
434
|
+
const y = m.value.y;
|
|
435
|
+
const sources = new Map(plan.sources);
|
|
436
|
+
const physical = Object.keys(y).filter((k) => {
|
|
437
|
+
const part = PART_KEY.exec(k);
|
|
438
|
+
return sources.has(part ? part[1] : k) || (part && plan.kind === "full");
|
|
439
|
+
});
|
|
440
|
+
const held = new Map(entriesOf(Object.fromEntries(physical.map((k) => [k, y[k]]))));
|
|
441
|
+
for (const [key, value] of sources) if (held.get(key) !== value) return;
|
|
442
|
+
if (!checkUpdate(update)) return;
|
|
443
|
+
const key = `${plan.kind === "full" ? "c" : "t"}${textHash(room.epoch).slice(0, 8)}-${(room.seq + 1).toString(36)}`;
|
|
444
|
+
if (Object.hasOwn(y, key)) return;
|
|
445
|
+
const ops = [...physical.map((source) => validateOp({ op: "del", path: ["y", source] })), validateOp({ op: "set", path: ["y", key], value: update })];
|
|
446
|
+
const before = pageBytes(m.value);
|
|
447
|
+
let trial;
|
|
448
|
+
try {
|
|
449
|
+
trial = applyAll(m.value, ops);
|
|
450
|
+
} catch {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
const bytes = pageBytes(trial.value);
|
|
454
|
+
trial.undo();
|
|
455
|
+
if (bytes > COMPACTION.activeBytes && bytes > before) {
|
|
456
|
+
// A full fold of the whole page that still does not fit: the page is as small as it
|
|
457
|
+
// gets, so the next write is refused as full instead of waiting for another fold.
|
|
458
|
+
if (plan.kind === "full" && physical.length === Object.keys(y).length) {
|
|
459
|
+
host.lastFull = { bytes: before, seq: room.seq };
|
|
460
|
+
host.deletesSince = 0;
|
|
461
|
+
}
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const clean = !m.behind && m.landed === m.last;
|
|
465
|
+
for (const [source] of plan.sources) {
|
|
466
|
+
host.retired.add(source);
|
|
467
|
+
if (host.retired.size > COMPACTION.retired) host.retired.delete(host.retired.values().next().value);
|
|
468
|
+
}
|
|
469
|
+
room.seq += 1;
|
|
470
|
+
if (plan.kind === "full") {
|
|
471
|
+
host.lastFull = { bytes, seq: room.seq };
|
|
472
|
+
host.deletesSince = 0;
|
|
473
|
+
}
|
|
474
|
+
const data = { epoch: room.epoch, seq: room.seq, name: "page", ops, from: "compactor", n: 0, kind: plan.kind };
|
|
475
|
+
advance(data);
|
|
476
|
+
if (clean) m.landed = room.seq;
|
|
477
|
+
post({ type: "data", data });
|
|
478
|
+
handlers.frame(cloneJson(data));
|
|
479
|
+
if (clean) {
|
|
480
|
+
tellLanded(["page"]);
|
|
481
|
+
post({ type: "landed", name: "page", at: { epoch: room.epoch, seq: room.seq } });
|
|
482
|
+
}
|
|
483
|
+
const shape = pageShape(m.value.y, COMPACTION);
|
|
484
|
+
if (shape.loose >= COMPACTION.soft || !shape.normal) due();
|
|
485
|
+
}
|
|
486
|
+
|
|
354
487
|
function roomApply(from, frame) {
|
|
355
488
|
if (!frame || typeof frame.name !== "string" || !Number.isInteger(frame.n) || frame.n < 1 || !Array.isArray(frame.ops) || !frame.ops.length) {
|
|
356
489
|
return { status: 400, body: { error: "bad-frame" }, retryAfter: 0 };
|
|
@@ -365,16 +498,39 @@ export function localTransport({ seed, key = "clay-live:" + window.location.path
|
|
|
365
498
|
let ops;
|
|
366
499
|
try {
|
|
367
500
|
ops = frame.ops.map(validateOp);
|
|
368
|
-
applyAll(m.value, ops).undo();
|
|
369
501
|
} catch (err) {
|
|
370
502
|
return { status: 422, body: { error: err.message }, retryAfter: 0 };
|
|
371
503
|
}
|
|
504
|
+
const host = room.host;
|
|
505
|
+
const admission = frame.name === "page" && isYValue(m.value) ? admitPageOps({ y: m.value.y, ops, state: { ...host, compaction: frame.compaction }, limits: COMPACTION }) : null;
|
|
506
|
+
if (admission?.error) {
|
|
507
|
+
if (admission.error === "checkpoint-required") due();
|
|
508
|
+
return refused(admission.error, admission.retryAfter);
|
|
509
|
+
}
|
|
510
|
+
let trial;
|
|
511
|
+
try {
|
|
512
|
+
trial = applyAll(m.value, ops);
|
|
513
|
+
} catch (err) {
|
|
514
|
+
return { status: 422, body: { error: err.message }, retryAfter: 0 };
|
|
515
|
+
}
|
|
516
|
+
const bytes = admission ? pageBytes(trial.value) : 0;
|
|
517
|
+
trial.undo();
|
|
518
|
+
if (admission && bytes > admission.limit) {
|
|
519
|
+
if (admission.failed || (host.lastFull && host.deletesSince === 0)) return refused("page-full");
|
|
520
|
+
due(true);
|
|
521
|
+
return refused("checkpoint-required");
|
|
522
|
+
}
|
|
372
523
|
room.seq += 1;
|
|
373
524
|
room.lastN.set(nKey, { n: frame.n, seq: room.seq });
|
|
374
525
|
const data = { epoch: room.epoch, seq: room.seq, name: frame.name, ops, from, n: frame.n };
|
|
375
526
|
advance(data);
|
|
376
527
|
post({ type: "data", data });
|
|
377
528
|
handlers.frame(cloneJson(data));
|
|
529
|
+
if (admission) {
|
|
530
|
+
host.deletesSince += admission.deleted;
|
|
531
|
+
if (!admission.failed && bytes > COMPACTION.activeBytes) due(true);
|
|
532
|
+
else if (!admission.failed && admission.shape.loose + admission.added >= COMPACTION.soft) due();
|
|
533
|
+
}
|
|
378
534
|
return { status: 200, body: { seq: room.seq, epoch: room.epoch }, retryAfter: 0 };
|
|
379
535
|
}
|
|
380
536
|
|
|
@@ -416,6 +572,7 @@ export function localTransport({ seed, key = "clay-live:" + window.location.path
|
|
|
416
572
|
}
|
|
417
573
|
if (value === null || typeof value !== "object" || Array.isArray(value) !== Array.isArray(m.value)) return null;
|
|
418
574
|
const file = { seq: seq ?? null, hash: textHash(serializeLive(value)) };
|
|
575
|
+
if (name === "page" && isYValue(m.value) && isYValue(value)) return mergePageFile(m, value, file);
|
|
419
576
|
// A stamp of this room places the file. Still the value its stamp names, it
|
|
420
577
|
// is a capture and holds no merge past the stamp; edited, or unstamped, it
|
|
421
578
|
// descends from the file before it, which holds the merges in inFile.
|
|
@@ -456,6 +613,40 @@ export function localTransport({ seed, key = "clay-live:" + window.location.path
|
|
|
456
613
|
return { changed, moved: true };
|
|
457
614
|
}
|
|
458
615
|
|
|
616
|
+
// A save of the page merges as a union: every key whose content the room cannot
|
|
617
|
+
// rebuild from its own keys goes in under a fresh name of this room's, never the
|
|
618
|
+
// file's, so a key the room retired cannot come back through it. The merge is a
|
|
619
|
+
// move like any other, and the room is due a compaction after it.
|
|
620
|
+
function mergePageFile(m, value, file) {
|
|
621
|
+
if (value.g !== m.value.g) return null;
|
|
622
|
+
const host = room.host;
|
|
623
|
+
const adds = unionFileEntries({
|
|
624
|
+
y: m.value.y,
|
|
625
|
+
fileY: value.y,
|
|
626
|
+
retired: host.retired,
|
|
627
|
+
rangesFor: host.ranges,
|
|
628
|
+
keyFor: (i) => `f${textHash(room.epoch).slice(0, 8)}-${(room.seq + 1).toString(36)}-${i.toString(36)}`,
|
|
629
|
+
});
|
|
630
|
+
m.file = file;
|
|
631
|
+
const taken = new Set();
|
|
632
|
+
const ops = [];
|
|
633
|
+
for (const [key, v] of adds) {
|
|
634
|
+
if (Object.hasOwn(m.value.y, key) || taken.has(key)) continue;
|
|
635
|
+
taken.add(key);
|
|
636
|
+
ops.push(validateOp({ op: "set", path: ["y", key], value: v }));
|
|
637
|
+
}
|
|
638
|
+
if (!ops.length) return { changed: false, moved: false };
|
|
639
|
+
m.value = applyAll(m.value, ops).value;
|
|
640
|
+
room.seq += 1;
|
|
641
|
+
mirror.seq = room.seq;
|
|
642
|
+
m.last = room.seq;
|
|
643
|
+
pushLog(m, { seq: room.seq, ops });
|
|
644
|
+
m.inFile = [...m.inFile.filter((seq) => seq > m.floor), room.seq];
|
|
645
|
+
host.lastFull = null;
|
|
646
|
+
due();
|
|
647
|
+
return { changed: true, moved: true };
|
|
648
|
+
}
|
|
649
|
+
|
|
459
650
|
// A file frame (another tab's or device's save, or this browser's own
|
|
460
651
|
// checkpoint coming back), per saved store: { text, at, seq }. The sequencer
|
|
461
652
|
// merges it; every other tab keeps the newest, for the room it may take.
|
|
@@ -529,7 +720,7 @@ export function localTransport({ seed, key = "clay-live:" + window.location.path
|
|
|
529
720
|
}
|
|
530
721
|
|
|
531
722
|
function onMessage({ data: m }) {
|
|
532
|
-
if (stopped || !m || m.
|
|
723
|
+
if (stopped || !m || m.version !== VERSION) return;
|
|
533
724
|
if (room && ((m.type === "data" && m.data?.epoch === room.epoch) || ((m.type === "data-state" || m.type === "closed") && m.state?.epoch === room.epoch && m.state.writer !== clientId))) {
|
|
534
725
|
forked();
|
|
535
726
|
return;
|
|
@@ -590,7 +781,7 @@ export function localTransport({ seed, key = "clay-live:" + window.location.path
|
|
|
590
781
|
if (!covered) return { status: "refused", code: "uncovered", ...held };
|
|
591
782
|
closeRoom();
|
|
592
783
|
unsubscribe?.();
|
|
593
|
-
laneKey = want
|
|
784
|
+
laneKey = scopeOf(want);
|
|
594
785
|
scopeFloor = want;
|
|
595
786
|
channel = localChannel(laneKey);
|
|
596
787
|
unsubscribe = channel.subscribe("durable", (data) => onMessage({ data }));
|
|
@@ -632,7 +823,7 @@ export function localTransport({ seed, key = "clay-live:" + window.location.path
|
|
|
632
823
|
};
|
|
633
824
|
},
|
|
634
825
|
async send(_clientId, frame) {
|
|
635
|
-
|
|
826
|
+
if (room) return roomApply(clientId, frame);
|
|
636
827
|
return (await request({ type: "post", frame, epoch: mirror.epoch })) || { status: 0, body: null, retryAfter: 0 };
|
|
637
828
|
},
|
|
638
829
|
// The sequencer's state, installed into the mirror first. An answer to a
|
package/src/live/ops.js
CHANGED
|
@@ -318,6 +318,22 @@ export function serializeLive(value) {
|
|
|
318
318
|
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
+
// True exactly when serializeLive gives both values the same text: the same keys in the
|
|
322
|
+
// same order and equal leaves, read without writing either value out.
|
|
323
|
+
export function sameLive(a, b) {
|
|
324
|
+
if (a === b) return true;
|
|
325
|
+
if (!isContainer(a) || !isContainer(b) || Array.isArray(a) !== Array.isArray(b)) return false;
|
|
326
|
+
if (Array.isArray(a)) {
|
|
327
|
+
if (a.length !== b.length) return false;
|
|
328
|
+
for (let i = 0; i < a.length; i++) if (!sameLive(a[i], b[i])) return false;
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
const keys = Object.keys(a), other = Object.keys(b);
|
|
332
|
+
if (keys.length !== other.length) return false;
|
|
333
|
+
for (let i = 0; i < keys.length; i++) if (keys[i] !== other[i] || !sameLive(a[keys[i]], b[keys[i]])) return false;
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
|
|
321
337
|
// A live tag's lineage, `live-at="<epoch>:<seq>:<hash>"`: the room position its
|
|
322
338
|
// value was written at, and a hash of the exact text, so a room can tell its own
|
|
323
339
|
// unedited value from an edit of it.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import * as Y from '../vendor/yjs.vendor.js';
|
|
2
|
+
import { toBase64, fromBase64 } from '../vendor/yjs.vendor.js';
|
|
3
|
+
import { PART_KEY, isBase64, entriesOf } from '../page/ybind/parts.js';
|
|
4
|
+
import { rangesOf, unite, within, docRanges } from '../page/ybind/containment.js';
|
|
5
|
+
|
|
6
|
+
export const COMPACTION = Object.freeze({
|
|
7
|
+
version: 1,
|
|
8
|
+
loose: 32,
|
|
9
|
+
soft: 16,
|
|
10
|
+
activeBytes: 3840 * 1024,
|
|
11
|
+
deleteReserve: 64 * 1024,
|
|
12
|
+
newPerFrame: 4,
|
|
13
|
+
tailFloor: 64 * 1024,
|
|
14
|
+
leaseMs: 60 * 1000,
|
|
15
|
+
retired: 8192,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const ROOM_ERROR_STATUS = Object.freeze({
|
|
19
|
+
'checkpoint-required': 503, 'page-full': 409, 'compaction-failed': 409,
|
|
20
|
+
'page-immutable': 409, 'page-batch': 409, 'page-update-invalid': 422, 'page-upgrade': 404,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const CLIENT_KEY = /^[ur][0-9a-z]+(-[0-9a-z]+){2}$/;
|
|
24
|
+
export const HOST_KEY = /^[ctf][0-9a-z]{8}-[0-9a-z]+(-[0-9a-z]+)?$/;
|
|
25
|
+
const CHECKPOINT_KEY = /^(seed|c[0-9a-z]{8}-[0-9a-z]+)$/;
|
|
26
|
+
|
|
27
|
+
export const isCheckpointKey = (key) => CHECKPOINT_KEY.test(key);
|
|
28
|
+
|
|
29
|
+
export function pageShape(y, limits = COMPACTION) {
|
|
30
|
+
let checkpoints = 0, loose = 0, parts = 0, legacy = 0;
|
|
31
|
+
for (const key of Object.keys(y)) {
|
|
32
|
+
if (PART_KEY.test(key)) { parts++; continue; }
|
|
33
|
+
if (isCheckpointKey(key)) checkpoints++;
|
|
34
|
+
else loose++;
|
|
35
|
+
if (!CLIENT_KEY.test(key) && !HOST_KEY.test(key) && key !== 'seed') legacy++;
|
|
36
|
+
}
|
|
37
|
+
return { checkpoints, loose, parts, legacy, normal: parts === 0 && checkpoints <= 1 && loose <= limits.loose };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function planCompaction(y, { overflow = false } = {}, limits = COMPACTION) {
|
|
41
|
+
const shape = pageShape(y, limits);
|
|
42
|
+
const entries = entriesOf(y);
|
|
43
|
+
const checkpoint = entries.filter(([key]) => isCheckpointKey(key));
|
|
44
|
+
const loose = entries.filter(([key]) => !isCheckpointKey(key));
|
|
45
|
+
const looseBytes = loose.reduce((sum, [, v]) => sum + v.length, 0);
|
|
46
|
+
const checkpointBytes = checkpoint.reduce((sum, [, v]) => sum + v.length, 0);
|
|
47
|
+
const full = !shape.normal || overflow || looseBytes > Math.max(limits.tailFloor, checkpointBytes / 4);
|
|
48
|
+
if (full) return { kind: 'full', sources: entries };
|
|
49
|
+
if (loose.length < 2 || shape.loose < limits.soft) return null;
|
|
50
|
+
return { kind: 'tail', sources: loose };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function buildCandidate({ kind, sources, encode = Y.encodeStateAsUpdateV2 }) {
|
|
54
|
+
const updates = sources.map(([, v]) => fromBase64(v));
|
|
55
|
+
if (kind === 'tail') {
|
|
56
|
+
const merged = Y.mergeUpdatesV2(updates);
|
|
57
|
+
const index = rangesOf(merged);
|
|
58
|
+
for (const update of updates) if (!within(index, rangesOf(update))) return { failure: 'not-covered' };
|
|
59
|
+
return { update: toBase64(merged) };
|
|
60
|
+
}
|
|
61
|
+
const doc = new Y.Doc({ gc: true });
|
|
62
|
+
try {
|
|
63
|
+
for (const update of updates) Y.applyUpdateV2(doc, update);
|
|
64
|
+
if (doc.store.pendingStructs || doc.store.pendingDs) {
|
|
65
|
+
const merged = Y.mergeUpdatesV2(updates);
|
|
66
|
+
const index = rangesOf(merged);
|
|
67
|
+
for (const update of updates) if (!within(index, rangesOf(update))) return { failure: 'not-covered' };
|
|
68
|
+
return { update: toBase64(merged), pending: true };
|
|
69
|
+
}
|
|
70
|
+
const index = docRanges(doc);
|
|
71
|
+
for (const update of updates) if (!within(index, rangesOf(update))) return { failure: 'not-covered' };
|
|
72
|
+
const out = toBase64(encode(doc));
|
|
73
|
+
const check = new Y.Doc({ gc: true });
|
|
74
|
+
try {
|
|
75
|
+
Y.applyUpdateV2(check, fromBase64(out));
|
|
76
|
+
const stored = docRanges(check);
|
|
77
|
+
if (!stored.complete) return { failure: 'not-covered' };
|
|
78
|
+
for (const update of updates) if (!within(stored, rangesOf(update))) return { failure: 'not-covered' };
|
|
79
|
+
} finally { check.destroy(); }
|
|
80
|
+
return { update: out };
|
|
81
|
+
} catch (error) {
|
|
82
|
+
return { failure: 'invalid-update', message: error?.message || String(error) };
|
|
83
|
+
} finally {
|
|
84
|
+
doc.destroy();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function checkUpdate(text) {
|
|
89
|
+
if (typeof text !== 'string' || !isBase64(text)) return null;
|
|
90
|
+
try {
|
|
91
|
+
const { items, deletes } = rangesOf(fromBase64(text));
|
|
92
|
+
let deleted = 0;
|
|
93
|
+
for (const spans of deletes.values()) for (const [from, to] of spans) deleted += to - from;
|
|
94
|
+
return { deleted, inserts: items.size > 0 };
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function admitPageOps({ y, ops, state, limits }) {
|
|
101
|
+
if (state.compaction !== limits.version) return { error: 'page-upgrade' };
|
|
102
|
+
let added = 0;
|
|
103
|
+
let deleted = 0;
|
|
104
|
+
let inserts = false;
|
|
105
|
+
for (const op of ops) {
|
|
106
|
+
const key = op.path?.[1];
|
|
107
|
+
if (op.op !== 'set' || typeof key !== 'string' || !CLIENT_KEY.test(key)) return { error: 'page-immutable' };
|
|
108
|
+
if (Object.hasOwn(y, key)) {
|
|
109
|
+
if (y[key] !== op.value) return { error: 'page-immutable' };
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (state.retired.has(key)) return { error: 'page-immutable' };
|
|
113
|
+
const checked = checkUpdate(op.value);
|
|
114
|
+
if (!checked) return { error: 'page-update-invalid' };
|
|
115
|
+
added += 1;
|
|
116
|
+
deleted += checked.deleted;
|
|
117
|
+
inserts ||= checked.inserts;
|
|
118
|
+
}
|
|
119
|
+
if (added > limits.newPerFrame) return { error: 'page-batch' };
|
|
120
|
+
// A page whose compaction failed keeps taking edits up to its byte limit, as a room
|
|
121
|
+
// without compaction does, and refuses past it as full instead of waiting for a fold.
|
|
122
|
+
if (state.failed) return { added, deleted, inserts, limit: limits.activeBytes + (inserts ? 0 : limits.deleteReserve), shape: null, failed: true };
|
|
123
|
+
const shape = pageShape(y, limits);
|
|
124
|
+
if (!shape.normal || shape.loose + added > limits.loose) return { error: 'checkpoint-required', retryAfter: 0 };
|
|
125
|
+
return { added, deleted, inserts, limit: limits.activeBytes + (inserts ? 0 : limits.deleteReserve), shape };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function contained(roomEntries, fileValue, rangesFor) {
|
|
129
|
+
const index = unite(roomEntries.map(([k, v]) => rangesFor(k, v)));
|
|
130
|
+
return within(index, rangesOf(fromBase64(fileValue)));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const UNIQUE_KEY = (key) => CLIENT_KEY.test(key) || HOST_KEY.test(key);
|
|
134
|
+
|
|
135
|
+
export function createRangeCache(max = 256) {
|
|
136
|
+
const cache = new Map();
|
|
137
|
+
return (key, value) => {
|
|
138
|
+
const hit = cache.get(key);
|
|
139
|
+
if (hit && hit.value === value) return hit.ranges;
|
|
140
|
+
const ranges = rangesOf(fromBase64(value));
|
|
141
|
+
cache.delete(key);
|
|
142
|
+
cache.set(key, { value, ranges });
|
|
143
|
+
if (cache.size > max) cache.delete(cache.keys().next().value);
|
|
144
|
+
return ranges;
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function unionFileEntries({ y, fileY, retired, rangesFor, keyFor = (i, key) => key }) {
|
|
149
|
+
const roomEntries = entriesOf(y);
|
|
150
|
+
const have = new Map(roomEntries);
|
|
151
|
+
let index = null;
|
|
152
|
+
const adds = [];
|
|
153
|
+
for (const [key, value] of entriesOf(fileY ?? {})) {
|
|
154
|
+
if (have.get(key) === value) continue;
|
|
155
|
+
if (retired.has(key) && UNIQUE_KEY(key)) continue;
|
|
156
|
+
let ranges;
|
|
157
|
+
try { ranges = rangesOf(fromBase64(value)); } catch { continue; }
|
|
158
|
+
index ??= unite(roomEntries.map(([k, v]) => rangesFor(k, v)));
|
|
159
|
+
if (!within(index, ranges)) adds.push([key, value]);
|
|
160
|
+
}
|
|
161
|
+
return adds.map(([key, value], i) => [keyFor(i, key), value]);
|
|
162
|
+
}
|
package/src/live/server-lane.js
CHANGED
|
@@ -46,6 +46,7 @@ export async function fetchLiveState(clientId, since = null, { signal } = {}) {
|
|
|
46
46
|
const url = dataUrl();
|
|
47
47
|
if (clientId) url.searchParams.set("client-id", clientId);
|
|
48
48
|
if (since && typeof since.epoch === "string" && Number.isInteger(since.seq)) url.searchParams.set("since", `${since.epoch}:${since.seq}`);
|
|
49
|
+
url.searchParams.set("compaction", "1");
|
|
49
50
|
try {
|
|
50
51
|
const response = await fetch(url.href, {
|
|
51
52
|
credentials: "same-origin",
|
package/src/live/store.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Local ops change state at once. When authoritative moves, state is rebuilt in
|
|
5
5
|
// place, so an object or list page code holds a reference to stays live.
|
|
6
6
|
|
|
7
|
-
import { validateOp, checkOp, applyOp, applyOps, applyTracked, undoTracked, cloneJson, assignDeep, coalesce, overlaps, serializeLive } from "./ops.js";
|
|
7
|
+
import { validateOp, checkOp, applyOp, applyOps, applyTracked, undoTracked, cloneJson, assignDeep, coalesce, overlaps, serializeLive, sameLive } from "./ops.js";
|
|
8
8
|
|
|
9
9
|
const MUTATORS = new Set(["push", "pop", "shift", "unshift", "splice", "sort", "reverse", "fill", "copyWithin"]);
|
|
10
10
|
const INDEX = /^(0|[1-9]\d*)$/;
|
|
@@ -112,6 +112,8 @@ export function createStore({ name, value, ephemeral = false }) {
|
|
|
112
112
|
if (!isContainer(value)) throw new TypeError(`clay.live("${name}"): the tag must hold a JSON object or list`);
|
|
113
113
|
|
|
114
114
|
let authoritative = cloneJson(value);
|
|
115
|
+
// The last room a rebase described, for a listener that subscribes after the lane joined.
|
|
116
|
+
let lastRoom = null;
|
|
115
117
|
const state = cloneJson(value);
|
|
116
118
|
// state is authoritative with pending replayed over it. Only a confirm whose
|
|
117
119
|
// ops no longer apply breaks that, until the next rebuild.
|
|
@@ -171,7 +173,8 @@ export function createStore({ name, value, ephemeral = false }) {
|
|
|
171
173
|
console.warn(`clay.live("${name}"): ignored an incoming value that is not ${kind(state)}`);
|
|
172
174
|
return false;
|
|
173
175
|
}
|
|
174
|
-
|
|
176
|
+
if (room) lastRoom = room;
|
|
177
|
+
const before = cloneJson(state);
|
|
175
178
|
const prev = authoritative;
|
|
176
179
|
authoritative = cloneJson(next);
|
|
177
180
|
if (drop) pending = pending.filter((entry) => entry.id < drop[0] || entry.id > drop[1]);
|
|
@@ -179,7 +182,7 @@ export function createStore({ name, value, ephemeral = false }) {
|
|
|
179
182
|
else pending = through === undefined ? pending.slice(confirmed) : pending.filter((entry) => entry.id > through);
|
|
180
183
|
rebuild();
|
|
181
184
|
tell({ source, frame, room, cause, prev, before: (path) => readPath(prev, path), next: authoritative });
|
|
182
|
-
if (
|
|
185
|
+
if (!sameLive(before, state)) emit(source, null);
|
|
183
186
|
return true;
|
|
184
187
|
}
|
|
185
188
|
|
|
@@ -361,6 +364,9 @@ export function createStore({ name, value, ephemeral = false }) {
|
|
|
361
364
|
get authoritative() {
|
|
362
365
|
return authoritative;
|
|
363
366
|
},
|
|
367
|
+
get room() {
|
|
368
|
+
return lastRoom;
|
|
369
|
+
},
|
|
364
370
|
get pending() {
|
|
365
371
|
return pending.map((entry) => entry.op);
|
|
366
372
|
},
|
|
@@ -374,6 +380,7 @@ export function createStore({ name, value, ephemeral = false }) {
|
|
|
374
380
|
},
|
|
375
381
|
setMode(next) {
|
|
376
382
|
mode = next;
|
|
383
|
+
if (next !== "fast") lastRoom = null;
|
|
377
384
|
},
|
|
378
385
|
// A transport hears every local op batch, in order, as it is committed.
|
|
379
386
|
onLocal(fn) {
|
|
@@ -471,6 +478,7 @@ export function createStore({ name, value, ephemeral = false }) {
|
|
|
471
478
|
}
|
|
472
479
|
return readPath(authoritative, path);
|
|
473
480
|
};
|
|
481
|
+
if (room) lastRoom = room;
|
|
474
482
|
tell({ source, frame, room, cause, get prev() { return prev(); }, before, next: authoritative });
|
|
475
483
|
if (changed) emit(source, null);
|
|
476
484
|
return true;
|
package/src/loader-logic.js
CHANGED
|
@@ -214,7 +214,7 @@ const DRAFT_DROPS_CORE = ["attrs/refetch-on-save.js", "core/conflict-notice.js"]
|
|
|
214
214
|
// Only that host's copies auto-add the update check; another source names it.
|
|
215
215
|
const TEMPLATE_SOURCE_PREFIX = "https://hyperclay.com/templates/";
|
|
216
216
|
|
|
217
|
-
export function resolveModules(params, isEditMode, { isDraftMode = false, hasSap = false, canEdit = false, templateSource = null } = {}) {
|
|
217
|
+
export function resolveModules(params, isEditMode, { isDraftMode = false, hasSap = false, hasVariants = false, canEdit = false, templateSource = null } = {}) {
|
|
218
218
|
const core = [...CORE_WAVES.always];
|
|
219
219
|
if (isEditMode) core.push(...CORE_WAVES.editOnly);
|
|
220
220
|
|
|
@@ -228,6 +228,8 @@ export function resolveModules(params, isEditMode, { isDraftMode = false, hasSap
|
|
|
228
228
|
if (spec.default) enabled.add(name);
|
|
229
229
|
}
|
|
230
230
|
if (hasSap) enabled.add("derive");
|
|
231
|
+
// Two or more looks need the dock's variant pill, and only someone who can edit sees it.
|
|
232
|
+
if (hasVariants && (isEditMode || canEdit)) enabled.add("menu");
|
|
231
233
|
// ai-edit cannot run without the wire, so excluding the wire excludes it too.
|
|
232
234
|
if (excluded.has("wire")) enabled.delete("ai-edit");
|
|
233
235
|
if (!isEditMode && !isDraftMode && enabled.has("page")) enabled.add("page-view");
|