@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/core/save.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import throttle from "../lib/throttle.js";
|
|
13
13
|
import Mutation from "../lib/mutation.js";
|
|
14
|
-
import { isDraftMode, isEditMode, isOwner } from "./is-edit-mode.js";
|
|
14
|
+
import { isDraftMode, isEditMode, isOwner, learnedHostWritable } from "./is-edit-mode.js";
|
|
15
15
|
import {
|
|
16
16
|
saveCapturedHtml,
|
|
17
17
|
getPageContents,
|
|
@@ -19,10 +19,17 @@ import {
|
|
|
19
19
|
addDocumentTransform,
|
|
20
20
|
isSaveInProgress,
|
|
21
21
|
saveFateIsUnknown,
|
|
22
|
-
withKeepalive
|
|
22
|
+
withKeepalive,
|
|
23
|
+
setExitSave,
|
|
24
|
+
reserveSaveLane,
|
|
25
|
+
releaseSaveLane,
|
|
26
|
+
keepaliveHeld,
|
|
27
|
+
knownPageFormatFloor,
|
|
28
|
+
PAGE_FORMAT_META
|
|
23
29
|
} from "./save-core.js";
|
|
24
30
|
import { captureForComparisonAndDirty, captureForSaveAndComparison, captureForDirtyCheck, captureSaveClone, releaseCaptureArtifact } from "./snapshot.js";
|
|
25
31
|
import { saveLimit, saveLimits, inspectSaveSize } from "./save-size.js";
|
|
32
|
+
import { SLICED_SAVE_MIN_BYTES } from "./save-slices.js";
|
|
26
33
|
import { paintSignature } from "../lib/derived.js";
|
|
27
34
|
import { NO_TRIGGER_AUTOSAVE_SELECTOR } from "../lib/region-policy.js";
|
|
28
35
|
import { seedEtag, learnConditionalSaves, lastSeenEtag, recordEtag } from "./etag.js";
|
|
@@ -40,7 +47,7 @@ import { initUserGesture, markExplicitSave, clearExplicitSave } from "../lib/use
|
|
|
40
47
|
import { bytesUnsaved } from "./unsaved-warning.js";
|
|
41
48
|
import { bootPhase } from "./boot-phase.js";
|
|
42
49
|
import { createAutosaveScheduler } from "../lib/autosave-scheduler.js";
|
|
43
|
-
import { noteRecoveryCapture, noteRecoverySettled } from "./recovery.js";
|
|
50
|
+
import { beginRecoveryCapture, finishRecoveryCapture, noteRecoveryCapture, noteRecoverySettled } from "./recovery.js";
|
|
44
51
|
|
|
45
52
|
// Keep this library's own root state out of the saved bytes.
|
|
46
53
|
//
|
|
@@ -544,7 +551,7 @@ function heldForPreview(callback, resolve) {
|
|
|
544
551
|
const skipped = skipped_(saveHoldReason());
|
|
545
552
|
callback(skipped);
|
|
546
553
|
resolve(skipped);
|
|
547
|
-
return
|
|
554
|
+
return skipped;
|
|
548
555
|
}
|
|
549
556
|
|
|
550
557
|
// ============================================
|
|
@@ -632,6 +639,29 @@ function sessionClosed(msg) {
|
|
|
632
639
|
return skipped_(msg);
|
|
633
640
|
}
|
|
634
641
|
|
|
642
|
+
// A save this client will not send: the host has already answered that this page may not be
|
|
643
|
+
// written. The shape is any refused save's, carrying the `forbidden` code a host's own 403
|
|
644
|
+
// comes back with and `sent: false` because no request was made, so the chip, the notice and
|
|
645
|
+
// the exit barrier read it exactly as they read that refusal. Sending it would only earn the
|
|
646
|
+
// same answer, later.
|
|
647
|
+
function forbidden_() {
|
|
648
|
+
return {
|
|
649
|
+
ok: false,
|
|
650
|
+
msg: 'You are not allowed to save this document.',
|
|
651
|
+
msgType: 'forbidden',
|
|
652
|
+
code: 'forbidden',
|
|
653
|
+
status: 403,
|
|
654
|
+
sent: false,
|
|
655
|
+
etag: null
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// The host has said no, and nothing here overrides that: a page it will not store is not
|
|
660
|
+
// written by an autosave, a Cmd+S, or the exit's own flush.
|
|
661
|
+
function refusesSave() {
|
|
662
|
+
return learnedHostWritable() === false;
|
|
663
|
+
}
|
|
664
|
+
|
|
635
665
|
// The exit's own flush is not an ordinary save: it is the barrier writing the bytes the
|
|
636
666
|
// exit is about to account for, so it runs under a permit. Nothing else does — a Cmd+S or
|
|
637
667
|
// a [trigger-save] press while admission is closed stays parked.
|
|
@@ -833,6 +863,346 @@ export function setLastSavedContents(val) {
|
|
|
833
863
|
|
|
834
864
|
const monotonic = () => (typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now());
|
|
835
865
|
|
|
866
|
+
// The size at which a document's work hands the page back, shared with the source map's
|
|
867
|
+
// own install and repair. Re-exported so this module keeps answering for the save side.
|
|
868
|
+
export { SLICED_SAVE_MIN_BYTES };
|
|
869
|
+
// The main-thread time one preparation slice may spend before handing the page back.
|
|
870
|
+
const SLICED_SAVE_BUDGET_MS = 4;
|
|
871
|
+
// What a save answers when the page changed underneath its preparation. A skip, because
|
|
872
|
+
// nothing was sent: the bytes are still here, and the next save starts over.
|
|
873
|
+
const SAVE_PREPARE_CANCELLED = 'Save cancelled: the page changed while it prepared';
|
|
874
|
+
|
|
875
|
+
// The save preparing across tasks right now, for a flush to wait on: it has taken the lane
|
|
876
|
+
// and sent nothing, so its own events have not fired yet.
|
|
877
|
+
let preparing = null;
|
|
878
|
+
// The lazily loaded driver for the steps. Null until something asks, so a page whose
|
|
879
|
+
// documents are small never loads it, and false-keyed off for good if it cannot load.
|
|
880
|
+
let prepDriver = null;
|
|
881
|
+
let prepLoading = null;
|
|
882
|
+
let prepFailed = false;
|
|
883
|
+
|
|
884
|
+
function loadPreparer() {
|
|
885
|
+
if (!prepLoading) {
|
|
886
|
+
prepLoading = import('./save-prepare.js').then(
|
|
887
|
+
(mod) => { prepDriver = mod; return mod; },
|
|
888
|
+
(err) => {
|
|
889
|
+
prepFailed = true;
|
|
890
|
+
console.info('clayjs: sliced save off (' + (err && err.message ? err.message : err) + '); saves prepare in one task');
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
return prepLoading;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// The size of the bytes the last save wrote. A document that has never saved is judged by
|
|
899
|
+
// the document it was served as, which the browser has already measured.
|
|
900
|
+
function servedSize() {
|
|
901
|
+
try {
|
|
902
|
+
return performance.getEntriesByType('navigation')[0]?.decodedBodySize || 0;
|
|
903
|
+
} catch {
|
|
904
|
+
return 0;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
const lastSavedSize = () => (typeof lastSavedBytes === 'string' ? lastSavedBytes.length : servedSize());
|
|
908
|
+
|
|
909
|
+
// Whether this save prepares across tasks instead of in the caller's.
|
|
910
|
+
//
|
|
911
|
+
// A hidden tab has no typing to protect and timers there are throttled: it keeps the
|
|
912
|
+
// one-task path. A keepalive save belongs to a page that is going away and cannot wait for
|
|
913
|
+
// a task boundary, so the unload flush keeps it too.
|
|
914
|
+
function preparesSliced() {
|
|
915
|
+
if (prepFailed) return false;
|
|
916
|
+
if (document.hidden) return false;
|
|
917
|
+
if (keepaliveHeld()) return false;
|
|
918
|
+
return lastSavedSize() >= SLICED_SAVE_MIN_BYTES;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Why a preparing job must not be sent any more, or null when it may go out.
|
|
922
|
+
//
|
|
923
|
+
// Ordinary typing is deliberately NOT one of these: the capture is a checkpoint, and
|
|
924
|
+
// saving it later is the point. Only an invalidation cancels: the session, the mode, the
|
|
925
|
+
// host's permission, a newer write to this document, a morph, or a document format that
|
|
926
|
+
// rose while the job prepared. A rebuilt source map is not one: a render laid out against
|
|
927
|
+
// the old pairing falls back to today's bytes, and a stale artifact is never adopted.
|
|
928
|
+
function prepareInvalidation(ctx, epoch, morph) {
|
|
929
|
+
if (morph) return 'morph';
|
|
930
|
+
if (saveGenerationAt !== epoch.generation || !isEditMode) return 'mode';
|
|
931
|
+
if (!savesAdmitted(ctx.permit)) return 'session';
|
|
932
|
+
if (refusesSave()) return 'refused';
|
|
933
|
+
if (baselineRevision !== epoch.baselines) return 'newer save';
|
|
934
|
+
if (Math.max(knownPageFormatFloor(), ctx.pageFormatFloor) > epoch.floor) return 'floor';
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// A merge arrives as a burst of applied frames: a retry waits for a quiet moment, never
|
|
939
|
+
// longer than two seconds, so it captures the page the burst left behind.
|
|
940
|
+
function mergeSettled(quietMs = 150, maxMs = 2000) {
|
|
941
|
+
return new Promise((resolve) => {
|
|
942
|
+
const started = monotonic();
|
|
943
|
+
let timer = null;
|
|
944
|
+
const done = () => { document.removeEventListener('clay:sync-applied', bump); resolve(); };
|
|
945
|
+
const bump = () => {
|
|
946
|
+
clearTimeout(timer);
|
|
947
|
+
timer = setTimeout(done, Math.max(0, Math.min(quietMs, maxMs - (monotonic() - started))));
|
|
948
|
+
};
|
|
949
|
+
document.addEventListener('clay:sync-applied', bump);
|
|
950
|
+
bump();
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Everything one capture decides, for both save paths.
|
|
956
|
+
*
|
|
957
|
+
* The same bytes, the same refusals and the same receipts either way: a capture taken in
|
|
958
|
+
* one task and a capture prepared across tasks differ only in when their verification
|
|
959
|
+
* ran, never in what is asked of the result. `noteRecovery` is how the async path records
|
|
960
|
+
* coverage from its pinned candidates; `release` gives back whatever the capture holds
|
|
961
|
+
* when nothing is sent.
|
|
962
|
+
*/
|
|
963
|
+
async function settleCaptured({
|
|
964
|
+
explicit,
|
|
965
|
+
forced = false,
|
|
966
|
+
forSave,
|
|
967
|
+
forComparison,
|
|
968
|
+
forDirty,
|
|
969
|
+
captureId,
|
|
970
|
+
gateToken,
|
|
971
|
+
costMs,
|
|
972
|
+
pageFormatFloor = 0,
|
|
973
|
+
quietConflict = false,
|
|
974
|
+
laneHeld = false,
|
|
975
|
+
lane = null,
|
|
976
|
+
capture,
|
|
977
|
+
noteRecovery,
|
|
978
|
+
release,
|
|
979
|
+
callback,
|
|
980
|
+
}) {
|
|
981
|
+
noteRecovery();
|
|
982
|
+
|
|
983
|
+
if (explicit && !forced) {
|
|
984
|
+
// An explicit save asks the DIRTY question: write anything the person would
|
|
985
|
+
// otherwise lose, including an edit inside a batching region that was never
|
|
986
|
+
// going to autosave itself.
|
|
987
|
+
unsavedChanges = (forDirty !== lastSavedDirty);
|
|
988
|
+
logSaveCheck('savePage dirty check', !unsavedChanges);
|
|
989
|
+
|
|
990
|
+
// Skip if content hasn't changed. Clearing the explicit intent here is the
|
|
991
|
+
// whole point of scoping it: pressing Save on a clean page sends nothing, and
|
|
992
|
+
// leaving "a human asked for this" armed would hand it to the next
|
|
993
|
+
// background write and hide exactly the clobber the guard watches for.
|
|
994
|
+
if (!unsavedChanges) {
|
|
995
|
+
clearExplicitSave();
|
|
996
|
+
gateClearIfUnchanged(gateToken);
|
|
997
|
+
release();
|
|
998
|
+
const skipped = skipped_('No changes to save');
|
|
999
|
+
settleRecoveryCapture(captureId, skipped);
|
|
1000
|
+
if (typeof callback === 'function') callback(skipped);
|
|
1001
|
+
return { result: skipped, dispatched: false };
|
|
1002
|
+
}
|
|
1003
|
+
} else if (!explicit) {
|
|
1004
|
+
// For autosave: while the page is still settling, content must differ from
|
|
1005
|
+
// BOTH the load-time baseline and the last save, so module setup churn cannot
|
|
1006
|
+
// trigger a save. Once settled, the baseline veto is disarmed and only the
|
|
1007
|
+
// last save matters — otherwise undoing back to the page's original state
|
|
1008
|
+
// can never be persisted, because it matches the baseline forever.
|
|
1009
|
+
const differsFromBaseline = !baselineActive || forComparison !== baselineContents;
|
|
1010
|
+
const differsFromLastSave = forComparison !== lastSavedContents;
|
|
1011
|
+
|
|
1012
|
+
logSaveCheck('throttled vs baseline', !differsFromBaseline);
|
|
1013
|
+
logSaveCheck('throttled vs lastSave', !differsFromLastSave);
|
|
1014
|
+
|
|
1015
|
+
if (!(differsFromBaseline && differsFromLastSave)) {
|
|
1016
|
+
// This capture's comparison leaves out batching (no-trigger-autosave)
|
|
1017
|
+
// regions, which the gate counts, so on a page that has one the dirty
|
|
1018
|
+
// domain must match too before the gate is cleared.
|
|
1019
|
+
let clear = false;
|
|
1020
|
+
try {
|
|
1021
|
+
clear = !differsFromLastSave && (!document.querySelector(NO_TRIGGER_AUTOSAVE_SELECTOR)
|
|
1022
|
+
|| (pageMaybeDirty() && forDirty === lastSavedDirty));
|
|
1023
|
+
} catch (err) {
|
|
1024
|
+
console.error('savePageThrottled: dirty domain check failed', err);
|
|
1025
|
+
}
|
|
1026
|
+
if (clear) gateClearIfUnchanged(gateToken);
|
|
1027
|
+
autosaveScheduler.noteCost(costMs);
|
|
1028
|
+
release();
|
|
1029
|
+
const skipped = skipped_('No changes to save');
|
|
1030
|
+
settleRecoveryCapture(captureId, skipped);
|
|
1031
|
+
if (typeof callback === 'function') callback(skipped);
|
|
1032
|
+
return { result: skipped, dispatched: false };
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
unsavedChanges = true;
|
|
1036
|
+
|
|
1037
|
+
// A save that is not this lane's own (a raw saveHtml) may still be on the
|
|
1038
|
+
// wire. The capture has already been taken, so answer like the explicit lane
|
|
1039
|
+
// and keep these newer bytes queued behind it. A prepared save holds the lane
|
|
1040
|
+
// itself, so it can never be looking at somebody else's request here.
|
|
1041
|
+
if (!laneHeld && isSaveInProgress()) {
|
|
1042
|
+
pendingSave = true;
|
|
1043
|
+
release();
|
|
1044
|
+
const skipped = skipped_('Save already in progress');
|
|
1045
|
+
settleRecoveryCapture(captureId, skipped);
|
|
1046
|
+
if (typeof callback === 'function') callback(skipped);
|
|
1047
|
+
return { result: skipped, dispatched: false };
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Start debounced 'saving' state (only shows if save takes >500ms)
|
|
1052
|
+
setSavingState();
|
|
1053
|
+
|
|
1054
|
+
// Use the captured save path with our pre-captured content (avoids double capture)
|
|
1055
|
+
const comparison = { forComparison, forDirty };
|
|
1056
|
+
inFlightComparisons.add(comparison);
|
|
1057
|
+
ownSaveOnWire = true;
|
|
1058
|
+
return new Promise((resolve) => {
|
|
1059
|
+
saveCapturedHtml(forSave, captureId, (result) => {
|
|
1060
|
+
ownSaveOnWire = false;
|
|
1061
|
+
inFlightComparisons.delete(comparison);
|
|
1062
|
+
const completedAt = monotonic();
|
|
1063
|
+
applySaveResult(result, comparison.forComparison, comparison.forDirty, forced ? 'updated after force save' : 'updated after save', gateToken, forSave, captureId, quietConflict);
|
|
1064
|
+
if (!explicit) autosaveScheduler.noteCost(costMs + (monotonic() - completedAt));
|
|
1065
|
+
if (typeof callback === 'function') callback(result);
|
|
1066
|
+
resolve({ result, dispatched: true });
|
|
1067
|
+
// The save that queued behind this one goes out from here, in this request's own
|
|
1068
|
+
// task, which is where it has always gone out: a flush's own call then finds the
|
|
1069
|
+
// lane busy and waits that request out, which is what its 'clean' answer describes.
|
|
1070
|
+
// A prepared save holds the lane itself, so its caller does the draining instead.
|
|
1071
|
+
if (!laneHeld) drainPendingSave();
|
|
1072
|
+
}, { capture, pageFormatFloor, lane });
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/**
|
|
1077
|
+
* Prepare one capture across tasks and send it, or answer like a skip.
|
|
1078
|
+
*
|
|
1079
|
+
* The lane was reserved before this function's first await, so nothing else can start a
|
|
1080
|
+
* save on the page this capture read. Every outcome gives it back: a send hands it to
|
|
1081
|
+
* `runSave`, which settles it with the bookkeeping, and a cancellation gives it back here.
|
|
1082
|
+
*/
|
|
1083
|
+
async function runSlicedSave(ctx, lane) {
|
|
1084
|
+
// A morph replaces the page under a job that is only half verified. What it applied may
|
|
1085
|
+
// be a disk frame or a peer's edit, so these bytes describe a page that is gone.
|
|
1086
|
+
const applied = { over: false };
|
|
1087
|
+
const onApplied = () => { applied.over = true; };
|
|
1088
|
+
document.addEventListener('clay:sync-applied', onApplied);
|
|
1089
|
+
try {
|
|
1090
|
+
for (let round = 0; round < 2; round++) {
|
|
1091
|
+
const gateToken = gateCaptureToken();
|
|
1092
|
+
const epoch = {
|
|
1093
|
+
generation: saveGenerationAt,
|
|
1094
|
+
baselines: baselineRevision,
|
|
1095
|
+
floor: Math.max(knownPageFormatFloor(), ctx.pageFormatFloor),
|
|
1096
|
+
};
|
|
1097
|
+
const job = prepDriver.beginSaveCapture({
|
|
1098
|
+
requestRefresh: true,
|
|
1099
|
+
forRequest: true,
|
|
1100
|
+
pageFormatMarker: epoch.floor ? { name: PAGE_FORMAT_META, content: String(epoch.floor) } : null,
|
|
1101
|
+
});
|
|
1102
|
+
// The copies this capture could cover are the ones held right now: reading them
|
|
1103
|
+
// after the preparation would both miss A's coverage and claim a newer copy's.
|
|
1104
|
+
beginRecoveryCapture(job.captureId);
|
|
1105
|
+
let why = null;
|
|
1106
|
+
const signal = {
|
|
1107
|
+
get aborted() {
|
|
1108
|
+
const reason = prepareInvalidation(ctx, epoch, applied.over);
|
|
1109
|
+
if (reason) why = reason;
|
|
1110
|
+
return !!reason;
|
|
1111
|
+
},
|
|
1112
|
+
};
|
|
1113
|
+
let prepared;
|
|
1114
|
+
try {
|
|
1115
|
+
prepared = await prepDriver.runPreparedSteps(job.steps(), { budgetMs: SLICED_SAVE_BUDGET_MS, signal });
|
|
1116
|
+
} catch (err) {
|
|
1117
|
+
console.error(`${ctx.explicit ? 'savePage' : 'savePageThrottled'}: preparing the save failed`, err);
|
|
1118
|
+
job.drop();
|
|
1119
|
+
const result = { msg: err && err.message ? err.message : 'Save failed', msgType: 'error', code: err?.code ?? null, etag: null };
|
|
1120
|
+
setSaveState('error', result.msg, '', { sent: false, code: result.code });
|
|
1121
|
+
settleRecoveryCapture(job.captureId, result);
|
|
1122
|
+
if (typeof ctx.callback === 'function') ctx.callback(result);
|
|
1123
|
+
return { result, dispatched: false };
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
const late = prepared.aborted ? why : prepareInvalidation(ctx, epoch, applied.over);
|
|
1127
|
+
if (!late) {
|
|
1128
|
+
const captured = prepared.result;
|
|
1129
|
+
return await settleCaptured({
|
|
1130
|
+
explicit: ctx.explicit,
|
|
1131
|
+
forced: ctx.forced,
|
|
1132
|
+
forSave: captured.forSave,
|
|
1133
|
+
forComparison: captured.forComparison,
|
|
1134
|
+
forDirty: captured.forDirty,
|
|
1135
|
+
captureId: captured.captureId,
|
|
1136
|
+
gateToken,
|
|
1137
|
+
costMs: prepared.activeMs,
|
|
1138
|
+
pageFormatFloor: ctx.pageFormatFloor,
|
|
1139
|
+
quietConflict: ctx.quietConflict,
|
|
1140
|
+
laneHeld: true,
|
|
1141
|
+
lane,
|
|
1142
|
+
capture: captured,
|
|
1143
|
+
noteRecovery: () => finishRecoveryCapture(captured.captureId, captured.forSave),
|
|
1144
|
+
release: () => job.drop(),
|
|
1145
|
+
callback: ctx.callback,
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// Nothing is sent from a capture that cannot be trusted: the clones and the
|
|
1150
|
+
// candidate go, and the waiters hear the skip contract's answer.
|
|
1151
|
+
job.drop();
|
|
1152
|
+
const skipped = skipped_(SAVE_PREPARE_CANCELLED);
|
|
1153
|
+
// A session's closed admission cancelled this preparation: the page is between
|
|
1154
|
+
// modes, and the attempt is remembered so `resume` replays it.
|
|
1155
|
+
if (late === 'session') parkedAutosave = true;
|
|
1156
|
+
settleRecoveryCapture(job.captureId, skipped);
|
|
1157
|
+
// The document format rose under a job already prepared: its marker and its
|
|
1158
|
+
// verification both predate the floor, so the capture is taken again at the new one.
|
|
1159
|
+
if (late === 'floor' && round === 0) continue;
|
|
1160
|
+
// The person asked for this save: a merge that landed under it does not get to
|
|
1161
|
+
// swallow it. Once the merge settles the page is captured again, once.
|
|
1162
|
+
if (late === 'morph' && ctx.explicit && round === 0) {
|
|
1163
|
+
await mergeSettled();
|
|
1164
|
+
applied.over = false;
|
|
1165
|
+
// The retry captures again rather than asking the dirty gate: an editor's content
|
|
1166
|
+
// lives where the gate does not look, and the capture itself skips a clean page.
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
if (typeof ctx.callback === 'function') ctx.callback(skipped);
|
|
1170
|
+
return { result: skipped, dispatched: false };
|
|
1171
|
+
}
|
|
1172
|
+
const skipped = skipped_(SAVE_PREPARE_CANCELLED);
|
|
1173
|
+
if (typeof ctx.callback === 'function') ctx.callback(skipped);
|
|
1174
|
+
return { result: skipped, dispatched: false };
|
|
1175
|
+
} finally {
|
|
1176
|
+
document.removeEventListener('clay:sync-applied', onApplied);
|
|
1177
|
+
releaseSaveLane(lane);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/**
|
|
1182
|
+
* Start a prepared save, or answer false so the caller takes the one-task path.
|
|
1183
|
+
*
|
|
1184
|
+
* The lane is reserved and the capture is begun in THIS task, before anything awaits.
|
|
1185
|
+
*/
|
|
1186
|
+
function startSlicedSave(ctx) {
|
|
1187
|
+
const lane = reserveSaveLane();
|
|
1188
|
+
if (!lane) return false;
|
|
1189
|
+
const task = runSlicedSave(ctx, lane).catch((err) => {
|
|
1190
|
+
console.error('save: preparing the save failed', err);
|
|
1191
|
+
const result = { msg: err && err.message ? err.message : 'Save failed', msgType: 'error', code: err?.code ?? null, etag: null };
|
|
1192
|
+
setSaveState('error', result.msg, '', { sent: false, code: result.code });
|
|
1193
|
+
if (typeof ctx.callback === 'function') ctx.callback(result);
|
|
1194
|
+
return { result, dispatched: false };
|
|
1195
|
+
});
|
|
1196
|
+
preparing = task;
|
|
1197
|
+
task.then((outcome) => {
|
|
1198
|
+
if (preparing === task) preparing = null;
|
|
1199
|
+
ctx.resolve(outcome.result);
|
|
1200
|
+
ctx.settleSoonQueued(outcome.result);
|
|
1201
|
+
if (outcome.dispatched || pendingSave || pendingAutosave || soonQueuedWaiters.length) drainPendingSave();
|
|
1202
|
+
});
|
|
1203
|
+
return true;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
836
1206
|
/**
|
|
837
1207
|
* One save attempt, explicit or automatic.
|
|
838
1208
|
*
|
|
@@ -862,19 +1232,38 @@ function attemptSaveNow({ kind, permit = null }, callback = () => {}) {
|
|
|
862
1232
|
if (!isEditMode && !window.clay?.testMode) {
|
|
863
1233
|
if (explicit) clearExplicitSave();
|
|
864
1234
|
const skipped = skipped_('Not in edit mode');
|
|
1235
|
+
// The page left edit mode, so no later save is coming to take these callers.
|
|
1236
|
+
takeSoonQueued().forEach((done) => done(skipped));
|
|
865
1237
|
callback(skipped);
|
|
866
1238
|
return resolve(skipped);
|
|
867
1239
|
}
|
|
868
1240
|
|
|
869
1241
|
// A session has admission closed: the page is between modes, and neither lane writes
|
|
870
|
-
// until it decides.
|
|
1242
|
+
// until it decides. The attempt is remembered so `resume` replays it, explicit or not.
|
|
871
1243
|
if (!savesAdmitted(permit)) {
|
|
872
|
-
|
|
1244
|
+
parkedAutosave = true;
|
|
873
1245
|
const skipped = sessionClosed('Save session is quiesced');
|
|
1246
|
+
// The soon() callers waiting on this save hear its refusal: a queued waiter that is
|
|
1247
|
+
// never answered is a promise the page cannot settle, and the refusal is the answer
|
|
1248
|
+
// to the save it was moved to.
|
|
1249
|
+
if (explicit) takeSoonQueued().forEach((done) => done(skipped));
|
|
874
1250
|
callback(skipped);
|
|
875
1251
|
return resolve(skipped);
|
|
876
1252
|
}
|
|
877
1253
|
|
|
1254
|
+
// The host has told this tab this page is no longer writable. Nothing sends a request
|
|
1255
|
+
// the client already knows will be refused: the text stays here, and the answer is the
|
|
1256
|
+
// one a host's 403 gets, so the indicator says so and the exit refuses like a failed
|
|
1257
|
+
// save. The flush inside the exit barrier passes through here too.
|
|
1258
|
+
if (refusesSave()) {
|
|
1259
|
+
if (explicit) clearExplicitSave();
|
|
1260
|
+
const refused = forbidden_();
|
|
1261
|
+
setSaveState('error', refused.msg, '', { code: refused.code, status: refused.status, sent: false });
|
|
1262
|
+
takeSoonQueued().forEach((done) => done(refused));
|
|
1263
|
+
callback(refused);
|
|
1264
|
+
return resolve(refused);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
878
1267
|
// A save is already on the wire. Remember that a newer state is waiting rather
|
|
879
1268
|
// than dropping it: the in-flight request carries the older bytes, and if no
|
|
880
1269
|
// further mutation happens to retrigger autosave, the newer ones would never
|
|
@@ -882,7 +1271,13 @@ function attemptSaveNow({ kind, permit = null }, callback = () => {}) {
|
|
|
882
1271
|
// Not cleared here: pendingSave means this request is deferred, not
|
|
883
1272
|
// abandoned, and drainPendingSave is what eventually sends it.
|
|
884
1273
|
if (explicit) {
|
|
885
|
-
|
|
1274
|
+
const held = heldForPreview(callback, resolve);
|
|
1275
|
+
if (held) {
|
|
1276
|
+
// A hold's replay is an autosave, which takes nobody's queued waiters, and a hold
|
|
1277
|
+
// released without a replay drops the missed save: the hold is their answer.
|
|
1278
|
+
takeSoonQueued().forEach((done) => done(held));
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
886
1281
|
if (isSaveInProgress()) {
|
|
887
1282
|
pendingSave = true;
|
|
888
1283
|
const skipped = skipped_('Save already in progress');
|
|
@@ -902,6 +1297,15 @@ function attemptSaveNow({ kind, permit = null }, callback = () => {}) {
|
|
|
902
1297
|
callback(skipped);
|
|
903
1298
|
return resolve(skipped);
|
|
904
1299
|
}
|
|
1300
|
+
// A save preparing across tasks holds the lane and has sent nothing yet. This
|
|
1301
|
+
// records intent and nothing more: capturing the page again for it would be the
|
|
1302
|
+
// second full snapshot this split exists to prevent.
|
|
1303
|
+
if (preparing) {
|
|
1304
|
+
pendingAutosave = true;
|
|
1305
|
+
const skipped = skipped_('Save already in progress');
|
|
1306
|
+
callback(skipped);
|
|
1307
|
+
return resolve(skipped);
|
|
1308
|
+
}
|
|
905
1309
|
// The dirty gate counts every change no save has accounted for, and it is
|
|
906
1310
|
// the cheap half of the old dirtyMatchesLastSave check. When it says clean
|
|
907
1311
|
// there is nothing to compare, so answer like the old comparison did and
|
|
@@ -919,6 +1323,11 @@ function attemptSaveNow({ kind, permit = null }, callback = () => {}) {
|
|
|
919
1323
|
const prepare = window.clay?.source?.ensureForSave?.();
|
|
920
1324
|
if (prepare) return prepare.then(() => resolve(attemptSaveNow({ kind, permit }, callback)));
|
|
921
1325
|
|
|
1326
|
+
if (preparesSliced() && !prepDriver) {
|
|
1327
|
+
loadPreparer().then(() => resolve(attemptSaveNow({ kind, permit }, callback)));
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
922
1331
|
// From here this call answers: it either writes, or reports why it did not,
|
|
923
1332
|
// and the soon() callers waiting on the save queued behind the one that was
|
|
924
1333
|
// on the wire when they asked get that answer.
|
|
@@ -932,6 +1341,21 @@ function attemptSaveNow({ kind, permit = null }, callback = () => {}) {
|
|
|
932
1341
|
setOfflineStateQuiet();
|
|
933
1342
|
}
|
|
934
1343
|
|
|
1344
|
+
// A document this big prepares its save across tasks, so typing is never blocked for
|
|
1345
|
+
// the whole verification. The claim and the capture are both taken in this task.
|
|
1346
|
+
if (preparesSliced()) {
|
|
1347
|
+
if (startSlicedSave({
|
|
1348
|
+
explicit,
|
|
1349
|
+
forced: false,
|
|
1350
|
+
permit,
|
|
1351
|
+
pageFormatFloor: 0,
|
|
1352
|
+
quietConflict: false,
|
|
1353
|
+
settleSoonQueued,
|
|
1354
|
+
callback,
|
|
1355
|
+
resolve,
|
|
1356
|
+
})) return;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
935
1359
|
// Single capture: clone once, get all three versions
|
|
936
1360
|
// forSave strips non-persisted regions ([no-save]/[save-remove])
|
|
937
1361
|
// forComparison additionally strips every autosave-off region
|
|
@@ -961,99 +1385,22 @@ function attemptSaveNow({ kind, permit = null }, callback = () => {}) {
|
|
|
961
1385
|
|
|
962
1386
|
// The recovery hook sees the accepted capture's exact bytes, before anything is
|
|
963
1387
|
// sent, so a copy can be proved to be in them rather than merely to be nearby.
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
releaseCaptureArtifact(capture);
|
|
981
|
-
const skipped = skipped_('No changes to save');
|
|
982
|
-
settleRecoveryCapture(captureId, skipped);
|
|
983
|
-
callback(skipped);
|
|
984
|
-
resolve(skipped);
|
|
985
|
-
settleSoonQueued(skipped);
|
|
986
|
-
return;
|
|
987
|
-
}
|
|
988
|
-
} else {
|
|
989
|
-
// For autosave: while the page is still settling, content must differ from
|
|
990
|
-
// BOTH the load-time baseline and the last save, so module setup churn cannot
|
|
991
|
-
// trigger a save. Once settled, the baseline veto is disarmed and only the
|
|
992
|
-
// last save matters — otherwise undoing back to the page's original state
|
|
993
|
-
// can never be persisted, because it matches the baseline forever.
|
|
994
|
-
const differsFromBaseline = !baselineActive || forComparison !== baselineContents;
|
|
995
|
-
const differsFromLastSave = forComparison !== lastSavedContents;
|
|
996
|
-
|
|
997
|
-
logSaveCheck('throttled vs baseline', !differsFromBaseline);
|
|
998
|
-
logSaveCheck('throttled vs lastSave', !differsFromLastSave);
|
|
999
|
-
|
|
1000
|
-
if (!(differsFromBaseline && differsFromLastSave)) {
|
|
1001
|
-
// This capture's comparison leaves out batching (no-trigger-autosave)
|
|
1002
|
-
// regions, which the gate counts, so on a page that has one the dirty
|
|
1003
|
-
// domain must match too before the gate is cleared.
|
|
1004
|
-
let clear = false;
|
|
1005
|
-
try {
|
|
1006
|
-
clear = !differsFromLastSave && (!document.querySelector(NO_TRIGGER_AUTOSAVE_SELECTOR)
|
|
1007
|
-
|| (pageMaybeDirty() && forDirty === lastSavedDirty));
|
|
1008
|
-
} catch (err) {
|
|
1009
|
-
console.error('savePageThrottled: dirty domain check failed', err);
|
|
1010
|
-
}
|
|
1011
|
-
if (clear) gateClearIfUnchanged(gateToken);
|
|
1012
|
-
autosaveScheduler.noteCost(preparedMs);
|
|
1013
|
-
releaseCaptureArtifact(capture);
|
|
1014
|
-
const skipped = skipped_('No changes to save');
|
|
1015
|
-
settleRecoveryCapture(captureId, skipped);
|
|
1016
|
-
callback(skipped);
|
|
1017
|
-
resolve(skipped);
|
|
1018
|
-
return;
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
unsavedChanges = true;
|
|
1022
|
-
|
|
1023
|
-
// A save that is not this lane's own (a raw saveHtml) may still be on the
|
|
1024
|
-
// wire. The capture has already been taken, so answer like the explicit lane
|
|
1025
|
-
// and keep these newer bytes queued behind it.
|
|
1026
|
-
if (isSaveInProgress()) {
|
|
1027
|
-
pendingSave = true;
|
|
1028
|
-
releaseCaptureArtifact(capture);
|
|
1029
|
-
const skipped = skipped_('Save already in progress');
|
|
1030
|
-
settleRecoveryCapture(captureId, skipped);
|
|
1031
|
-
callback(skipped);
|
|
1032
|
-
resolve(skipped);
|
|
1033
|
-
return;
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
// Start debounced 'saving' state (only shows if save takes >500ms)
|
|
1038
|
-
setSavingState();
|
|
1039
|
-
|
|
1040
|
-
// Use the captured save path with our pre-captured content (avoids double capture)
|
|
1041
|
-
const comparison = { forComparison, forDirty };
|
|
1042
|
-
inFlightComparisons.add(comparison);
|
|
1043
|
-
ownSaveOnWire = true;
|
|
1044
|
-
saveCapturedHtml(forSave, captureId, (result) => {
|
|
1045
|
-
ownSaveOnWire = false;
|
|
1046
|
-
inFlightComparisons.delete(comparison);
|
|
1047
|
-
const completedAt = monotonic();
|
|
1048
|
-
applySaveResult(result, comparison.forComparison, comparison.forDirty, 'updated after save', gateToken, forSave, captureId);
|
|
1049
|
-
if (!explicit) autosaveScheduler.noteCost(preparedMs + (monotonic() - completedAt));
|
|
1050
|
-
if (typeof callback === 'function') {
|
|
1051
|
-
callback(result);
|
|
1052
|
-
}
|
|
1053
|
-
resolve(result);
|
|
1054
|
-
settleSoonQueued(result);
|
|
1055
|
-
drainPendingSave();
|
|
1056
|
-
}, { capture });
|
|
1388
|
+
settleCaptured({
|
|
1389
|
+
explicit,
|
|
1390
|
+
forSave,
|
|
1391
|
+
forComparison,
|
|
1392
|
+
forDirty,
|
|
1393
|
+
captureId,
|
|
1394
|
+
gateToken,
|
|
1395
|
+
costMs: preparedMs,
|
|
1396
|
+
capture,
|
|
1397
|
+
noteRecovery: () => noteRecoveryCapture({ captureId, html: forSave }),
|
|
1398
|
+
release: () => releaseCaptureArtifact(capture),
|
|
1399
|
+
callback,
|
|
1400
|
+
}).then((outcome) => {
|
|
1401
|
+
resolve(outcome.result);
|
|
1402
|
+
settleSoonQueued(outcome.result);
|
|
1403
|
+
});
|
|
1057
1404
|
});
|
|
1058
1405
|
}
|
|
1059
1406
|
|
|
@@ -1080,43 +1427,99 @@ function savePagePermitted(permit, callback = () => {}) {
|
|
|
1080
1427
|
/**
|
|
1081
1428
|
* Force-save the current page (skips dirty check).
|
|
1082
1429
|
*
|
|
1430
|
+
* `permit` is the exit's own save permit: the one write a closed session still admits.
|
|
1431
|
+
* Internal to the exit flush, which no page can reach.
|
|
1432
|
+
*
|
|
1083
1433
|
* @param {Function} callback - Optional callback for custom handling
|
|
1084
1434
|
* @returns {Promise<{msg: string, msgType: string}>}
|
|
1085
1435
|
*/
|
|
1086
|
-
export function savePageForce(callback = () => {}, { quietConflict = false, pageFormatFloor = 0 } = {}) {
|
|
1087
|
-
if (capabilityPending) return capabilityKnown.then(() => savePageForce(callback, { quietConflict, pageFormatFloor }));
|
|
1436
|
+
export function savePageForce(callback = () => {}, { quietConflict = false, pageFormatFloor = 0, permit = null } = {}) {
|
|
1437
|
+
if (capabilityPending) return capabilityKnown.then(() => savePageForce(callback, { quietConflict, pageFormatFloor, permit }));
|
|
1088
1438
|
return new Promise((resolve) => {
|
|
1089
1439
|
if (!isEditMode && !window.clay?.testMode) {
|
|
1090
1440
|
const skipped = skipped_('Not in edit mode');
|
|
1441
|
+
// A soon() caller queued behind a save that ends here hears this answer: the page left
|
|
1442
|
+
// edit mode, and no later save is coming to take them.
|
|
1443
|
+
takeSoonQueued().forEach((done) => done(skipped));
|
|
1091
1444
|
callback(skipped);
|
|
1092
1445
|
return resolve(skipped);
|
|
1093
1446
|
}
|
|
1094
1447
|
|
|
1095
1448
|
// A forced save is still an ordinary one for admission: only the exit's own flush
|
|
1096
|
-
// carries the permit.
|
|
1097
|
-
|
|
1449
|
+
// carries the permit. A revoked permit is refused even where admission has reopened,
|
|
1450
|
+
// so a continuation the exit already gave up on cannot write later input.
|
|
1451
|
+
if (!savesAdmitted(permit) || (permit && !permitHolds(permit))) {
|
|
1098
1452
|
const skipped = sessionClosed('Save session is quiesced');
|
|
1453
|
+
// A closed session parks its own attempt and sends nothing more without a permit, so
|
|
1454
|
+
// the refusal is the last word these callers get from this page.
|
|
1455
|
+
takeSoonQueued().forEach((done) => done(skipped));
|
|
1099
1456
|
callback(skipped);
|
|
1100
1457
|
return resolve(skipped);
|
|
1101
1458
|
}
|
|
1102
1459
|
|
|
1103
|
-
|
|
1460
|
+
// A force save is not a way around the host's answer: these bytes are the ones it has
|
|
1461
|
+
// already refused, and a request it will reject is not sent.
|
|
1462
|
+
if (refusesSave()) {
|
|
1463
|
+
const refused = forbidden_();
|
|
1464
|
+
setSaveState('error', refused.msg, '', { code: refused.code, status: refused.status, sent: false });
|
|
1465
|
+
// The host's refusal is the standing answer for this page, so nothing later will take
|
|
1466
|
+
// these callers: they hear it here.
|
|
1467
|
+
takeSoonQueued().forEach((done) => done(refused));
|
|
1468
|
+
callback(refused);
|
|
1469
|
+
return resolve(refused);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// A held save's replay is not the waiters' answer: a hold released without a replay
|
|
1473
|
+
// drops the missed save, and these callers would wait for one that never comes.
|
|
1474
|
+
const held = heldForPreview(callback, resolve);
|
|
1475
|
+
if (held) {
|
|
1476
|
+
takeSoonQueued().forEach((done) => done(held));
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1104
1479
|
|
|
1105
1480
|
if (isSaveInProgress()) {
|
|
1106
1481
|
pendingSave = true;
|
|
1107
1482
|
const skipped = skipped_('Save already in progress');
|
|
1483
|
+
// The parked attempt behind the request on the wire is not the save these callers
|
|
1484
|
+
// asked for, and its drain can be held for good: the skip is their answer.
|
|
1485
|
+
takeSoonQueued().forEach((done) => done(skipped));
|
|
1108
1486
|
callback(skipped);
|
|
1109
1487
|
return resolve(skipped);
|
|
1110
1488
|
}
|
|
1111
1489
|
|
|
1112
1490
|
const prepare = window.clay?.source?.ensureForSave?.();
|
|
1113
|
-
if (prepare) return prepare.then(() => resolve(savePageForce(callback, { quietConflict, pageFormatFloor })));
|
|
1491
|
+
if (prepare) return prepare.then(() => resolve(savePageForce(callback, { quietConflict, pageFormatFloor, permit })));
|
|
1114
1492
|
|
|
1115
1493
|
const wasOffline = !navigator.onLine;
|
|
1116
1494
|
if (wasOffline) {
|
|
1117
1495
|
setOfflineStateQuiet();
|
|
1118
1496
|
}
|
|
1119
1497
|
|
|
1498
|
+
// A forced save prepares like an ordinary one when the document is this big; it only
|
|
1499
|
+
// skips the dirty question, which `forced` says here.
|
|
1500
|
+
if (preparesSliced() && !prepDriver) {
|
|
1501
|
+
loadPreparer().then(() => resolve(savePageForce(callback, { quietConflict, pageFormatFloor, permit })));
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// From here this call answers, and the soon() callers waiting now are waiting on its
|
|
1506
|
+
// capture: one that queues after it stays queued for the save the drain starts.
|
|
1507
|
+
const soonQueued = takeSoonQueued();
|
|
1508
|
+
const settleSoonQueued = (result) => soonQueued.forEach((done) => done(result));
|
|
1509
|
+
|
|
1510
|
+
if (preparesSliced()) {
|
|
1511
|
+
if (startSlicedSave({
|
|
1512
|
+
explicit: true,
|
|
1513
|
+
forced: true,
|
|
1514
|
+
permit,
|
|
1515
|
+
pageFormatFloor,
|
|
1516
|
+
quietConflict,
|
|
1517
|
+
settleSoonQueued,
|
|
1518
|
+
callback,
|
|
1519
|
+
resolve,
|
|
1520
|
+
})) return;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1120
1523
|
let capture, forSave, forComparison, forDirty, captureId;
|
|
1121
1524
|
const gateToken = gateCaptureToken();
|
|
1122
1525
|
try {
|
|
@@ -1129,29 +1532,39 @@ export function savePageForce(callback = () => {}, { quietConflict = false, page
|
|
|
1129
1532
|
if (typeof callback === 'function') {
|
|
1130
1533
|
callback(result);
|
|
1131
1534
|
}
|
|
1132
|
-
|
|
1535
|
+
resolve(result);
|
|
1536
|
+
settleSoonQueued(result);
|
|
1537
|
+
return;
|
|
1133
1538
|
}
|
|
1134
1539
|
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1540
|
+
settleCaptured({
|
|
1541
|
+
explicit: true,
|
|
1542
|
+
forced: true,
|
|
1543
|
+
forSave,
|
|
1544
|
+
forComparison,
|
|
1545
|
+
forDirty,
|
|
1546
|
+
captureId,
|
|
1547
|
+
gateToken,
|
|
1548
|
+
costMs: 0,
|
|
1549
|
+
pageFormatFloor,
|
|
1550
|
+
quietConflict,
|
|
1551
|
+
capture,
|
|
1552
|
+
noteRecovery: () => noteRecoveryCapture({ captureId, html: forSave }),
|
|
1553
|
+
release: () => releaseCaptureArtifact(capture),
|
|
1554
|
+
callback,
|
|
1555
|
+
}).then((outcome) => {
|
|
1556
|
+
resolve(outcome.result);
|
|
1557
|
+
// The waiters asked for a save soon and this one landed: its result is their answer.
|
|
1558
|
+
settleSoonQueued(outcome.result);
|
|
1559
|
+
});
|
|
1152
1560
|
});
|
|
1153
1561
|
}
|
|
1154
1562
|
|
|
1563
|
+
// The exit's own checkpoint, through the slot save-core holds for it: a page exit has
|
|
1564
|
+
// to save this tab's own input while admission is closed, and only a save minted here
|
|
1565
|
+
// can do it. Set at load, read only by the live plugin's exit flush.
|
|
1566
|
+
setExitSave((permit) => savePageForce(() => {}, { permit }));
|
|
1567
|
+
|
|
1155
1568
|
// Resolve on the next save that settles, or reject once the deadline has passed.
|
|
1156
1569
|
// Every settled state is one, not just success: a save that failed is an answer about
|
|
1157
1570
|
// the page's bytes too, and the caller decides what to do with it.
|
|
@@ -1211,37 +1624,54 @@ export async function flushSave({ keepalive = false, timeoutMs = 15000, permit =
|
|
|
1211
1624
|
// the generation that owns the page now, so a flush during a transition is still a
|
|
1212
1625
|
// write in this generation and never the next one's.
|
|
1213
1626
|
const held = permit || openSavePermit();
|
|
1627
|
+
const run = { over: false };
|
|
1214
1628
|
let timer = null;
|
|
1215
1629
|
const timeout = new Promise((_, reject) => {
|
|
1216
1630
|
timer = setTimeout(() => reject(flushError('Save timed out.', 'failed')), Math.max(0, timeoutMs));
|
|
1217
1631
|
});
|
|
1218
1632
|
try {
|
|
1219
|
-
return await Promise.race([flushBytes(keepalive, deadline, held), timeout]);
|
|
1633
|
+
return await Promise.race([flushBytes(keepalive, deadline, held, run), timeout]);
|
|
1220
1634
|
} catch (err) {
|
|
1221
1635
|
if (err && typeof err.state === 'string') throw err;
|
|
1222
1636
|
throw flushError(err?.message || 'Save failed', 'failed');
|
|
1223
1637
|
} finally {
|
|
1638
|
+
run.over = true;
|
|
1224
1639
|
clearTimeout(timer);
|
|
1225
1640
|
if (!permit) closeSavePermit(held);
|
|
1226
1641
|
}
|
|
1227
1642
|
}
|
|
1228
1643
|
|
|
1229
|
-
async function flushBytes(keepalive, deadline, permit) {
|
|
1644
|
+
async function flushBytes(keepalive, deadline, permit, run = { over: false }) {
|
|
1230
1645
|
// Load-time module churn is not the person's work: autosave never saves it, and
|
|
1231
1646
|
// neither does a flush. Wait for the baseline to settle (at most MAX_SETTLE_MS).
|
|
1232
1647
|
if (!baselineSettled()) await nextEvent('clay:baseline-settled', deadline);
|
|
1648
|
+
if (run.over) return null;
|
|
1233
1649
|
|
|
1234
1650
|
let saved = false;
|
|
1235
1651
|
let etag = null;
|
|
1236
1652
|
for (let attempt = 0; attempt < 4; attempt++) {
|
|
1237
|
-
|
|
1653
|
+
// A save still preparing holds the lane and has sent nothing, so it has fired none of
|
|
1654
|
+
// the events below: the flush waits for it rather than reading the page in the middle
|
|
1655
|
+
// of a capture it cannot see.
|
|
1656
|
+
if (preparing) {
|
|
1657
|
+
await preparing;
|
|
1658
|
+
if (run.over) return null;
|
|
1659
|
+
}
|
|
1660
|
+
if (isSaveInProgress()) {
|
|
1661
|
+
await settled(deadline);
|
|
1662
|
+
if (run.over) return null;
|
|
1663
|
+
}
|
|
1238
1664
|
if (isSaveConflicted()) throw flushError('This document has a conflict to resolve.', 'conflict');
|
|
1239
1665
|
const token = gateCaptureToken();
|
|
1240
1666
|
if (!bytesUnsaved()) {
|
|
1241
1667
|
gateClearIfUnchanged(token);
|
|
1242
1668
|
if (hasUnsavedState()) throw flushError('This document has unsaved work a save cannot write.', 'blocked');
|
|
1669
|
+
// Nothing was written because there was nothing to write: the bytes on the page are
|
|
1670
|
+
// the ones the last accepted save left, so work a closed session parked is covered.
|
|
1671
|
+
parkedWork = false;
|
|
1243
1672
|
return { state: saved ? 'saved' : 'clean', etag: etag ?? lastSeenEtag() };
|
|
1244
1673
|
}
|
|
1674
|
+
if (run.over) return null;
|
|
1245
1675
|
const result = await withKeepalive(keepalive, () => savePagePermitted(permit));
|
|
1246
1676
|
// The lane was busy (a drained pending save is on the wire): wait it out next turn.
|
|
1247
1677
|
if (result.msgType === 'skipped') continue;
|