@panphora/clayjs 1.16.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 +72519 -59839
- package/dist/m/attrs/refetch-on-save-ZUE7PSCI.js +1 -0
- package/dist/m/attrs/save-freeze-LWGF6XUN.js +1 -0
- package/dist/m/c-2GRUM77Q.js +30 -0
- package/dist/m/c-2KIEGNC4.js +1 -0
- package/dist/m/{c-5R4PTRG2.js → c-2KL3BD52.js} +1 -1
- package/dist/m/c-2YYDMLNZ.js +1 -0
- 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-VFX45IZQ.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-5T6NZHNM.js +1 -0
- package/dist/m/{c-YDH7Q3NY.js → c-5ZQV7KC4.js} +1 -1
- package/dist/m/{c-Z36J63PP.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-AMYEFA2E.js +1 -0
- package/dist/m/c-AQYCG7QW.js +1 -0
- package/dist/m/c-ATKAOH23.js +1 -0
- package/dist/m/c-B3JM5ZXC.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-BIJALSRD.js +1 -0
- package/dist/m/c-BJ7YQ5QZ.js +1 -0
- package/dist/m/c-BPF2NSPV.js +1 -0
- package/dist/m/{c-KIPQJX4B.js → c-BRLCTNMC.js} +1 -1
- package/dist/m/c-BUAYFEV5.js +1 -0
- package/dist/m/c-C6IPRG34.js +1 -0
- package/dist/m/{c-2JL6E75N.js → c-CAS3NND4.js} +1 -1
- package/dist/m/{c-4DRMS4QG.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-7VGZ7RU5.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-OYTFP5QN.js → c-GY422N3Q.js} +1 -1
- package/dist/m/c-HAWFNBYG.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-HRDCD3WN.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-IVTRVENZ.js +1 -0
- package/dist/m/c-IW6GGOU2.js +1 -0
- package/dist/m/c-JKZJIUIP.js +1 -0
- package/dist/m/{c-NZMNXVF6.js → c-JNLX3RJB.js} +1 -1
- package/dist/m/c-JRKY44DK.js +1 -0
- package/dist/m/c-K3ADYNLU.js +1 -0
- package/dist/m/c-KBS3UV6B.js +1 -0
- package/dist/m/c-KGHVFRR3.js +1 -0
- package/dist/m/c-KJJPV77R.js +1 -0
- package/dist/m/{c-CZUSOB7V.js → c-KNPDOMDY.js} +1 -1
- package/dist/m/{c-5Y6TNX5W.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-LR4CGEPT.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-NEL7HQW3.js +1 -0
- package/dist/m/c-NQJRVJ56.js +1 -0
- 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-MMUIUUZG.js → c-OPLUZR3A.js} +1 -1
- package/dist/m/{c-J7CA54IP.js → c-OTMKN2OO.js} +1 -1
- package/dist/m/{c-LTGMXXKO.js → c-OYRZ7XG2.js} +1 -1
- package/dist/m/c-P3OFYUMA.js +1 -0
- 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-SWRJFCNN.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-R27DWQKX.js +1 -0
- package/dist/m/{c-HVYXIS72.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-S2U6NJU4.js +1 -0
- package/dist/m/c-SPZPGD3R.js +1 -0
- package/dist/m/c-SW4YEG2D.js +8 -0
- package/dist/m/c-SZGYW5HT.js +1 -0
- package/dist/m/c-T637T7SN.js +1 -0
- package/dist/m/c-TJAXGIRV.js +1 -0
- package/dist/m/{c-KKWLDOPM.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-TQZ2JYHD.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-VG5XIN4T.js +1 -0
- package/dist/m/c-VILENZTD.js +1 -0
- package/dist/m/{c-33HUYEDI.js → c-VJYEVRYK.js} +1 -1
- package/dist/m/c-VR2BJ4AN.js +1 -0
- 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-X5TSXZYJ.js +2 -0
- 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/{c-HT33EQRN.js → c-ZWIOJGMT.js} +1 -1
- package/dist/m/core/admin-attrs-YY7T5FGU.js +1 -0
- 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-6EVJGNPL.js +1 -0
- package/dist/m/core/is-edit-mode-GCX6767E.js +1 -0
- package/dist/m/core/{persist-HKHA66IY.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/events/index-RN2JNGBV.js +1 -0
- package/dist/m/internals/index-TGLQINN3.js +1 -0
- package/dist/m/lib/cache-bust-MUMFXZK6.js +1 -0
- 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 +2509 -1474
- package/dist/m/options/options-EJMS3K24.js +1 -0
- package/dist/m/page/editor-6LRB3GE4.js +304 -0
- 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-OYO5GCAO.js +1 -0
- 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-JCHVPF7P.js +1 -0
- package/dist/m/plugins/quickcrop-BQ5YYFCH.js +26 -0
- package/dist/m/plugins/{richclay-RC3L2F6X.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-NHGJXZK2.js +1 -0
- 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-VBWXEXU2.js +1 -0
- 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 +1560 -889
- package/dist/page/themes/light-1.css +33 -8
- package/dist/server/core.cjs +1554 -311
- 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/boot-phase.js +2 -1
- package/src/core/conflict-download.js +3 -29
- package/src/core/conflict-notice-ui.js +4 -7
- package/src/core/download.js +32 -0
- package/src/core/host-meta.js +56 -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 +111 -32
- 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 +209 -40
- 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/autosave-scheduler.js +3 -4
- 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 +122 -27
- package/src/page/board-views.js +2 -16
- package/src/page/boot-pack.js +1 -1
- package/src/page/chart-tables.js +29 -0
- package/src/page/chart.js +411 -72
- package/src/page/checklist.js +10 -135
- package/src/page/checkpoint-proof.js +295 -0
- package/src/page/chrome.js +257 -40
- package/src/page/clipboard-shape.js +126 -0
- package/src/page/document-title-dom.js +12 -0
- package/src/page/document-title-runtime.js +15 -1
- package/src/page/document-title.js +4 -1
- package/src/page/drag.js +33 -3
- package/src/page/editor.js +207 -31
- package/src/page/emoji-data.js +162 -0
- 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/board.js +12 -3
- package/src/page/features/chart.js +11 -0
- package/src/page/features/checklist.js +428 -0
- package/src/page/features/dates.js +180 -0
- package/src/page/features/emoji.js +187 -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/mentions.js +188 -0
- package/src/page/features/registry.js +24 -1
- package/src/page/features/table.js +375 -85
- 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 +33 -0
- package/src/page/format-gate.js +68 -18
- package/src/page/format-host.js +71 -0
- package/src/page/format-native-commands.js +158 -0
- package/src/page/format-native.js +42 -0
- package/src/page/image-actions.js +64 -0
- package/src/page/import-markdown.js +262 -0
- package/src/page/inline.js +32 -8
- package/src/page/items.js +141 -11
- package/src/page/markdown.js +30 -2
- package/src/page/media.js +35 -119
- package/src/page/mention-picker.js +99 -0
- package/src/page/merge.js +1 -1
- package/src/page/placeholder.js +14 -1
- package/src/page/projection.js +156 -25
- 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 +67 -0
- package/src/page/save-states.js +14 -4
- package/src/page/schema.js +52 -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/content.css +25 -1
- package/src/page/themes/light-1.css +33 -8
- 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 +58 -0
- package/src/page/view.js +13 -0
- package/src/page/ybind/binding.js +116 -6
- 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 +2 -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 +125 -18
- package/src/page/ybind/recovery-proof.js +32 -9
- package/src/page/ybind/recovery-replay.js +905 -4
- 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 +126 -15
- 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 +362 -43
- 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 +7 -4
- 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/checkpoint/index.js +2 -2
- 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 +28 -14
- 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/attrs/refetch-on-save-NWPPXOCZ.js +0 -1
- package/dist/m/attrs/save-freeze-FUP4Q4X3.js +0 -1
- package/dist/m/c-2M73252O.js +0 -1
- package/dist/m/c-2SPVQSHC.js +0 -1
- package/dist/m/c-35NBGMZB.js +0 -1
- package/dist/m/c-3IQRECYS.js +0 -1
- package/dist/m/c-4B6H4G5V.js +0 -1
- package/dist/m/c-4FFHZM32.js +0 -1
- package/dist/m/c-4YSCRICO.js +0 -1
- package/dist/m/c-54RCDCBU.js +0 -1
- package/dist/m/c-5M4YEKCX.js +0 -4
- package/dist/m/c-5V3MOVJT.js +0 -1
- package/dist/m/c-66O32MV3.js +0 -28
- package/dist/m/c-6HMB24EV.js +0 -1
- package/dist/m/c-7O6AFR65.js +0 -1
- package/dist/m/c-ANQTJ3B5.js +0 -2
- package/dist/m/c-AYVOREBE.js +0 -1
- package/dist/m/c-B2ZXPOS3.js +0 -3
- package/dist/m/c-BMIZKMPV.js +0 -1
- package/dist/m/c-BTDI4Y23.js +0 -1
- package/dist/m/c-CJZSLMVQ.js +0 -1
- package/dist/m/c-DBE6ORSI.js +0 -1
- package/dist/m/c-EDHUR4MX.js +0 -1
- package/dist/m/c-EY6UOM54.js +0 -1
- package/dist/m/c-F3CKJTJS.js +0 -1
- package/dist/m/c-F7Y6FLPY.js +0 -1
- package/dist/m/c-FA4DZVTC.js +0 -1
- package/dist/m/c-G2BUZITR.js +0 -1
- package/dist/m/c-GB3JTV6E.js +0 -1
- package/dist/m/c-GBGWXTX4.js +0 -1
- package/dist/m/c-GHBCCVG4.js +0 -1
- package/dist/m/c-GJAEGGXY.js +0 -1
- package/dist/m/c-GOWBDM6J.js +0 -1
- package/dist/m/c-IB52R237.js +0 -1
- package/dist/m/c-IKWNE4R4.js +0 -1
- package/dist/m/c-IU3IYEU6.js +0 -1
- package/dist/m/c-IZ2EBRAJ.js +0 -1
- package/dist/m/c-KR4UW4KW.js +0 -1
- package/dist/m/c-KY3VJGUA.js +0 -8
- package/dist/m/c-LFPHCXY3.js +0 -1
- package/dist/m/c-LRPUO2MX.js +0 -1
- package/dist/m/c-LXENEBV7.js +0 -1
- package/dist/m/c-NMJRXKPC.js +0 -8
- package/dist/m/c-OIBUTDXI.js +0 -1
- package/dist/m/c-PPOSCAQD.js +0 -1
- package/dist/m/c-QIX5BJYN.js +0 -1
- package/dist/m/c-QQHEX6HA.js +0 -1
- package/dist/m/c-R7RMANJI.js +0 -9
- package/dist/m/c-RI4NT75Y.js +0 -1
- package/dist/m/c-RM7J7BV2.js +0 -1
- package/dist/m/c-RUJKSUQW.js +0 -1
- package/dist/m/c-SGCCC4FW.js +0 -25
- package/dist/m/c-T4VO4OCB.js +0 -1
- package/dist/m/c-TQEMVJCS.js +0 -1
- package/dist/m/c-TR6RMDQ7.js +0 -3
- package/dist/m/c-UKHTLWUW.js +0 -1
- package/dist/m/c-UNN7LWCT.js +0 -1
- package/dist/m/c-UXBMRDZB.js +0 -235
- package/dist/m/c-V7PZZRG7.js +0 -1
- package/dist/m/c-VL52ADDR.js +0 -1
- package/dist/m/c-WNYBLIOZ.js +0 -1
- package/dist/m/c-WWDBYXLK.js +0 -1
- package/dist/m/c-XNNW2CWK.js +0 -1
- package/dist/m/c-Y6USGTS5.js +0 -5
- package/dist/m/c-YG2NJYS6.js +0 -135
- package/dist/m/c-Z4VRC7GU.js +0 -7
- package/dist/m/c-Z6K52OLP.js +0 -19
- package/dist/m/c-ZDY2MDAA.js +0 -21
- package/dist/m/c-ZUCOKOHT.js +0 -1
- package/dist/m/core/admin-attrs-VGNLAUDK.js +0 -1
- package/dist/m/core/autosave-E4PEFPZ6.js +0 -1
- package/dist/m/core/conflict-notice-VQ6Y64PH.js +0 -1
- package/dist/m/core/host-permission-XN3K2IW3.js +0 -1
- package/dist/m/core/is-edit-mode-JXHDVGUL.js +0 -1
- package/dist/m/core/recovery-TBBSICE2.js +0 -1
- package/dist/m/core/save-WUCHTQBE.js +0 -1
- package/dist/m/core/save-core-FJVHT5M3.js +0 -1
- package/dist/m/core/snapshot-F3SWDZMM.js +0 -1
- package/dist/m/core/unsaved-warning-STKBHE6G.js +0 -1
- package/dist/m/events/index-23DPTUT4.js +0 -1
- package/dist/m/internals/index-WOVAP3RU.js +0 -1
- package/dist/m/lib/cache-bust-CGWT6ZJI.js +0 -1
- package/dist/m/loader-HXX6YNH5.js +0 -1
- package/dist/m/manifest-F22DEDAC.js +0 -1
- package/dist/m/manifest-edit-C3C7376B.js +0 -1
- package/dist/m/options/options-2LH76JQ3.js +0 -1
- package/dist/m/page/editor-XUTB2HB2.js +0 -189
- package/dist/m/plugins/ai-edit-ZQ4CGWIP.js +0 -6
- package/dist/m/plugins/demo-ZIBFAA3R.js +0 -1
- package/dist/m/plugins/indicator-PV4YW2ID.js +0 -1
- package/dist/m/plugins/live-NUTTXSBS.js +0 -1
- package/dist/m/plugins/menu-HCKO3DSP.js +0 -138
- package/dist/m/plugins/page-KSUH7G6J.js +0 -2
- package/dist/m/plugins/page-view-W73OW7WI.js +0 -1
- package/dist/m/plugins/people-AJJ32YCG.js +0 -1
- package/dist/m/plugins/quickcrop-QNY3PKLD.js +0 -26
- package/dist/m/plugins/sortable-YRMQX3ZU.js +0 -1
- package/dist/m/plugins/source-KHK5UUSK.js +0 -1
- package/dist/m/plugins/undo-OF2L3CXU.js +0 -1
- package/dist/m/plugins/upgrade-ZNYODP5Y.js +0 -1
- package/dist/m/plugins/upload-XSGNILUY.js +0 -1
- package/dist/m/plugins/visitor-drafts-XK5GW55Q.js +0 -21
- package/dist/m/plugins/visitor-drafts-owner-KSSH75EV.js +0 -1
- package/dist/m/plugins/wire-ZWVWKF76.js +0 -1
- package/dist/m/reader-core-ILWLXVMP.js +0 -68
- package/dist/m/route/route-KOCZSEGN.js +0 -1
- package/dist/m/sync/live-sync-4AUVXVJU.js +0 -1
- package/dist/m/ui/index-EVHKMKBL.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-RC3L2F6X.js.LEGAL.txt → richclay-KLGETK77.js.LEGAL.txt} +0 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import * as Y from '../../vendor/yjs.vendor.js';
|
|
2
|
+
import { createRooms, LIVE_LIMITS } from './live-data-rooms.js';
|
|
3
|
+
import { COMPACTION, ROOM_ERROR_STATUS, buildCandidate, pageShape } from '../../live/page-compaction.js';
|
|
4
|
+
import { serializeLive, cloneJson, stampOf } from './live-data-ops.js';
|
|
5
|
+
import { entriesOf } from '../../page/ybind/parts.js';
|
|
6
|
+
import { rangesOf, within, docRanges } from '../../page/ybind/containment.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The D13 page-room conformance fixture: a seeded run of several tabs, restarts,
|
|
10
|
+
* resets and legacy file writes over one page room, driven through an adapter
|
|
11
|
+
* that stands in for a host's wiring of the room. Every run throws with the seed
|
|
12
|
+
* and the invariant it broke. The module reads no files: the caller passes the
|
|
13
|
+
* template HTML in, so a host runs the same fixture over its own HTTP handlers,
|
|
14
|
+
* error-status table, broadcast path and compactor.
|
|
15
|
+
*
|
|
16
|
+
* The adapter is a host's page room as its clients see it. All of its operations
|
|
17
|
+
* act on the room the host serves for `key`, and every one of them answers in
|
|
18
|
+
* wire terms:
|
|
19
|
+
*
|
|
20
|
+
* limits
|
|
21
|
+
* The host's compaction limits: { version, loose, soft, activeBytes,
|
|
22
|
+
* leaseMs, tailFloor }. The fixture asserts against these.
|
|
23
|
+
*
|
|
24
|
+
* advanceClock(ms)
|
|
25
|
+
* Move the host's clock forward by ms. The fixture advances time, it never
|
|
26
|
+
* reads it.
|
|
27
|
+
*
|
|
28
|
+
* onCompactionDue(listener)
|
|
29
|
+
* Register the host's hook: listener({ key, overflow }) whenever a room of
|
|
30
|
+
* that key wants a compaction. Replaces any earlier listener.
|
|
31
|
+
*
|
|
32
|
+
* open({ key, html, nodeId })
|
|
33
|
+
* restart({ key, html, nodeId })
|
|
34
|
+
* reset({ key, html, nodeId })
|
|
35
|
+
* Start the room for key from html: `open` on the host's registry, `restart`
|
|
36
|
+
* on a fresh registry (a new epoch, every job and frame stale), `reset` over
|
|
37
|
+
* the existing room. Answer { status: 200, body: { key, epoch, seq } }, or
|
|
38
|
+
* { status: 409, body: { error } } when the host refused.
|
|
39
|
+
*
|
|
40
|
+
* readState({ key, tabId, capable })
|
|
41
|
+
* The JSON the client receives for a state read: { epoch, seq, writer,
|
|
42
|
+
* stores: { page: { value, ... }, ... }, ... }, with the page store's value
|
|
43
|
+
* as the client's re-read carries it. `tabId` is the reading client (null for
|
|
44
|
+
* a broadcast's read) and `capable` says whether it advertises the page
|
|
45
|
+
* compaction capability (default true). Answers { status: 404, body: null }
|
|
46
|
+
* when the host has no room for key.
|
|
47
|
+
*
|
|
48
|
+
* apply({ key, tabId, frame })
|
|
49
|
+
* Apply one frame { name, n, epoch, pageGeneration, compaction, ops } from
|
|
50
|
+
* tabId, atomically. Answer { status: 200, body: { duplicate, seq, epoch, ... } }
|
|
51
|
+
* when the host took it, { status, body: { error, ... } } with the host's
|
|
52
|
+
* status for a refusal (the error code and any retryAfter), or
|
|
53
|
+
* { status: 409, body: { stale: true, epoch } } when the frame's epoch does
|
|
54
|
+
* not match the room the host serves.
|
|
55
|
+
*
|
|
56
|
+
* fileWritten({ key, html })
|
|
57
|
+
* The file for key was written. Answer { status: 200, body: { valueChanged,
|
|
58
|
+
* landed } } (landed: the store names the file now reaches), or
|
|
59
|
+
* { status: 404, body: null } when the host has no such room.
|
|
60
|
+
*
|
|
61
|
+
* usage({ key })
|
|
62
|
+
* What the host reports the room holds: { reported, receiptBytes,
|
|
63
|
+
* transitionBytes, reservedBytes, stores: { [name]: { bytes, logBytes } } }.
|
|
64
|
+
* `reported` is the host's own budget figure for every room it serves.
|
|
65
|
+
*
|
|
66
|
+
* beginCompaction({ key, overflow })
|
|
67
|
+
* Start a compaction job. Answer { status: 200, body: { token, kind, sources } }
|
|
68
|
+
* (sources: the [key, value] entries the candidate must cover, as the host's
|
|
69
|
+
* compactor receives them), or { status: 409, body: null } when no job began.
|
|
70
|
+
*
|
|
71
|
+
* buildCandidate({ job })
|
|
72
|
+
* Build the candidate for job ({ token, kind, sources }). Answer
|
|
73
|
+
* { status: 200, body: { update } } or { status: 409, body: { failure } }.
|
|
74
|
+
*
|
|
75
|
+
* commitCompaction({ token, candidate })
|
|
76
|
+
* Commit the built candidate. Answer { status: 200, body: { kind, seq, epoch, ... } },
|
|
77
|
+
* or { status: 409, body: { refused } } with the host's refusal code.
|
|
78
|
+
*
|
|
79
|
+
* abandonCompaction({ token })
|
|
80
|
+
* Drop job. Answer { status: 200, body: { abandoned: true } } or
|
|
81
|
+
* { status: 409, body: { abandoned: false } } when the token was stale.
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
const NET = 'net';
|
|
85
|
+
const TAG = /merge="page" clay="live"(?: live-at="[^"]*")?>(.*?)<\/script>/;
|
|
86
|
+
|
|
87
|
+
function rng(seed) {
|
|
88
|
+
let s = seed >>> 0 || 1;
|
|
89
|
+
return () => ((s = (Math.imul(s, 1664525) + 1013904223) >>> 0) / 4294967296);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The default adapter: one page room hosted by createRooms. Refusals answer with
|
|
94
|
+
* the host's status table (ROOM_ERROR_STATUS), reads answer with the JSON that
|
|
95
|
+
* goes to a client, due compactions are begun, built and committed inline, and
|
|
96
|
+
* restart swaps in a fresh registry with a new epoch and the same clock.
|
|
97
|
+
*/
|
|
98
|
+
export function createRoomsAdapter({ now = null, newEpoch = null, compaction = null } = {}) {
|
|
99
|
+
if (!compaction) throw new Error('createRoomsAdapter needs compaction limits');
|
|
100
|
+
const limits = Object.freeze({ ...COMPACTION, ...(typeof compaction === 'object' ? compaction : {}) });
|
|
101
|
+
let offset = 0;
|
|
102
|
+
let epochs = 0;
|
|
103
|
+
const at = () => (typeof now === 'function' ? now() : 0) + offset;
|
|
104
|
+
const epochOf = typeof newEpoch === 'function' ? newEpoch : () => `e${++epochs}`;
|
|
105
|
+
let listener = () => {};
|
|
106
|
+
let room = null;
|
|
107
|
+
const registry = () => {
|
|
108
|
+
const rooms = createRooms({ now: at, newEpoch: epochOf, compaction: limits });
|
|
109
|
+
rooms.onCompactionDue((target, { overflow }) => listener({ key: target.key, overflow }));
|
|
110
|
+
return rooms;
|
|
111
|
+
};
|
|
112
|
+
let rooms = registry();
|
|
113
|
+
const current = (key) => (room && (key === undefined || room.key === key) ? room : (key === undefined ? null : rooms.get(key)));
|
|
114
|
+
const opened = (next) => {
|
|
115
|
+
if (!next) return { status: 409, body: { error: 'rooms-full' } };
|
|
116
|
+
room = next;
|
|
117
|
+
return { status: 200, body: { key: next.key, epoch: next.epoch, seq: next.seq } };
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
limits,
|
|
122
|
+
advanceClock(ms = 0) {
|
|
123
|
+
offset += ms;
|
|
124
|
+
},
|
|
125
|
+
onCompactionDue(fn) {
|
|
126
|
+
listener = typeof fn === 'function' ? fn : () => {};
|
|
127
|
+
},
|
|
128
|
+
open({ key, html, nodeId = null } = {}) {
|
|
129
|
+
return opened(rooms.open(key, html, { nodeId }));
|
|
130
|
+
},
|
|
131
|
+
restart({ key, html, nodeId = null } = {}) {
|
|
132
|
+
rooms = registry();
|
|
133
|
+
room = null;
|
|
134
|
+
return opened(rooms.open(key, html, { nodeId }));
|
|
135
|
+
},
|
|
136
|
+
reset({ key, html, nodeId = null } = {}) {
|
|
137
|
+
return opened(rooms.reset(key, html, { nodeId }));
|
|
138
|
+
},
|
|
139
|
+
readState({ key, tabId = null, capable = true } = {}) {
|
|
140
|
+
const target = current(key);
|
|
141
|
+
if (!target) return { status: 404, body: null };
|
|
142
|
+
return { status: 200, body: rooms.state(target, tabId, undefined, { compaction: capable ? limits.version : undefined }) };
|
|
143
|
+
},
|
|
144
|
+
apply({ key, tabId = null, frame } = {}) {
|
|
145
|
+
const target = current(key);
|
|
146
|
+
if (!target) return { status: 404, body: { error: 'unknown-room' } };
|
|
147
|
+
const answer = rooms.apply(target, tabId, frame);
|
|
148
|
+
if (answer?.error) {
|
|
149
|
+
return { status: ROOM_ERROR_STATUS[answer.error] ?? 409, body: { error: answer.error, ...(answer.retryAfter !== undefined && { retryAfter: answer.retryAfter }), ...(answer.message && { message: answer.message }) } };
|
|
150
|
+
}
|
|
151
|
+
if (answer?.busy) return { status: 409, body: { error: 'busy', ...(answer.reason && { reason: answer.reason }) } };
|
|
152
|
+
if (answer?.stale) return { status: 409, body: { stale: true, epoch: answer.epoch ?? null } };
|
|
153
|
+
return { status: 200, body: answer };
|
|
154
|
+
},
|
|
155
|
+
fileWritten({ key, html } = {}) {
|
|
156
|
+
const out = rooms.fileWritten(key, html);
|
|
157
|
+
if (!out) return { status: 404, body: null };
|
|
158
|
+
return { status: 200, body: { valueChanged: Boolean(out.valueChanged), landed: out.landed ?? [] } };
|
|
159
|
+
},
|
|
160
|
+
usage({ key } = {}) {
|
|
161
|
+
const target = current(key);
|
|
162
|
+
if (!target) return { status: 404, body: null };
|
|
163
|
+
const stores = {};
|
|
164
|
+
for (const [name, s] of target.stores) stores[name] = { bytes: s.bytes, logBytes: s.logBytes };
|
|
165
|
+
return {
|
|
166
|
+
status: 200,
|
|
167
|
+
body: {
|
|
168
|
+
reported: rooms.used(),
|
|
169
|
+
receiptBytes: target.receiptBytes ?? 0,
|
|
170
|
+
transitionBytes: target.pageTransition?.bytes ?? 0,
|
|
171
|
+
reservedBytes: target.compaction?.job?.reserved ?? 0,
|
|
172
|
+
stores,
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
beginCompaction({ key, overflow = false } = {}) {
|
|
177
|
+
const target = current(key);
|
|
178
|
+
if (!target) return { status: 404, body: null };
|
|
179
|
+
const job = rooms.beginCompaction(target, { overflow });
|
|
180
|
+
if (!job) return { status: 409, body: null };
|
|
181
|
+
return { status: 200, body: job };
|
|
182
|
+
},
|
|
183
|
+
buildCandidate({ job } = {}) {
|
|
184
|
+
const result = buildCandidate({ kind: job.kind, sources: job.sources });
|
|
185
|
+
return { status: result.failure ? 409 : 200, body: result };
|
|
186
|
+
},
|
|
187
|
+
commitCompaction({ token, candidate } = {}) {
|
|
188
|
+
const answer = rooms.commitCompaction(token, candidate);
|
|
189
|
+
return { status: answer.refused ? 409 : 200, body: answer };
|
|
190
|
+
},
|
|
191
|
+
abandonCompaction({ token } = {}) {
|
|
192
|
+
const abandoned = rooms.abandonCompaction(token);
|
|
193
|
+
return { status: abandoned ? 200 : 409, body: { abandoned: Boolean(abandoned) } };
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Run the fixture. Answers the same stats object as before:
|
|
200
|
+
* { seed, admitted, refusedRetry, capacity, commits: { tail, full }, stale,
|
|
201
|
+
* staleChecked, restarts, resets, maxLoose, maxBytes, unchangedRefusals,
|
|
202
|
+
* reserve, landed, legacyWrites, legacyMerged, blocked, end }.
|
|
203
|
+
*/
|
|
204
|
+
export function runPageRoomConformance({ adapter, template, seed = 1, steps = 400, tabs: tabCount = 3, activeBytes = adapter.limits.activeBytes, tailFloor = adapter.limits.tailFloor, restarts = true, paste = 6000, log = false } = {}) {
|
|
205
|
+
const KEY = 'doc';
|
|
206
|
+
const r = rng(seed);
|
|
207
|
+
const pick = (n) => Math.floor(r() * n);
|
|
208
|
+
const limits = { activeBytes, loose: adapter.limits.loose, soft: adapter.limits.soft, leaseMs: adapter.limits.leaseMs, tailFloor };
|
|
209
|
+
const htmlOf = (value, at = null) => template.replace(TAG, () => `merge="page" clay="live"${at ? ` live-at="${at}"` : ''}>${JSON.stringify(value)}</script>`);
|
|
210
|
+
const fail = (message, extra = {}) => { throw Object.assign(new Error(`seed ${seed}: ${message}`), extra); };
|
|
211
|
+
const dueSet = new Set();
|
|
212
|
+
adapter.onCompactionDue(({ key, overflow }) => { if (key === KEY) dueSet.add({ key, overflow }); });
|
|
213
|
+
const opened = adapter.open({ key: KEY, html: template, nodeId: 1 });
|
|
214
|
+
if (opened.status !== 200) fail(`open refused with ${opened.status}`);
|
|
215
|
+
|
|
216
|
+
const state = () => {
|
|
217
|
+
const answer = adapter.readState({ key: KEY, capable: true });
|
|
218
|
+
if (answer.status !== 200) fail(`state read refused with ${answer.status}`);
|
|
219
|
+
return answer.body;
|
|
220
|
+
};
|
|
221
|
+
const page = () => state().stores.page.value;
|
|
222
|
+
const usage = () => {
|
|
223
|
+
const answer = adapter.usage({ key: KEY });
|
|
224
|
+
if (answer.status !== 200) fail(`usage read refused with ${answer.status}`);
|
|
225
|
+
return answer.body;
|
|
226
|
+
};
|
|
227
|
+
const held = (u) => u.receiptBytes + u.transitionBytes + u.reservedBytes +
|
|
228
|
+
Object.values(u.stores).reduce((total, s) => total + s.bytes + s.logBytes, 0);
|
|
229
|
+
|
|
230
|
+
const stats = { admitted: 0, refusedRetry: 0, capacity: 0, commits: { tail: 0, full: 0 }, stale: 0, staleChecked: 0, restarts: 0, resets: 0, maxLoose: 0, maxBytes: 0, unchangedRefusals: 0, reserve: 0, landed: 0 };
|
|
231
|
+
const pending = [];
|
|
232
|
+
let saved = cloneJson(page());
|
|
233
|
+
const admitted = [];
|
|
234
|
+
|
|
235
|
+
const tabs = Array.from({ length: tabCount }, (_, i) => {
|
|
236
|
+
const doc = new Y.Doc();
|
|
237
|
+
doc.clientID = 1000 + i;
|
|
238
|
+
const t = { id: `t${i}`, doc, inst: `${seed}${i}x${pick(1e6).toString(36)}`, counter: 0, n: 0, outbox: [], inflight: null, known: new Set(), journal: [], blocked: false, epoch: null };
|
|
239
|
+
doc.on('updateV2', (u, origin) => { if (origin === NET) return; t.journal.push(u); t.outbox.push(u); });
|
|
240
|
+
return t;
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const history = [{ value: saved, epoch: state().epoch, seq: state().seq }];
|
|
244
|
+
const legacy = { doc: new Y.Doc(), n: 0, writes: 0, merged: 0, keys: {} };
|
|
245
|
+
legacy.doc.clientID = 900;
|
|
246
|
+
|
|
247
|
+
const check = (where) => {
|
|
248
|
+
const u = usage();
|
|
249
|
+
const bytes = u.stores.page.bytes;
|
|
250
|
+
const shape = pageShape(page().y, limits);
|
|
251
|
+
stats.maxLoose = Math.max(stats.maxLoose, shape.loose);
|
|
252
|
+
stats.maxBytes = Math.max(stats.maxBytes, bytes);
|
|
253
|
+
if (bytes > LIVE_LIMITS.storeBytes) fail(`${where}: store over the hard cap`);
|
|
254
|
+
if (where === 'admit' && !shape.normal) fail(`${where}: admitted into a value out of normal form`, { shape });
|
|
255
|
+
if (where === 'admit' && bytes > activeBytes + 64 * 1024) fail(`${where}: active value ${bytes} over ${activeBytes} plus the delete reserve`);
|
|
256
|
+
if (bytes > activeBytes) stats.reserve++;
|
|
257
|
+
if (u.reported !== held(u)) fail(`${where}: budget ${u.reported} != ${held(u)}`);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const contains = (updates) => {
|
|
261
|
+
const d = new Y.Doc();
|
|
262
|
+
for (const [, v] of entriesOf(page().y)) Y.applyUpdateV2(d, Y.fromBase64(v));
|
|
263
|
+
const index = docRanges(d);
|
|
264
|
+
d.destroy();
|
|
265
|
+
return updates.every((u) => within(index, rangesOf(u)));
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const legacyWrite = () => {
|
|
269
|
+
const entry = history[pick(history.length)];
|
|
270
|
+
const base = entry.value;
|
|
271
|
+
for (const [, v] of entriesOf(base.y)) Y.applyUpdateV2(legacy.doc, Y.fromBase64(v), NET);
|
|
272
|
+
const got = [];
|
|
273
|
+
const on = (u, origin) => { if (origin !== NET) got.push(u); };
|
|
274
|
+
legacy.doc.on('updateV2', on);
|
|
275
|
+
const text = legacy.doc.getText('probe');
|
|
276
|
+
text.insert(pick(text.length + 1), 'L'.repeat(1 + pick(r() < 0.2 ? 20000 : 60)));
|
|
277
|
+
legacy.doc.off('updateV2', on);
|
|
278
|
+
const update = Y.mergeUpdatesV2(got);
|
|
279
|
+
const b64 = Y.toBase64(update);
|
|
280
|
+
const key = `u900-${(legacy.n++).toString(36)}`;
|
|
281
|
+
if (b64.length > 12 * 1024) {
|
|
282
|
+
const parts = Math.ceil(b64.length / (12 * 1024));
|
|
283
|
+
for (let i = 0; i < parts; i++) legacy.keys[`${key}.${i}.${parts}`] = b64.slice(i * 12 * 1024, (i + 1) * 12 * 1024);
|
|
284
|
+
} else legacy.keys[key] = b64;
|
|
285
|
+
const file = cloneJson(base);
|
|
286
|
+
Object.assign(file.y, legacy.keys);
|
|
287
|
+
if (r() < 0.3) file.y[`m900-${legacy.n}`] = Y.toBase64(Y.mergeUpdatesV2(entriesOf(base.y).map(([, v]) => Y.fromBase64(v))));
|
|
288
|
+
const keysBefore = Object.keys(page().y);
|
|
289
|
+
const valueBefore = cloneJson(page());
|
|
290
|
+
const at = stampOf(entry.epoch, entry.seq, serializeLive(file));
|
|
291
|
+
const out = adapter.fileWritten({ key: KEY, html: htmlOf(file, at) });
|
|
292
|
+
legacy.writes++;
|
|
293
|
+
const after = state();
|
|
294
|
+
if (!after.stores.page) fail('a merge dropped the page store');
|
|
295
|
+
const y = after.stores.page.value.y;
|
|
296
|
+
if (keysBefore.some((k) => !(k in y) || y[k] !== valueBefore.y[k])) fail('a file merge deleted or changed a room key');
|
|
297
|
+
if (!contains([update])) fail('a file merge lost the legacy edit');
|
|
298
|
+
if (out.body?.valueChanged) legacy.merged++;
|
|
299
|
+
if (out.body?.landed?.includes('page')) stats.landed++;
|
|
300
|
+
admitted.push(update);
|
|
301
|
+
const u = usage();
|
|
302
|
+
if (u.reported !== held(u)) fail('budget after merge');
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const sync = (t) => {
|
|
306
|
+
const y = page().y;
|
|
307
|
+
for (const [k, v] of entriesOf(y)) if (!t.known.has(k)) { t.known.add(k); Y.applyUpdateV2(t.doc, Y.fromBase64(v), NET); }
|
|
308
|
+
const epoch = state().epoch;
|
|
309
|
+
if (t.epoch !== epoch) {
|
|
310
|
+
t.epoch = epoch;
|
|
311
|
+
t.inflight = null;
|
|
312
|
+
const scratch = new Y.Doc();
|
|
313
|
+
for (const [, v] of entriesOf(y)) Y.applyUpdateV2(scratch, Y.fromBase64(v));
|
|
314
|
+
const diff = Y.encodeStateAsUpdateV2(t.doc, Y.encodeStateVector(scratch));
|
|
315
|
+
const covered = within(docRanges(scratch), rangesOf(Y.encodeStateAsUpdateV2(t.doc)));
|
|
316
|
+
scratch.destroy();
|
|
317
|
+
if (!covered) t.outbox.push(diff);
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const send = (t) => {
|
|
322
|
+
if (t.blocked) return;
|
|
323
|
+
if (!t.inflight) {
|
|
324
|
+
if (!t.outbox.length) return;
|
|
325
|
+
const merged = Y.mergeUpdatesV2(t.outbox);
|
|
326
|
+
t.outbox = [];
|
|
327
|
+
t.inflight = { key: `u${t.doc.clientID.toString(36)}-${t.inst}-${(t.counter++).toString(36)}`, value: Y.toBase64(merged), n: ++t.n, update: merged };
|
|
328
|
+
}
|
|
329
|
+
const f = t.inflight;
|
|
330
|
+
const before = serializeLive(page());
|
|
331
|
+
const seqBefore = state().seq;
|
|
332
|
+
const now = state();
|
|
333
|
+
const answer = adapter.apply({ key: KEY, tabId: t.id, frame: { name: 'page', n: f.n, epoch: now.epoch, pageGeneration: now.pageTransition?.id ?? null, compaction: 1, ops: [{ op: 'set', path: ['y', f.key], value: f.value }] } });
|
|
334
|
+
const body = answer.body ?? {};
|
|
335
|
+
if (body.error) {
|
|
336
|
+
if (serializeLive(page()) !== before || state().seq !== seqBefore) fail(`refusal ${body.error} changed the room`);
|
|
337
|
+
stats.unchangedRefusals++;
|
|
338
|
+
if (body.error === 'checkpoint-required') { stats.refusedRetry++; return; }
|
|
339
|
+
if (body.error === 'page-full') { stats.capacity++; t.blocked = true; return; }
|
|
340
|
+
fail(`unexpected refusal ${body.error}`);
|
|
341
|
+
}
|
|
342
|
+
if (body.stale) { t.inflight = null; return; }
|
|
343
|
+
if (!body.duplicate) { stats.admitted++; admitted.push(f.update); }
|
|
344
|
+
t.known.add(f.key);
|
|
345
|
+
t.inflight = null;
|
|
346
|
+
check('admit');
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const compactBegin = () => {
|
|
350
|
+
if (!dueSet.size) return;
|
|
351
|
+
const [{ overflow }] = dueSet;
|
|
352
|
+
dueSet.clear();
|
|
353
|
+
const answer = adapter.beginCompaction({ key: KEY, overflow });
|
|
354
|
+
if (answer.status === 200) pending.push({ job: answer.body, epoch: state().epoch, value: serializeLive(page()) });
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const compactFinish = () => {
|
|
358
|
+
if (!pending.length) return;
|
|
359
|
+
const entry = pending.shift();
|
|
360
|
+
const { job } = entry;
|
|
361
|
+
const choice = r();
|
|
362
|
+
if (choice < 0.08) {
|
|
363
|
+
adapter.advanceClock(limits.leaseMs + 1);
|
|
364
|
+
const next = adapter.beginCompaction({ key: KEY });
|
|
365
|
+
const before = serializeLive(page());
|
|
366
|
+
const result = adapter.buildCandidate({ job });
|
|
367
|
+
const answer = adapter.commitCompaction({ token: job.token, candidate: result.body });
|
|
368
|
+
stats.staleChecked++;
|
|
369
|
+
if (answer.status === 200) fail('an expired token committed');
|
|
370
|
+
if (serializeLive(page()) !== before) fail('a stale commit changed the room');
|
|
371
|
+
stats.stale++;
|
|
372
|
+
if (next.status === 200) pending.push({ job: next.body, epoch: state().epoch });
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (choice < 0.12) { adapter.abandonCompaction({ token: job.token }); return; }
|
|
376
|
+
const built = adapter.buildCandidate({ job });
|
|
377
|
+
if (built.status !== 200) fail(`candidate failed: ${built.body.failure}`);
|
|
378
|
+
const before = serializeLive(page());
|
|
379
|
+
const answer = adapter.commitCompaction({ token: job.token, candidate: built.body });
|
|
380
|
+
if (answer.status !== 200) {
|
|
381
|
+
const refused = answer.body.refused;
|
|
382
|
+
if (serializeLive(page()) !== before) fail(`refused commit ${refused} changed the room`);
|
|
383
|
+
if (entry.epoch !== state().epoch) { stats.stale++; return; }
|
|
384
|
+
if (refused !== 'stale-token' && refused !== 'too-large') fail(`commit refused: ${refused}`);
|
|
385
|
+
stats.stale++;
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
stats.commits[answer.body.kind]++;
|
|
389
|
+
check('commit');
|
|
390
|
+
if (!contains(admitted)) fail('a commit lost an admitted update');
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const edit = (t) => {
|
|
394
|
+
const text = t.doc.getText('probe');
|
|
395
|
+
const len = text.length;
|
|
396
|
+
const roll = r();
|
|
397
|
+
if (roll < 0.55 || len < 50) text.insert(pick(len + 1), 'x'.repeat(1 + pick(roll < 0.05 ? paste : 80)) + t.id);
|
|
398
|
+
else text.delete(pick(len), Math.min(len, 1 + pick(400)));
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
for (let step = 0; step < steps; step++) {
|
|
402
|
+
adapter.advanceClock(50);
|
|
403
|
+
const t = tabs[pick(tabs.length)];
|
|
404
|
+
const roll = r();
|
|
405
|
+
if (roll < 0.45) edit(t);
|
|
406
|
+
else if (roll < 0.75) send(t);
|
|
407
|
+
else if (roll < 0.82) compactBegin();
|
|
408
|
+
else if (roll < 0.9) compactFinish();
|
|
409
|
+
else if (roll < 0.95) { sync(t); saved = cloneJson(page()); history.push({ value: saved, epoch: state().epoch, seq: state().seq }); if (history.length > 8) history.shift(); }
|
|
410
|
+
else if (roll < 0.97) legacyWrite();
|
|
411
|
+
else if (restarts && roll < 0.985) {
|
|
412
|
+
const before = saved;
|
|
413
|
+
const next = adapter.restart({ key: KEY, html: htmlOf(before), nodeId: 1 });
|
|
414
|
+
if (next.status !== 200) fail(`restart refused with ${next.status}`);
|
|
415
|
+
dueSet.clear();
|
|
416
|
+
pending.length = 0;
|
|
417
|
+
stats.restarts++;
|
|
418
|
+
for (const x of tabs) x.known = new Set();
|
|
419
|
+
history.length = 0;
|
|
420
|
+
history.push({ value: cloneJson(before), epoch: next.body.epoch, seq: next.body.seq });
|
|
421
|
+
admitted.length = 0;
|
|
422
|
+
} else if (restarts) {
|
|
423
|
+
const next = adapter.reset({ key: KEY, html: htmlOf(saved), nodeId: 1 });
|
|
424
|
+
if (next.status === 200) { stats.resets++; for (const x of tabs) x.known = new Set(); admitted.length = 0; history.length = 0; history.push({ value: cloneJson(saved), epoch: next.body.epoch, seq: next.body.seq }); }
|
|
425
|
+
}
|
|
426
|
+
const epoch = state().epoch;
|
|
427
|
+
for (const x of tabs) if (x.epoch !== epoch) sync(x);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
for (let round = 0; round < 400; round++) {
|
|
431
|
+
let busy = false;
|
|
432
|
+
for (const t of tabs) {
|
|
433
|
+
sync(t);
|
|
434
|
+
if (!t.blocked && (t.inflight || t.outbox.length)) { send(t); busy = true; }
|
|
435
|
+
}
|
|
436
|
+
if (dueSet.size) { compactBegin(); busy = true; }
|
|
437
|
+
if (pending.length) { compactFinish(); busy = true; }
|
|
438
|
+
adapter.advanceClock(50);
|
|
439
|
+
if (!busy) break;
|
|
440
|
+
}
|
|
441
|
+
for (const t of tabs) sync(t);
|
|
442
|
+
check('end');
|
|
443
|
+
const lost = tabs.filter((t) => !t.blocked && !contains(t.journal));
|
|
444
|
+
if (lost.length) fail(`edits lost for ${lost.map((t) => t.id)}`);
|
|
445
|
+
const blocked = tabs.filter((t) => t.blocked);
|
|
446
|
+
for (const t of blocked) {
|
|
447
|
+
const mine = docRanges(t.doc);
|
|
448
|
+
if (!t.journal.every((u) => within(mine, rangesOf(u)))) fail(`blocked tab ${t.id} lost its own edits`);
|
|
449
|
+
}
|
|
450
|
+
if (!blocked.length) {
|
|
451
|
+
const roomDoc = new Y.Doc();
|
|
452
|
+
for (const [, v] of entriesOf(page().y)) Y.applyUpdateV2(roomDoc, Y.fromBase64(v));
|
|
453
|
+
const want = roomDoc.getText('probe').toString();
|
|
454
|
+
for (const t of tabs) if (t.doc.getText('probe').toString() !== want) fail(`tab ${t.id} diverged`);
|
|
455
|
+
roomDoc.destroy();
|
|
456
|
+
}
|
|
457
|
+
const end = page();
|
|
458
|
+
const out = { seed, ...stats, legacyWrites: legacy.writes, legacyMerged: legacy.merged, blocked: blocked.length, end: { bytes: usage().stores.page.bytes, ...pageShape(end.y, limits) } };
|
|
459
|
+
if (log) console.log(JSON.stringify(out));
|
|
460
|
+
return out;
|
|
461
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* parse-steps.js — the same parse5 parse, interrupted between slices.
|
|
3
|
+
*
|
|
4
|
+
* A whole-document save has to parse the bytes it is about to send, and on an 11 MB
|
|
5
|
+
* document that one synchronous parse is most of the time the save spends on the main
|
|
6
|
+
* thread. Slicing it cannot make it cheaper, only smaller per task, and it is worth
|
|
7
|
+
* doing because a paused parse is still the same parse: one `Parser`, fed consecutive
|
|
8
|
+
* slices of the exact final string, is what `Parser.parse` does in one call. Nothing
|
|
9
|
+
* but the suspension point changes, so the tree, every `sourceCodeLocation` offset and
|
|
10
|
+
* the parse errors come out identical to a one-shot `parse`.
|
|
11
|
+
*
|
|
12
|
+
* SO DO NOT RESET ANYTHING BETWEEN SLICES. The tokenizer's pending token, the
|
|
13
|
+
* insertion mode, the open elements and the error counts all carry across a yield;
|
|
14
|
+
* that state IS the parser. `parseAll` drains the same generator that `parseInSteps` resumes,
|
|
15
|
+
* so the two forms cannot drift apart: a change to the loop is a change to both.
|
|
16
|
+
*
|
|
17
|
+
* LAZY. This module is imported only by source code that already loads parse5, so it
|
|
18
|
+
* adds nothing to a page that never parses. `src/lib/slice.js` decides whether a yield
|
|
19
|
+
* hands the event loop back: `scheduler.yield()` when the browser has it and a message
|
|
20
|
+
* task otherwise — never a microtask, because a microtask does not return to the event
|
|
21
|
+
* loop and the typing between two slices would still wait. Idle time is not used either:
|
|
22
|
+
* a continuous typist must not starve the save.
|
|
23
|
+
*
|
|
24
|
+
* The errors are the same `Map` of code to count that `model` and the verifier keep,
|
|
25
|
+
* and the caller's own `onParseError` still receives every event in order.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { Parser } from '../vendor/parse5.vendor.js';
|
|
29
|
+
import { drain, runSliced } from '../lib/slice.js';
|
|
30
|
+
|
|
31
|
+
const DEFAULT_CHUNK = 16384;
|
|
32
|
+
const DEFAULT_BUDGET_MS = 4;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* One parser, one error map, and the options the caller handed us — plus an
|
|
36
|
+
* `onParseError` that counts first and then forwards, so the returned `errors` is
|
|
37
|
+
* exactly what a one-shot caller would have built for itself.
|
|
38
|
+
*/
|
|
39
|
+
function beginParse(options) {
|
|
40
|
+
const errors = new Map();
|
|
41
|
+
const forward = options?.onParseError;
|
|
42
|
+
const parser = new Parser({
|
|
43
|
+
...options,
|
|
44
|
+
onParseError: (e) => {
|
|
45
|
+
errors.set(e.code, (errors.get(e.code) || 0) + 1);
|
|
46
|
+
forward?.(e);
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
return { parser, errors };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Write the next slice and return where the one after it starts. */
|
|
53
|
+
function writeSlice(parser, html, cursor, chunk) {
|
|
54
|
+
const end = Math.min(cursor + chunk, html.length);
|
|
55
|
+
parser.tokenizer.write(html.slice(cursor, end), end >= html.length);
|
|
56
|
+
return end;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The whole document, one slice at a time, `yield`ing after each written chunk.
|
|
61
|
+
*
|
|
62
|
+
* `drain` runs this in one task and `runSliced` resumes it across tasks, so the parser is
|
|
63
|
+
* written once and the one-shot and sliced forms cannot disagree about it.
|
|
64
|
+
*/
|
|
65
|
+
export function* parseSteps(html, options, { chunk = DEFAULT_CHUNK } = {}) {
|
|
66
|
+
const { parser, errors } = beginParse(options);
|
|
67
|
+
let cursor = 0;
|
|
68
|
+
do {
|
|
69
|
+
cursor = writeSlice(parser, html, cursor, chunk);
|
|
70
|
+
if (cursor < html.length) yield;
|
|
71
|
+
} while (cursor < html.length);
|
|
72
|
+
return { document: parser.document, errors };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The whole document, in one task. */
|
|
76
|
+
export function parseAll(html, options) {
|
|
77
|
+
return drain(parseSteps(html, options));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The whole document, in slices of `chunk` characters, yielding to a task after roughly
|
|
82
|
+
* `budgetMs` of writing.
|
|
83
|
+
*
|
|
84
|
+
* An abort returns `{ aborted: true }` and no document at all: a partial tree is not a
|
|
85
|
+
* smaller version of this document, and nothing downstream may adopt it as one.
|
|
86
|
+
*/
|
|
87
|
+
export async function parseInSteps(html, options, {
|
|
88
|
+
budgetMs = DEFAULT_BUDGET_MS,
|
|
89
|
+
chunk = DEFAULT_CHUNK,
|
|
90
|
+
yieldTask,
|
|
91
|
+
signal,
|
|
92
|
+
} = {}) {
|
|
93
|
+
return runSliced(parseSteps(html, options, { chunk }), { budgetMs, signal, yieldTask });
|
|
94
|
+
}
|