@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
package/src/route/route.js
CHANGED
|
@@ -119,7 +119,9 @@ export function set(patch, { replace = false } = {}) {
|
|
|
119
119
|
history.replaceState(history.state, "", bare + hash);
|
|
120
120
|
apply();
|
|
121
121
|
} else if (hash) {
|
|
122
|
+
// Applied now, as the other branches are: the hashchange that follows finds nothing new.
|
|
122
123
|
location.hash = hash;
|
|
124
|
+
apply();
|
|
123
125
|
} else {
|
|
124
126
|
history.pushState(history.state, "", bare);
|
|
125
127
|
apply();
|
|
@@ -161,6 +163,7 @@ export function start() {
|
|
|
161
163
|
mirrorRoot(parseHash(location.hash));
|
|
162
164
|
window.addEventListener("hashchange", apply);
|
|
163
165
|
document.addEventListener("clay:sync-applied", queueApply);
|
|
166
|
+
document.addEventListener("clay:drafts-restored", queueApply);
|
|
164
167
|
document.addEventListener("clay:ready", queueApply);
|
|
165
168
|
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", onDomReady, { once: true });
|
|
166
169
|
else onDomReady();
|
|
@@ -173,6 +176,7 @@ export function stop() {
|
|
|
173
176
|
lastUnmatched = "";
|
|
174
177
|
window.removeEventListener("hashchange", apply);
|
|
175
178
|
document.removeEventListener("clay:sync-applied", queueApply);
|
|
179
|
+
document.removeEventListener("clay:drafts-restored", queueApply);
|
|
176
180
|
document.removeEventListener("clay:ready", queueApply);
|
|
177
181
|
document.removeEventListener("DOMContentLoaded", onDomReady);
|
|
178
182
|
if (observer) observer.disconnect();
|
|
@@ -57,4 +57,8 @@ export { makePageTransition, pageTransitionState } from './live-page-history.js'
|
|
|
57
57
|
|
|
58
58
|
export { documentEtag, ifMatchSatisfied } from './spec-wire.js';
|
|
59
59
|
|
|
60
|
-
export { LIVE_LIMITS, REFUSED_MS, createRooms, roomIsDirty, PersistentReservationError } from './live-data-rooms.js';
|
|
60
|
+
export { LIVE_LIMITS, REFUSED_MS, COMPACTOR, createRooms, roomIsDirty, PersistentReservationError } from './live-data-rooms.js';
|
|
61
|
+
|
|
62
|
+
export { COMPACTION, ROOM_ERROR_STATUS } from '../../live/page-compaction.js';
|
|
63
|
+
|
|
64
|
+
export { createPageCompactor, pageCompactionWorkerUrl } from './page-compactor.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { isDeepStrictEqual } from 'node:util';
|
|
3
3
|
import { parseJsonRelaxed } from 'hyper-morph/json-parse';
|
|
4
|
-
import { validateOp, checkOp, applyOp, applyTracked, undoTracked, sizeChange, cloneJson, serializeLive, textHash, diffOps } from './live-data-ops.js';
|
|
4
|
+
import { validateOp, checkOp, applyOp, applyOps, applyTracked, undoTracked, sizeChange, cloneJson, serializeLive, textHash, diffOps } from './live-data-ops.js';
|
|
5
5
|
import { liveTags } from './live-data-tags.js';
|
|
6
6
|
import { checkPageConversion, PageConversionError } from './page-conversion.js';
|
|
7
7
|
import { checkPageInitial } from './page-write-admission.js';
|
|
@@ -10,6 +10,8 @@ import { makePageTransition, pageTransitionState } from './live-page-history.js'
|
|
|
10
10
|
export { liveTags } from './live-data-tags.js';
|
|
11
11
|
|
|
12
12
|
import { frameSet, contains, descendants, receiptHas, receiptBytes, saveReceipt, postReceipt, replaceReceipt, retireReceipt, RECEIPT_LIMIT } from './live-provenance.js';
|
|
13
|
+
import { COMPACTION, pageShape, planCompaction, checkUpdate, admitPageOps, createRangeCache, unionFileEntries, buildCandidate } from '../../live/page-compaction.js';
|
|
14
|
+
import { PART_KEY, entriesOf } from '../../page/ybind/parts.js';
|
|
13
15
|
|
|
14
16
|
const byteLength = (text) => Buffer.byteLength(text, 'utf8');
|
|
15
17
|
const sizeOf = (value) => byteLength(serializeLive(value));
|
|
@@ -54,6 +56,7 @@ export const LIVE_LIMITS = Object.freeze({
|
|
|
54
56
|
});
|
|
55
57
|
|
|
56
58
|
const PAGE = /^page(\.[0-9]+)?$/;
|
|
59
|
+
export const COMPACTOR = 'compactor';
|
|
57
60
|
const isContainer = (v) => v !== null && typeof v === 'object';
|
|
58
61
|
|
|
59
62
|
// The stores a room starts with. A store over the store cap is left out, and so
|
|
@@ -193,7 +196,26 @@ export class PersistentReservationError extends Error {
|
|
|
193
196
|
}
|
|
194
197
|
}
|
|
195
198
|
|
|
196
|
-
export function createRooms({ now = Date.now, newEpoch = randomUUID, liveBytes = LIVE_LIMITS.liveBytes } = {}) {
|
|
199
|
+
export function createRooms({ now = Date.now, newEpoch = randomUUID, liveBytes = LIVE_LIMITS.liveBytes, compaction = null } = {}) {
|
|
200
|
+
const hostLimits = compaction ? Object.freeze({ ...COMPACTION, ...(typeof compaction === 'object' ? compaction : {}) }) : null;
|
|
201
|
+
const jobs = new WeakMap();
|
|
202
|
+
let onDue = () => {};
|
|
203
|
+
const hostMode = (yjs) => (hostLimits && yjs ? { fence: 0, job: null, lastFull: null, deletesSince: 0, contentSeq: 0, failed: null, retired: new Set(), legacy: new Set(), ranges: createRangeCache() } : null);
|
|
204
|
+
// A copy of a room to try a file against. Its compaction state is room data plus a
|
|
205
|
+
// running job and a range cache, which hold functions: the copy starts with neither.
|
|
206
|
+
const projectRoom = (room) => {
|
|
207
|
+
const compaction = room.compaction;
|
|
208
|
+
if (!compaction) return structuredClone(room);
|
|
209
|
+
room.compaction = null;
|
|
210
|
+
let projected;
|
|
211
|
+
try { projected = structuredClone(room); } finally { room.compaction = compaction; }
|
|
212
|
+
projected.compaction = { ...structuredClone({ ...compaction, job: null, ranges: null }), ranges: createRangeCache() };
|
|
213
|
+
return projected;
|
|
214
|
+
};
|
|
215
|
+
const due = (room, overflow = false) => {
|
|
216
|
+
if (!room.compaction || room.compaction.job || room.compaction.failed) return;
|
|
217
|
+
try { onDue(room, { overflow }); } catch {}
|
|
218
|
+
};
|
|
197
219
|
const rooms = new Map();
|
|
198
220
|
const pageReservations = new Map();
|
|
199
221
|
const reservedNodes = new Map();
|
|
@@ -218,6 +240,7 @@ export function createRooms({ now = Date.now, newEpoch = randomUUID, liveBytes =
|
|
|
218
240
|
seen: new Map(),
|
|
219
241
|
emptySince: null,
|
|
220
242
|
...(inspectPage(html).kind === 'yjs' && { pageTransition: makePageTransition() }),
|
|
243
|
+
compaction: hostMode(inspectPage(html).kind === 'yjs'),
|
|
221
244
|
});
|
|
222
245
|
|
|
223
246
|
const roomBytes = (room) => {
|
|
@@ -226,11 +249,97 @@ export function createRooms({ now = Date.now, newEpoch = randomUUID, liveBytes =
|
|
|
226
249
|
return total;
|
|
227
250
|
};
|
|
228
251
|
const heldBy = (room) => {
|
|
229
|
-
let total = (room.receiptBytes ?? 0) + (room.pageTransition?.bytes ?? 0);
|
|
252
|
+
let total = (room.receiptBytes ?? 0) + (room.pageTransition?.bytes ?? 0) + (room.compaction?.job?.reserved ?? 0);
|
|
230
253
|
for (const s of room.stores.values()) total += s.bytes + s.logBytes;
|
|
231
254
|
return total;
|
|
232
255
|
};
|
|
233
256
|
|
|
257
|
+
const admit = (room, store, frame, ops) => {
|
|
258
|
+
const state = room.compaction;
|
|
259
|
+
const answer = admitPageOps({ y: store.value.y, ops, state: { ...room.compaction, compaction: frame.compaction }, limits: hostLimits });
|
|
260
|
+
if (answer.error) {
|
|
261
|
+
if (answer.error === 'checkpoint-required') due(room);
|
|
262
|
+
return answer;
|
|
263
|
+
}
|
|
264
|
+
const { added, deleted, limit, shape } = answer;
|
|
265
|
+
return {
|
|
266
|
+
limit,
|
|
267
|
+
over() {
|
|
268
|
+
if (answer.failed || (state.lastFull && state.deletesSince === 0)) return { error: 'page-full' };
|
|
269
|
+
due(room, true);
|
|
270
|
+
return { error: 'checkpoint-required', retryAfter: 0 };
|
|
271
|
+
},
|
|
272
|
+
admitted(bytes) {
|
|
273
|
+
state.deletesSince += deleted;
|
|
274
|
+
state.contentSeq = room.seq;
|
|
275
|
+
if (answer.failed) return;
|
|
276
|
+
if (bytes > hostLimits.activeBytes) due(room, true);
|
|
277
|
+
else if (shape.loose + added >= hostLimits.soft) due(room);
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const mergePageFile = (room, store, value) => {
|
|
283
|
+
const state = room.compaction;
|
|
284
|
+
if (value.g !== store.value.g) return null;
|
|
285
|
+
const adds = unionFileEntries({
|
|
286
|
+
y: store.value.y,
|
|
287
|
+
fileY: value.y,
|
|
288
|
+
retired: state.retired,
|
|
289
|
+
rangesFor: state.ranges,
|
|
290
|
+
keyFor: (i) => `f${textHash(room.epoch).slice(0, 8)}-${(room.seq + 1).toString(36)}-${i.toString(36)}`,
|
|
291
|
+
});
|
|
292
|
+
const seq = room.seq + 1;
|
|
293
|
+
const taken = new Set();
|
|
294
|
+
const ops = [];
|
|
295
|
+
for (const [key, v] of adds) {
|
|
296
|
+
if (Object.hasOwn(store.value.y, key) || taken.has(key)) continue;
|
|
297
|
+
taken.add(key);
|
|
298
|
+
ops.push(validateOp({ op: 'set', path: ['y', key], value: v }));
|
|
299
|
+
}
|
|
300
|
+
if (!ops.length) return null;
|
|
301
|
+
const fits = (n) => n <= LIVE_LIMITS.storeBytes && roomBytes(room) - store.bytes + n <= LIVE_LIMITS.roomBytes;
|
|
302
|
+
let next = applyOps(cloneJson(store.value), ops);
|
|
303
|
+
let bytes = sizeOf(next);
|
|
304
|
+
if (!fits(bytes)) {
|
|
305
|
+
// A file from an older tab repeats most of the page, so the plain union can be twice
|
|
306
|
+
// its real size: fold the room and the file into one checkpoint before giving up.
|
|
307
|
+
const built = buildCandidate({ kind: 'full', sources: [...entriesOf(store.value.y), ...adds] });
|
|
308
|
+
if (!built.update || built.pending) return 'dropped';
|
|
309
|
+
const retire = Object.keys(store.value.y);
|
|
310
|
+
ops.length = 0;
|
|
311
|
+
for (const key of retire) ops.push(validateOp({ op: 'del', path: ['y', key] }));
|
|
312
|
+
ops.push(validateOp({ op: 'set', path: ['y', `c${textHash(room.epoch).slice(0, 8)}-${seq.toString(36)}`], value: built.update }));
|
|
313
|
+
next = applyOps(cloneJson(store.value), ops);
|
|
314
|
+
bytes = sizeOf(next);
|
|
315
|
+
if (!fits(bytes)) return 'dropped';
|
|
316
|
+
for (const [key] of entriesOf(store.value.y)) {
|
|
317
|
+
state.retired.add(key);
|
|
318
|
+
if (state.retired.size > hostLimits.retired) state.retired.delete(state.retired.values().next().value);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const logged = byteLength(JSON.stringify(ops));
|
|
322
|
+
store.value = next;
|
|
323
|
+
store.bytes = bytes;
|
|
324
|
+
room.seq = seq;
|
|
325
|
+
store.last = seq;
|
|
326
|
+
store.log.push({ seq, ops, bytes: logged, merge: true });
|
|
327
|
+
store.inFile.add(seq);
|
|
328
|
+
store.logBytes += logged;
|
|
329
|
+
evict(store);
|
|
330
|
+
state.contentSeq = seq;
|
|
331
|
+
state.lastFull = null;
|
|
332
|
+
due(room);
|
|
333
|
+
return true;
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const endJob = (job) => {
|
|
337
|
+
if (job.room.compaction?.job !== job) return false;
|
|
338
|
+
job.room.compaction.job = null;
|
|
339
|
+
if (rooms.get(job.room.key) === job.room) used -= job.reserved;
|
|
340
|
+
return true;
|
|
341
|
+
};
|
|
342
|
+
|
|
234
343
|
const pageSource = (key, nodeId) => {
|
|
235
344
|
if (nodeId == null) throw new PageConversionError('invalid-document');
|
|
236
345
|
if (pageReservations.has(key) || persistentReservations.has(key) || reservedNodes.has(String(nodeId))) throw new PageConversionError('page-converting');
|
|
@@ -308,13 +417,14 @@ export function createRooms({ now = Date.now, newEpoch = randomUUID, liveBytes =
|
|
|
308
417
|
if (tag.name !== 'page' || !isYPageValue(value)) continue;
|
|
309
418
|
} else if (isYPageValue(value, { complete: false })) continue;
|
|
310
419
|
}
|
|
420
|
+
const host = Boolean(room.compaction && tag.name === 'page' && room.pageTransition?.format === 'yjs');
|
|
311
421
|
const before = store.landed;
|
|
312
422
|
const base = baseOf(room, store, tag.at);
|
|
313
423
|
// A capture (stamped, and still the value its stamp names) holds no merge
|
|
314
424
|
// past its stamp. Anything else descends from the file on disk, which
|
|
315
425
|
// holds the merges in inFile: they are not replayed over it.
|
|
316
426
|
const stamped = placed(room, store, tag.at) && tag.at.hash === textHash(serializeLive(value));
|
|
317
|
-
if (base < store.floor) {
|
|
427
|
+
if (!host && base < store.floor) {
|
|
318
428
|
store.landed = -1;
|
|
319
429
|
if (before !== -1) landed.push(tag.name);
|
|
320
430
|
continue;
|
|
@@ -322,53 +432,69 @@ export function createRooms({ now = Date.now, newEpoch = randomUUID, liveBytes =
|
|
|
322
432
|
// A restore proves neither frame inclusion nor descent from a capture.
|
|
323
433
|
const by = restored ? null : provenBy(room, tag.name, tag, stamped);
|
|
324
434
|
const frames = tag.at?.epoch === room.epoch ? frameSet(by) : [];
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
return by.v === 2 && p && p.at.epoch === tag.at.epoch && p.at.seq === tag.at.seq && contains(ancestry, ...p.capture);
|
|
331
|
-
};
|
|
332
|
-
if (stamped) store.inFile.clear();
|
|
333
|
-
const entries = [];
|
|
334
|
-
for (const entry of store.log) {
|
|
335
|
-
if (entry.seq <= base) continue;
|
|
336
|
-
if (own(entry)) store.inFile.add(entry.seq);
|
|
337
|
-
else if (stamped || !store.inFile.has(entry.seq)) entries.push(entry);
|
|
338
|
-
}
|
|
339
|
-
for (const entry of entries) {
|
|
340
|
-
for (const op of entry.ops) {
|
|
341
|
-
try {
|
|
342
|
-
checkOp(value, op);
|
|
343
|
-
value = applyOp(value, op);
|
|
344
|
-
} catch {}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
const text = serializeLive(value);
|
|
348
|
-
if (text !== serializeLive(store.value)) {
|
|
349
|
-
const bytes = byteLength(text);
|
|
350
|
-
if (bytes > LIVE_LIMITS.storeBytes || roomBytes(room) - store.bytes + bytes > LIVE_LIMITS.roomBytes) {
|
|
435
|
+
if (host) {
|
|
436
|
+
// No log replay: the room takes in every edit the file holds and that it
|
|
437
|
+
// lacks, each under a fresh key, and never deletes or overwrites its own.
|
|
438
|
+
const merged = mergePageFile(room, store, value);
|
|
439
|
+
if (merged === 'dropped') {
|
|
351
440
|
room.stores.delete(tag.name);
|
|
352
441
|
valueChanged = true;
|
|
353
442
|
continue;
|
|
354
443
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
444
|
+
if (merged) valueChanged = true;
|
|
445
|
+
if (stamped && tag.at.epoch === room.epoch) {
|
|
446
|
+
store.landed = tag.at.seq >= room.compaction.contentSeq ? store.last : tag.at.seq;
|
|
447
|
+
if (store.landed !== before) landed.push(tag.name);
|
|
448
|
+
}
|
|
449
|
+
} else {
|
|
450
|
+
const ancestry = descendants(by);
|
|
451
|
+
const own = (entry) => {
|
|
452
|
+
if (!by) return false;
|
|
453
|
+
if (!entry.merge) return contains(frames, entry.from, entry.n);
|
|
454
|
+
const p = entry.provenance;
|
|
455
|
+
return by.v === 2 && p && p.at.epoch === tag.at.epoch && p.at.seq === tag.at.seq && contains(ancestry, ...p.capture);
|
|
456
|
+
};
|
|
457
|
+
if (stamped) store.inFile.clear();
|
|
458
|
+
const entries = [];
|
|
459
|
+
for (const entry of store.log) {
|
|
460
|
+
if (entry.seq <= base) continue;
|
|
461
|
+
if (own(entry)) store.inFile.add(entry.seq);
|
|
462
|
+
else if (stamped || !store.inFile.has(entry.seq)) entries.push(entry);
|
|
463
|
+
}
|
|
464
|
+
for (const entry of entries) {
|
|
465
|
+
for (const op of entry.ops) {
|
|
466
|
+
try {
|
|
467
|
+
checkOp(value, op);
|
|
468
|
+
value = applyOp(value, op);
|
|
469
|
+
} catch {}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
const text = serializeLive(value);
|
|
473
|
+
if (text !== serializeLive(store.value)) {
|
|
474
|
+
const bytes = byteLength(text);
|
|
475
|
+
if (bytes > LIVE_LIMITS.storeBytes || roomBytes(room) - store.bytes + bytes > LIVE_LIMITS.roomBytes) {
|
|
476
|
+
room.stores.delete(tag.name);
|
|
477
|
+
valueChanged = true;
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
// Logged like a frame, so a capture stamped before this merge gets it
|
|
481
|
+
// replayed on top (leaf ops: a capture that holds it already is unchanged).
|
|
482
|
+
const ops = diffOps(store.value, value);
|
|
483
|
+
store.value = value;
|
|
484
|
+
store.bytes = bytes;
|
|
485
|
+
room.seq += 1;
|
|
486
|
+
store.last = room.seq;
|
|
487
|
+
const provenance = by?.v === 2 ? { at: { epoch: tag.at.epoch, seq: tag.at.seq }, by: by.frames, capture: by.capture } : null;
|
|
488
|
+
const logged = byteLength(JSON.stringify(provenance ? { ops, provenance } : ops));
|
|
489
|
+
store.log.push({ seq: room.seq, ops, bytes: logged, merge: true, ...(provenance && { provenance }) });
|
|
490
|
+
store.inFile.add(room.seq);
|
|
491
|
+
store.logBytes += logged;
|
|
492
|
+
evict(store);
|
|
493
|
+
valueChanged = true;
|
|
494
|
+
}
|
|
495
|
+
store.landed = entries.length ? base : store.last;
|
|
496
|
+
if (store.landed !== before) landed.push(tag.name);
|
|
369
497
|
}
|
|
370
|
-
store.landed = entries.length ? base : store.last;
|
|
371
|
-
if (store.landed !== before) landed.push(tag.name);
|
|
372
498
|
// Saves record exact inclusion; only legacy captures advance a watermark.
|
|
373
499
|
if (by?.v === 2) {
|
|
374
500
|
for (const [client, ranges] of frames) {
|
|
@@ -449,6 +575,7 @@ export function createRooms({ now = Date.now, newEpoch = randomUUID, liveBytes =
|
|
|
449
575
|
reservation.result = null;
|
|
450
576
|
return null;
|
|
451
577
|
}
|
|
578
|
+
if (room.compaction?.job) endJob(room.compaction.job);
|
|
452
579
|
const before = heldBy(room);
|
|
453
580
|
const { stores, transition } = reservation.target;
|
|
454
581
|
for (const name of room.stores.keys()) if (PAGE.test(name)) room.stores.delete(name);
|
|
@@ -465,6 +592,7 @@ export function createRooms({ now = Date.now, newEpoch = randomUUID, liveBytes =
|
|
|
465
592
|
bytes += store.bytes;
|
|
466
593
|
}
|
|
467
594
|
room.pageTransition = transition;
|
|
595
|
+
if (hostLimits) room.compaction = hostMode(transition.format === 'yjs');
|
|
468
596
|
reservation.target = null;
|
|
469
597
|
used += heldBy(room) - before - reservation.budget;
|
|
470
598
|
reservation.budget = 0;
|
|
@@ -547,7 +675,7 @@ export function createRooms({ now = Date.now, newEpoch = randomUUID, liveBytes =
|
|
|
547
675
|
if (!store || store.ephemeral) throw new PersistentReservationError('invalid-target');
|
|
548
676
|
}
|
|
549
677
|
const currentHeld = heldBy(record.room);
|
|
550
|
-
const projected =
|
|
678
|
+
const projected = projectRoom(record.room);
|
|
551
679
|
reconcileFile(projected, record.key, html, {});
|
|
552
680
|
for (const [name, store] of record.room.stores) {
|
|
553
681
|
if (!store.ephemeral && !projected.stores.has(name)) throw new PersistentReservationError('budget');
|
|
@@ -738,6 +866,8 @@ export function createRooms({ now = Date.now, newEpoch = randomUUID, liveBytes =
|
|
|
738
866
|
}
|
|
739
867
|
const nKey = frame.name + '\n' + clientId;
|
|
740
868
|
const last = room.lastN.get(nKey);
|
|
869
|
+
const admission = room.compaction && frame.name === 'page' && !receiptHas(last, frame.n) ? admit(room, store, frame, ops) : null;
|
|
870
|
+
if (admission?.error) return admission;
|
|
741
871
|
// Answered with the seq the sender's last frame landed at, so the tab knows
|
|
742
872
|
// which applied state holds it.
|
|
743
873
|
if (receiptHas(last, frame.n)) return { duplicate: true, seq: frame.n <= last.n ? last.seq : last.savedSeq, epoch: room.epoch };
|
|
@@ -793,8 +923,13 @@ export function createRooms({ now = Date.now, newEpoch = randomUUID, liveBytes =
|
|
|
793
923
|
}
|
|
794
924
|
if (bytes > LIVE_LIMITS.storeBytes || roomBytes(room) - store.bytes + bytes > LIVE_LIMITS.roomBytes) {
|
|
795
925
|
undo();
|
|
926
|
+
if (admission) return admission.over(bytes);
|
|
796
927
|
return { error: 'too-large' };
|
|
797
928
|
}
|
|
929
|
+
if (admission && bytes > admission.limit) {
|
|
930
|
+
undo();
|
|
931
|
+
return admission.over(bytes);
|
|
932
|
+
}
|
|
798
933
|
const logged = done.length ? byteLength(JSON.stringify(ops)) : 0;
|
|
799
934
|
const grows = done.length ? bytes - store.bytes + logAfter(store, logged) - store.logBytes : 0;
|
|
800
935
|
const retiredBytes = victim === null ? 0 : receiptBytes(room.lastN.get(victim));
|
|
@@ -819,19 +954,28 @@ export function createRooms({ now = Date.now, newEpoch = randomUUID, liveBytes =
|
|
|
819
954
|
room.lastSentAt.set(clientId, now());
|
|
820
955
|
room.seen.set(clientId, now());
|
|
821
956
|
if (PAGE.test(frame.name) && typeof frame.adopted === 'string' && frame.adopted.length <= 64) room.adopted = frame.adopted;
|
|
957
|
+
if (admission && done.length) admission.admitted(bytes);
|
|
822
958
|
const writerGone = room.writer && connected && !connected.includes(room.writer);
|
|
823
959
|
if ((!room.writer || writerGone) && (!connected || connected.includes(clientId))) room.writer = clientId;
|
|
824
960
|
return { seq: room.seq, epoch: room.epoch, name: frame.name, ops, writer: room.writer, checkpoint: pressed(store), receiptGeneration: room.receiptGeneration ?? 0, ...(transition && { pageGeneration: transition.id }) };
|
|
825
961
|
},
|
|
826
962
|
|
|
827
963
|
/** Everything a joining tab needs, with clientId's own receipt per store. Values are the room's own objects: serialize, never mutate. With since in this room's epoch, moves carries each store's log entries after since.seq (null where the log no longer reaches back that far). */
|
|
828
|
-
state(room, clientId = null, since = undefined) {
|
|
964
|
+
state(room, clientId = null, since = undefined, { compaction: capable } = {}) {
|
|
829
965
|
const stores = {};
|
|
966
|
+
const legacy = room.compaction?.legacy;
|
|
967
|
+
if (legacy && clientId && capable !== undefined) {
|
|
968
|
+
if (capable === hostLimits.version) legacy.delete(clientId);
|
|
969
|
+
else legacy.add(clientId);
|
|
970
|
+
}
|
|
971
|
+
const hidden = Boolean(legacy && (capable !== undefined ? capable !== hostLimits.version : clientId && legacy.has(clientId)));
|
|
830
972
|
for (const [name, s] of room.stores) {
|
|
973
|
+
if (hidden && name === 'page') continue;
|
|
831
974
|
const seen = {};
|
|
832
975
|
const mine = clientId ? room.lastN.get(name + '\n' + clientId) : null;
|
|
833
976
|
if (mine) seen[clientId] = mine.n;
|
|
834
|
-
stores[name] = { value: s.value, ephemeral: s.ephemeral, from: s.from, landed: s.landed, floor: s.floor, last: s.last, seen, ...(mine?.saved?.length && { saved: mine.saved })
|
|
977
|
+
stores[name] = { value: s.value, ephemeral: s.ephemeral, from: s.from, landed: s.landed, floor: s.floor, last: s.last, seen, ...(mine?.saved?.length && { saved: mine.saved }),
|
|
978
|
+
...(name === 'page' && room.compaction && { compaction: { v: hostLimits.version, loose: hostLimits.loose, activeBytes: hostLimits.activeBytes } }) };
|
|
835
979
|
}
|
|
836
980
|
// A client's own read (since is its { epoch, seq }, or null for a first
|
|
837
981
|
// read) carries moves: those after its seq in its own epoch, else this
|
|
@@ -876,7 +1020,11 @@ export function createRooms({ now = Date.now, newEpoch = randomUUID, liveBytes =
|
|
|
876
1020
|
if (room.writer !== clientId) return { writerChanged: false, writer: room.writer };
|
|
877
1021
|
let best = null;
|
|
878
1022
|
let bestAt = -1;
|
|
879
|
-
|
|
1023
|
+
// A tab too old to see the page store cannot checkpoint it: a capable tab goes first.
|
|
1024
|
+
const seen = remaining.filter((candidate) => room.seen.has(candidate));
|
|
1025
|
+
const legacy = room.compaction?.legacy;
|
|
1026
|
+
const capable = legacy ? seen.filter((candidate) => !legacy.has(candidate)) : seen;
|
|
1027
|
+
for (const id of capable.length ? capable : seen) {
|
|
880
1028
|
const at = room.lastSentAt.get(id) ?? 0;
|
|
881
1029
|
if (at > bestAt) {
|
|
882
1030
|
best = id;
|
|
@@ -898,6 +1046,9 @@ export function createRooms({ now = Date.now, newEpoch = randomUUID, liveBytes =
|
|
|
898
1046
|
*/
|
|
899
1047
|
sweep(connectedOf = () => [], { mayExpire = () => true } = {}) {
|
|
900
1048
|
const t = now();
|
|
1049
|
+
for (const room of rooms.values()) {
|
|
1050
|
+
if (room.compaction?.job && t >= room.compaction.job.deadline) endJob(room.compaction.job);
|
|
1051
|
+
}
|
|
901
1052
|
for (const [key, room] of rooms) {
|
|
902
1053
|
if (!roomIsDirty(room) && mayExpire(room) && !pageReservations.has(key) && !persistentReservations.has(key) && room.emptySince !== null && t - room.emptySince >= LIVE_LIMITS.retainMs) {
|
|
903
1054
|
used -= heldBy(room);
|
|
@@ -948,6 +1099,107 @@ export function createRooms({ now = Date.now, newEpoch = randomUUID, liveBytes =
|
|
|
948
1099
|
return changed;
|
|
949
1100
|
},
|
|
950
1101
|
|
|
1102
|
+
onCompactionDue(listener) {
|
|
1103
|
+
onDue = typeof listener === 'function' ? listener : () => {};
|
|
1104
|
+
},
|
|
1105
|
+
|
|
1106
|
+
beginCompaction(room, { overflow = false } = {}) {
|
|
1107
|
+
const state = room?.compaction;
|
|
1108
|
+
if (!state || rooms.get(room.key) !== room || state.failed) return null;
|
|
1109
|
+
if (state.job) {
|
|
1110
|
+
if (now() < state.job.deadline) return null;
|
|
1111
|
+
endJob(state.job);
|
|
1112
|
+
}
|
|
1113
|
+
const store = room.stores.get('page');
|
|
1114
|
+
if (!store || pageReservations.has(room.key) || persistentReservations.has(room.key)) return null;
|
|
1115
|
+
const plan = planCompaction(store.value.y, { overflow }, hostLimits);
|
|
1116
|
+
if (!plan) return null;
|
|
1117
|
+
const reserved = plan.sources.reduce((sum, [k, v]) => sum + k.length + v.length, 0);
|
|
1118
|
+
if (used + reserved > liveBytes) return null;
|
|
1119
|
+
state.fence += 1;
|
|
1120
|
+
const token = Object.freeze({ key: room.key, epoch: room.epoch, fence: state.fence });
|
|
1121
|
+
const job = { token, room, store, epoch: room.epoch, generation: room.pageTransition?.id ?? null, kind: plan.kind,
|
|
1122
|
+
sources: new Map(plan.sources), physical: Object.keys(store.value.y), reserved, deadline: now() + hostLimits.leaseMs, deletesAtBegin: state.deletesSince };
|
|
1123
|
+
state.job = job;
|
|
1124
|
+
used += reserved;
|
|
1125
|
+
jobs.set(token, job);
|
|
1126
|
+
return { token, kind: plan.kind, sources: plan.sources };
|
|
1127
|
+
},
|
|
1128
|
+
|
|
1129
|
+
abandonCompaction(token, { failed = null } = {}) {
|
|
1130
|
+
const job = jobs.get(token);
|
|
1131
|
+
if (!job) return false;
|
|
1132
|
+
jobs.delete(token);
|
|
1133
|
+
const ended = endJob(job);
|
|
1134
|
+
if (ended && failed) job.room.compaction.failed = { reason: failed, at: now() };
|
|
1135
|
+
return ended;
|
|
1136
|
+
},
|
|
1137
|
+
|
|
1138
|
+
commitCompaction(token, result) {
|
|
1139
|
+
const job = jobs.get(token);
|
|
1140
|
+
if (!job) return { refused: 'stale-token' };
|
|
1141
|
+
jobs.delete(token);
|
|
1142
|
+
const { room, store } = job;
|
|
1143
|
+
if (!endJob(job)) return { refused: 'stale-token' };
|
|
1144
|
+
if (rooms.get(room.key) !== room || room.epoch !== job.epoch || room.stores.get('page') !== store ||
|
|
1145
|
+
(room.pageTransition?.id ?? null) !== job.generation) return { refused: 'stale-token' };
|
|
1146
|
+
const y = store.value.y;
|
|
1147
|
+
const retire = job.physical.filter((k) => {
|
|
1148
|
+
const part = PART_KEY.exec(k);
|
|
1149
|
+
return job.sources.has(part ? part[1] : k) || (part && job.kind === 'full');
|
|
1150
|
+
});
|
|
1151
|
+
if (retire.some((k) => !Object.hasOwn(y, k))) return { refused: 'sources-changed' };
|
|
1152
|
+
const held = new Map(entriesOf(Object.fromEntries(retire.map((k) => [k, y[k]]))));
|
|
1153
|
+
for (const [k, v] of job.sources) if (held.get(k) !== v) return { refused: 'sources-changed' };
|
|
1154
|
+
if (typeof result?.update !== 'string' || !checkUpdate(result.update)) return { refused: 'bad-candidate' };
|
|
1155
|
+
const key = `${job.kind === 'full' ? 'c' : 't'}${textHash(room.epoch).slice(0, 8)}-${(room.seq + 1).toString(36)}`;
|
|
1156
|
+
if (Object.hasOwn(y, key)) return { refused: 'bad-candidate' };
|
|
1157
|
+
const ops = [...retire.map((k) => ({ op: 'del', path: ['y', k] })), { op: 'set', path: ['y', key], value: result.update }].map(validateOp);
|
|
1158
|
+
let value = store.value;
|
|
1159
|
+
let bytes = store.bytes;
|
|
1160
|
+
const done = [];
|
|
1161
|
+
for (const op of ops) {
|
|
1162
|
+
const rec = applyTracked(value, op);
|
|
1163
|
+
if (!rec) continue;
|
|
1164
|
+
done.push(rec);
|
|
1165
|
+
bytes += sizeChange(rec, sizeOf, bytes);
|
|
1166
|
+
value = rec.root;
|
|
1167
|
+
}
|
|
1168
|
+
if (bytes > hostLimits.activeBytes && bytes > store.bytes) {
|
|
1169
|
+
for (let i = done.length - 1; i >= 0; i--) undoTracked(done[i]);
|
|
1170
|
+
// A full fold of the whole page that still does not fit: the page is as small as it
|
|
1171
|
+
// gets, so the next write is refused as full instead of waiting for another fold.
|
|
1172
|
+
if (job.kind === 'full' && retire.length === Object.keys(y).length) {
|
|
1173
|
+
room.compaction.lastFull = { bytes: store.bytes, seq: room.seq, deletesAtBegin: job.deletesAtBegin };
|
|
1174
|
+
room.compaction.deletesSince -= job.deletesAtBegin;
|
|
1175
|
+
}
|
|
1176
|
+
return { refused: 'too-large' };
|
|
1177
|
+
}
|
|
1178
|
+
const clean = store.landed === store.last;
|
|
1179
|
+
const logged = byteLength(JSON.stringify(ops));
|
|
1180
|
+
used += bytes - store.bytes + logAfter(store, logged) - store.logBytes;
|
|
1181
|
+
store.value = value;
|
|
1182
|
+
store.bytes = bytes;
|
|
1183
|
+
room.seq += 1;
|
|
1184
|
+
store.last = room.seq;
|
|
1185
|
+
if (clean) store.landed = store.last;
|
|
1186
|
+
store.log.push({ seq: room.seq, ops, bytes: logged, from: COMPACTOR, n: 0 });
|
|
1187
|
+
store.logBytes += logged;
|
|
1188
|
+
evict(store);
|
|
1189
|
+
const state = room.compaction;
|
|
1190
|
+
for (const k of job.sources.keys()) {
|
|
1191
|
+
state.retired.add(k);
|
|
1192
|
+
if (state.retired.size > hostLimits.retired) state.retired.delete(state.retired.values().next().value);
|
|
1193
|
+
}
|
|
1194
|
+
if (job.kind === 'full') {
|
|
1195
|
+
state.lastFull = { bytes, seq: room.seq, deletesAtBegin: job.deletesAtBegin };
|
|
1196
|
+
state.deletesSince -= job.deletesAtBegin;
|
|
1197
|
+
}
|
|
1198
|
+
if (pageShape(store.value.y, hostLimits).loose >= hostLimits.soft || !pageShape(store.value.y, hostLimits).normal) due(room);
|
|
1199
|
+
return { seq: room.seq, epoch: room.epoch, name: 'page', ops, from: COMPACTOR, kind: job.kind, landed: store.landed,
|
|
1200
|
+
...(room.pageTransition && { pageGeneration: room.pageTransition.id }) };
|
|
1201
|
+
},
|
|
1202
|
+
|
|
951
1203
|
/** Every room's key. */
|
|
952
1204
|
keys() {
|
|
953
1205
|
return [...rooms.keys()];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { parentPort } from 'node:worker_threads';
|
|
2
|
+
import { buildCandidate } from '../../live/page-compaction.js';
|
|
3
|
+
|
|
4
|
+
parentPort.on('message', ({ id, kind, sources }) => {
|
|
5
|
+
let result;
|
|
6
|
+
try {
|
|
7
|
+
result = buildCandidate({ kind, sources });
|
|
8
|
+
} catch (error) {
|
|
9
|
+
result = { failure: 'crashed', message: error?.message || String(error) };
|
|
10
|
+
}
|
|
11
|
+
parentPort.postMessage({ id, ...result });
|
|
12
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Worker } from 'node:worker_threads';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
|
|
4
|
+
const RETRY_MS = [1000, 5000, 30000];
|
|
5
|
+
|
|
6
|
+
const MODULE_URL = typeof __dirname === 'string' ? pathToFileURL(`${__dirname}/`).href : import.meta.url;
|
|
7
|
+
const WORKER_FILE = typeof __PAGE_COMPACTION_WORKER__ === 'string' ? __PAGE_COMPACTION_WORKER__ : './page-compaction-worker.js';
|
|
8
|
+
|
|
9
|
+
export const pageCompactionWorkerUrl = () => new URL(WORKER_FILE, MODULE_URL);
|
|
10
|
+
|
|
11
|
+
export function createPageCompactor({ rooms, onCommit = () => {}, workerUrl = pageCompactionWorkerUrl(),
|
|
12
|
+
timeoutMs = 30000, WorkerClass = Worker, setTimer = setTimeout, clearTimer = clearTimeout } = {}) {
|
|
13
|
+
const queue = new Map();
|
|
14
|
+
const retries = new Map();
|
|
15
|
+
let worker = null;
|
|
16
|
+
let running = null;
|
|
17
|
+
let ids = 0;
|
|
18
|
+
let closed = false;
|
|
19
|
+
|
|
20
|
+
// A worker's events count only while it is still the current one: a terminated worker
|
|
21
|
+
// exits on a later tick, after its replacement may already be running the next job.
|
|
22
|
+
const spawn = () => {
|
|
23
|
+
const instance = new WorkerClass(workerUrl);
|
|
24
|
+
worker = instance;
|
|
25
|
+
instance.unref?.();
|
|
26
|
+
instance.on('message', (message) => { if (worker === instance) finish(message); });
|
|
27
|
+
instance.on('error', () => { if (worker === instance) crashed(); });
|
|
28
|
+
instance.on('exit', () => { if (worker !== instance) return; worker = null; if (running) crashed(); });
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function crashed() {
|
|
32
|
+
const job = running;
|
|
33
|
+
if (!job) return;
|
|
34
|
+
running = null;
|
|
35
|
+
clearTimer(job.timer);
|
|
36
|
+
worker?.terminate?.();
|
|
37
|
+
worker = null;
|
|
38
|
+
const tries = (retries.get(job.room) ?? 0) + 1;
|
|
39
|
+
retries.set(job.room, tries);
|
|
40
|
+
rooms.abandonCompaction(job.token, tries > RETRY_MS.length ? { failed: 'crashed' } : {});
|
|
41
|
+
if (tries <= RETRY_MS.length) setTimer(() => due(job.room, { overflow: job.overflow }), RETRY_MS[tries - 1]);
|
|
42
|
+
pump();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function finish({ id, ...result }) {
|
|
46
|
+
const job = running;
|
|
47
|
+
if (!job || job.id !== id) return;
|
|
48
|
+
running = null;
|
|
49
|
+
clearTimer(job.timer);
|
|
50
|
+
if (result.failure) {
|
|
51
|
+
rooms.abandonCompaction(job.token, { failed: result.failure });
|
|
52
|
+
} else {
|
|
53
|
+
const answer = rooms.commitCompaction(job.token, result);
|
|
54
|
+
if (answer.seq !== undefined) {
|
|
55
|
+
retries.delete(job.room);
|
|
56
|
+
try { onCommit(job.room, answer); } catch {}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
pump();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function pump() {
|
|
63
|
+
if (closed || running || !queue.size) return;
|
|
64
|
+
const [room, { overflow }] = queue.entries().next().value;
|
|
65
|
+
queue.delete(room);
|
|
66
|
+
const begun = rooms.beginCompaction(room, { overflow });
|
|
67
|
+
if (!begun) { pump(); return; }
|
|
68
|
+
if (!worker) spawn();
|
|
69
|
+
const id = ++ids;
|
|
70
|
+
running = { id, room, overflow, token: begun.token, timer: setTimer(crashed, timeoutMs) };
|
|
71
|
+
worker.postMessage({ id, kind: begun.kind, sources: begun.sources });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function due(room, { overflow = false } = {}) {
|
|
75
|
+
const queued = queue.get(room);
|
|
76
|
+
queue.set(room, { overflow: overflow || Boolean(queued?.overflow) });
|
|
77
|
+
queueMicrotask(pump);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
rooms.onCompactionDue(due);
|
|
81
|
+
return {
|
|
82
|
+
get running() { return running !== null; },
|
|
83
|
+
close() {
|
|
84
|
+
closed = true;
|
|
85
|
+
rooms.onCompactionDue(null);
|
|
86
|
+
if (running) { clearTimer(running.timer); rooms.abandonCompaction(running.token); running = null; }
|
|
87
|
+
const current = worker;
|
|
88
|
+
worker = null;
|
|
89
|
+
return current?.terminate?.();
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|