@casualoffice/sheets 0.9.0 → 0.10.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/api-BI2VLYQ6.d.cts +62 -0
- package/dist/api-BI2VLYQ6.d.ts +62 -0
- package/dist/chrome.cjs +2569 -0
- package/dist/chrome.cjs.map +1 -0
- package/dist/chrome.d.cts +96 -0
- package/dist/chrome.d.ts +96 -0
- package/dist/chrome.js +2556 -0
- package/dist/chrome.js.map +1 -0
- package/dist/collab.cjs +770 -0
- package/dist/collab.cjs.map +1 -0
- package/dist/collab.d.cts +248 -0
- package/dist/collab.d.ts +248 -0
- package/dist/collab.js +737 -0
- package/dist/collab.js.map +1 -0
- package/dist/embed/embed-runtime.js +128 -128
- package/dist/embed.cjs +166 -0
- package/dist/embed.cjs.map +1 -1
- package/dist/embed.d.cts +78 -3
- package/dist/embed.d.ts +78 -3
- package/dist/embed.js +166 -0
- package/dist/embed.js.map +1 -1
- package/dist/index.cjs +262 -165
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +271 -168
- package/dist/index.js.map +1 -1
- package/dist/{protocol--KyBQUjU.d.cts → protocol-Cq4Cdoyi.d.cts} +19 -2
- package/dist/{protocol-cEzy7S0i.d.ts → protocol-DqDaG2yG.d.ts} +19 -2
- package/dist/sheets.cjs +102 -16
- package/dist/sheets.cjs.map +1 -1
- package/dist/sheets.d.cts +49 -63
- package/dist/sheets.d.ts +49 -63
- package/dist/sheets.js +111 -19
- package/dist/sheets.js.map +1 -1
- package/package.json +28 -3
- package/src/chrome/AutoSumPicker.tsx +176 -0
- package/src/chrome/BordersPicker.tsx +171 -0
- package/src/chrome/ChromeBottom.tsx +18 -0
- package/src/chrome/ChromeTop.tsx +21 -0
- package/src/chrome/ColorPicker.tsx +220 -0
- package/src/chrome/FindReplace.tsx +370 -0
- package/src/chrome/FormulaBar.tsx +378 -0
- package/src/chrome/Icon.tsx +43 -0
- package/src/chrome/MenuBar.tsx +336 -0
- package/src/chrome/NameBox.tsx +347 -0
- package/src/chrome/SheetTabs.tsx +346 -0
- package/src/chrome/StatusBar.tsx +232 -0
- package/src/chrome/Toolbar.tsx +401 -0
- package/src/chrome/fonts.ts +42 -0
- package/src/chrome/index.ts +24 -0
- package/src/collab/attachCollab.ts +151 -0
- package/src/collab/bridge-helpers.ts +97 -0
- package/src/collab/bridge.ts +885 -0
- package/src/collab/bridge.unit.test.ts +160 -0
- package/src/collab/index.ts +38 -0
- package/src/collab/replay-retry.ts +137 -0
- package/src/collab/replay-retry.unit.test.ts +223 -0
- package/src/collab/ws-url.ts +20 -0
- package/src/collab/ws-url.unit.test.ts +35 -0
- package/src/embed/EmbedHostTransport.ts +16 -1
- package/src/embed/EmbedTransport.ts +16 -0
- package/src/embed/EmbedTransport.unit.test.ts +88 -2
- package/src/embed/index.ts +7 -0
- package/src/embed/protocol.ts +34 -0
- package/src/embed-runtime/index.tsx +20 -0
- package/src/sheets/CasualSheets.tsx +204 -33
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/collab/index.ts","../src/collab/attachCollab.ts","../src/collab/bridge.ts","../src/collab/bridge-helpers.ts","../src/univer/lazy-plugins.ts","../src/collab/replay-retry.ts","../src/collab/ws-url.ts"],"sourcesContent":["/**\n * `@casualoffice/sheets/collab` — opt-in real-time co-editing.\n *\n * The editor is collab-unaware until a host calls `attachCollab(api, opts)`.\n * Yjs + Hocuspocus is the realtime transport; authoritative persistence stays\n * host-side (WOPI / backend) via the save/exit event contract.\n *\n * Requires the host to provide `yjs` and `@hocuspocus/provider` (peer deps) so\n * there's a single Yjs instance in the graph — two copies break `Y.Doc`\n * identity and awareness.\n */\n\nexport {\n attachCollab,\n type AttachCollabOptions,\n type CollabAttachable,\n type CollabHandle,\n type CollabRole,\n type CollabConnectionStatus,\n} from './attachCollab';\n\n// The mutation bridge — the framework-agnostic core (subscribes to\n// onMutationExecutedForCollab, replays with fromCollab, guards __splitChunk__).\n// Exposed for hosts that drive their own provider/doc lifecycle instead of\n// using attachCollab's batteries-included transport.\nexport {\n startBridge,\n type BridgeHandle,\n type BridgeOptions,\n SYNCED_MUTATIONS,\n REVERTABLE_MUTATIONS,\n} from './bridge';\nexport {\n type ReplayFailureRecord,\n type ReplayClassification,\n classifyReplayError,\n} from './replay-retry';\nexport { deepRewriteUnitId, rewriteJson1OpPathUnitId } from './bridge-helpers';\n","/**\n * attachCollab — opt-in real-time co-editing for `<CasualSheets>`.\n *\n * The editor ships **collab-unaware**: it boots, edits, and persists with no\n * knowledge of rooms or peers. A host that wants co-editing calls\n * `attachCollab(api, { room, server })` once after `onReady` and gets back a\n * detach handle. That's the entire public surface — everything else (presence\n * UI, password prompts, room preflight, reconnect banners) is the host's to\n * build on top of the returned `provider` / `doc`.\n *\n * Transport: Yjs + Hocuspocus, exactly as the reference host (`apps/web`) uses\n * it. The non-negotiable Univer hooks live in `./bridge` — it subscribes to\n * `ICommandService.onMutationExecutedForCollab`, applies remote mutations with\n * `IExecutionOptions.fromCollab` (echo-loop prevention), and guards\n * `params.__splitChunk__`.\n *\n * Persistence note: Yjs/Hocuspocus is the **realtime transport only**. The\n * authoritative document is still saved by the host via WOPI / its own backend\n * (the save/exit event contract) — collab does not turn the SDK into a store.\n */\n\nimport * as Y from 'yjs';\nimport { HocuspocusProvider, HocuspocusProviderWebsocket } from '@hocuspocus/provider';\nimport type { IWorkbookData } from '@univerjs/core';\nimport type { FUniver } from '@univerjs/core/facade';\nimport type { CasualSheetsAPI } from '../sheets/api';\nimport { startBridge, type BridgeHandle } from './bridge';\nimport { buildWsUrl } from './ws-url';\n\n/** Either the SDK's imperative API (`onReady`) or the bare FUniver facade.\n * Collab only needs the facade, so a host that holds the raw FUniver (the\n * reference app does) can attach without constructing a CasualSheetsAPI. */\nexport type CollabAttachable = CasualSheetsAPI | FUniver;\n\n/** Pull the FUniver facade out of whichever attachable form was passed.\n * FUniver exposes `getActiveWorkbook` directly; CasualSheetsAPI wraps the\n * facade on `.univer`. Discriminating on the method (not on `'univer' in …`)\n * is unambiguous either way. */\nfunction resolveFacade(api: CollabAttachable): FUniver {\n const maybe = api as Partial<FUniver> & Partial<CasualSheetsAPI>;\n return typeof maybe.getActiveWorkbook === 'function'\n ? (api as FUniver)\n : (maybe.univer as FUniver);\n}\n\n/** `write` peers broadcast their edits; `view` peers only receive. The\n * client-side gate is belt-and-braces — real enforcement is the server's\n * `role` check on the WS upgrade. */\nexport type CollabRole = 'view' | 'write';\n\n/** Coarse connection state mapped from Hocuspocus's provider status. */\nexport type CollabConnectionStatus = 'connecting' | 'live' | 'offline';\n\nexport interface AttachCollabOptions {\n /** Room / document id. Becomes the Hocuspocus document name. */\n room: string;\n /** Base WebSocket URL of the collab server, e.g. `wss://host/yjs`. */\n server: string;\n /** Room password, if the server gates the room. Sent on the WS URL as\n * `p=…` so the upgrade handler can validate before the protocol handshake. */\n password?: string;\n /** Auth token for the Hocuspocus handshake. The provider only sends its\n * auth submessage when this is truthy; servers with an `onAuthenticate`\n * hook keep the connection queued without it. Defaults to `'anon'` (the\n * reference server's hook only reads the `role` query param). */\n token?: string;\n /** `view` joins read-only. Defaults to `'write'`. */\n role?: CollabRole;\n /**\n * Called when a peer's compaction snapshot arrives — the host swaps the\n * workbook (typically `api.loadSnapshot(wb)`). MAY return a promise; the\n * bridge pauses op-log replay until it resolves so later mutations don't\n * land on the pre-swap unit.\n */\n onSnapshot?: (wb: IWorkbookData) => void | Promise<void>;\n /** Connection-status transitions — drive a status pill / offline banner. */\n onStatus?: (status: CollabConnectionStatus) => void;\n}\n\nexport interface CollabHandle {\n /** The underlying Yjs document — introspection / devtools / extra maps. */\n readonly doc: Y.Doc;\n /** The Hocuspocus provider — `awareness` for presence, `on('status')`, etc. */\n readonly provider: HocuspocusProvider;\n /** The mutation bridge — replay-failure diagnostics live here. */\n readonly bridge: BridgeHandle;\n /** Last known connection status. */\n status(): CollabConnectionStatus;\n /** Detach: tear down bridge + provider + doc. Idempotent. */\n detach(): void;\n}\n\n/**\n * Attach real-time collab to a live editor. Call once after `onReady`.\n * Returns a {@link CollabHandle}; call `.detach()` to leave the room\n * (and always before the editor unmounts).\n */\nexport function attachCollab(api: CollabAttachable, opts: AttachCollabOptions): CollabHandle {\n const facade = resolveFacade(api);\n const role: CollabRole = opts.role ?? 'write';\n\n const doc = new Y.Doc();\n // Match the reference host: drop the reconnect timeout to 10 s so a dropped\n // socket surfaces as `offline` quickly rather than after the 30 s default.\n const ws = new HocuspocusProviderWebsocket({\n url: buildWsUrl(opts.server, opts.room, role, opts.password),\n messageReconnectTimeout: 10_000,\n });\n const provider = new HocuspocusProvider({\n websocketProvider: ws,\n name: opts.room,\n document: doc,\n // Truthy token so the handshake completes even when the server has an\n // onAuthenticate hook (used for role enforcement). See option docs above.\n token: opts.token ?? 'anon',\n });\n\n const bridge = startBridge(facade, doc, {\n role,\n awareness: provider.awareness ?? undefined,\n onSnapshotReceived: opts.onSnapshot,\n });\n\n let current: CollabConnectionStatus = 'connecting';\n const onStatus = (ev: { status: string }) => {\n const next: CollabConnectionStatus =\n ev.status === 'connected' ? 'live' : ev.status === 'connecting' ? 'connecting' : 'offline';\n if (next === current) return;\n current = next;\n opts.onStatus?.(next);\n };\n provider.on('status', onStatus);\n\n let detached = false;\n const detach = () => {\n if (detached) return;\n detached = true;\n provider.off('status', onStatus);\n bridge.dispose();\n provider.destroy();\n doc.destroy();\n };\n\n return {\n doc,\n provider,\n bridge,\n status: () => current,\n detach,\n };\n}\n","import type * as Y from 'yjs';\nimport type { FUniver } from '@univerjs/core/facade';\nimport type { IWorkbookData } from '@univerjs/core';\nimport { ICommandService, type ICommandInfo, type IExecutionOptions } from '@univerjs/core';\nimport { SetRangeValuesUndoMutationFactory } from '@univerjs/sheets';\nimport { deepRewriteUnitId, rewriteJson1OpPathUnitId } from './bridge-helpers';\nimport { ensurePluginByName, type LazyPluginGroup } from '../univer';\nimport {\n classifyReplayError,\n pushDeadLetter,\n TRANSIENT_RETRY_DELAYS_MS,\n withRetry,\n type ReplayFailureRecord,\n} from './replay-retry';\n\n/**\n * Map mutation ids to the lazy-plugin group that owns the matching\n * mutation handler. The joiner replays peer mutations through Univer's\n * command service; if the receiving plugin hasn't been loaded yet\n * (lazy bundling), the mutation handler is missing and the change\n * silently drops on that peer. Bridge waits for the plugin to mount\n * before executing the mutation.\n */\nconst MUTATION_TO_LAZY_GROUP: Record<string, LazyPluginGroup> = {\n 'sheet.mutation.add-conditional-rule': 'cf',\n 'sheet.mutation.set-conditional-rule': 'cf',\n 'sheet.mutation.delete-conditional-rule': 'cf',\n 'sheet.mutation.move-conditional-rule': 'cf',\n 'sheet.mutation.add-table': 'table',\n 'sheet.mutation.delete-table': 'table',\n 'sheet.mutation.set-sheet-table': 'table',\n 'sheet.mutation.set-table-filter': 'table',\n 'sheet.mutation.set-filter-criteria': 'filter',\n 'sheet.mutation.set-filter-range': 'filter',\n 'sheet.mutation.remove-filter': 'filter',\n 'sheet.mutation.update-note': 'note',\n 'sheet.mutation.remove-note': 'note',\n 'sheet.mutation.add-hyper-link': 'hyperlink',\n 'sheet.mutation.remove-hyper-link': 'hyperlink',\n 'sheet.mutation.update-hyper-link': 'hyperlink',\n 'data-validation.mutation.addRule': 'dv',\n 'data-validation.mutation.removeRule': 'dv',\n 'data-validation.mutation.updateRule': 'dv',\n 'sheet.mutation.set-drawing-apply': 'drawing',\n};\n// y-protocols ships type declarations only as ESM and our tsconfig\n// doesn't pick them up cleanly; loose-type the Awareness surface we\n// actually use (getStates → Map keyed by clientID).\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype Awareness = { getStates(): Map<number, any> };\n\n/**\n * Yjs ↔ Univer mutation bridge. See docs/CO-EDITING.md for the design.\n *\n * Strategy: every non-collab mutation gets serialized into a Y.Array log;\n * peers replay each entry with `fromCollab: true`. The log is the source\n * of truth — late joiners read the whole array on connect via Yjs sync\n * and replay it once, ending up at the same state.\n *\n * Why an op log (not a state mirror): writing a per-mutation state mirror\n * for every mutation Univer emits (set-range-values, set-style, insert-row,\n * merge, hide-col, freeze, …) is dozens of handlers. The log generalizes\n * — any deterministic mutation just round-trips its params. Trade-off: no\n * per-cell CRDT merging on concurrent writes (Yjs orders inserts, then\n * Univer re-executes them; last writer wins at the mutation level). For\n * v1 that matches expectations.\n *\n * Echo-loop guard (per CLAUDE.md):\n * - Records carry the emitter's `clientId`. The observer skips records\n * it emitted itself (otherwise we'd double-apply our own writes).\n * - Remote applies pass `fromCollab: true` so Univer's\n * `onMutationExecutedForCollab` listener filters them back out via the\n * options check below.\n */\n\nconst LOG_KEY = 'ops';\n\n/**\n * Allowlist of mutation ids we sync. Listed explicitly to keep\n * undocumented / version-volatile mutations out of the log — anything\n * not here just stays local. Easier to add new ids than to debug a\n * silent corruption from a mutation that secretly references local\n * state (selections, render skeletons, etc.).\n */\nexport const SYNCED_MUTATIONS: ReadonlySet<string> = new Set([\n // Cell-level — values, formulas, styles, rich text.\n 'sheet.mutation.set-range-values',\n 'sheet.mutation.set-style',\n // Row / column structural.\n 'sheet.mutation.insert-row',\n 'sheet.mutation.insert-col',\n 'sheet.mutation.remove-row',\n 'sheet.mutation.remove-col',\n 'sheet.mutation.move-rows',\n 'sheet.mutation.move-cols',\n 'sheet.mutation.set-row-hidden',\n 'sheet.mutation.set-row-visible',\n 'sheet.mutation.set-col-hidden',\n 'sheet.mutation.set-col-visible',\n 'sheet.mutation.set-worksheet-row-height',\n 'sheet.mutation.set-worksheet-row-is-auto-height',\n 'sheet.mutation.set-worksheet-col-width',\n // Merges.\n 'sheet.mutation.add-worksheet-merge',\n 'sheet.mutation.remove-worksheet-merge',\n // Sheet lifecycle.\n 'sheet.mutation.insert-sheet',\n 'sheet.mutation.remove-sheet',\n 'sheet.mutation.set-worksheet-name',\n 'sheet.mutation.set-worksheet-order',\n // Sheet visibility — hide/show. NB: `set-worksheet-activate` is\n // deliberately omitted so each peer keeps their own active sheet\n // independent of which sheet another user is editing.\n 'sheet.mutation.set-worksheet-hidden',\n // Freeze.\n 'sheet.mutation.set-frozen',\n // Hyperlinks (sheets-hyper-link).\n 'sheet.mutation.add-hyper-link',\n 'sheet.mutation.remove-hyper-link',\n 'sheet.mutation.update-hyper-link',\n // Tab colour — picks up the right-click \"Tab color\" menu.\n 'sheet.mutation.set-tab-color',\n // Move + sort. Without these, a peer's cut-and-paste-cell-block or\n // sort-range action silently doesn't appear on receivers.\n 'sheet.mutation.move-range',\n 'sheet.mutation.reorder-range',\n // Per-row / per-column metadata (height, custom style, colData).\n // The narrower set-worksheet-row-height / set-worksheet-col-width\n // are already allowlisted; these are the broader resource-style\n // mutations Univer emits for \"Format → Row/Column\" operations.\n 'sheet.mutation.set-row-data',\n 'sheet.mutation.set-col-data',\n 'sheet.mutation.set-worksheet-default-style',\n // Format-as-table / sheets-table — adds/removes named tables and\n // their config. Picked up so the table chrome appears on both peers.\n 'sheet.mutation.add-table',\n 'sheet.mutation.delete-table',\n 'sheet.mutation.set-sheet-table',\n 'sheet.mutation.set-table-filter',\n // Autofilter (sheets-filter).\n 'sheet.mutation.set-filter-criteria',\n 'sheet.mutation.set-filter-range',\n 'sheet.mutation.remove-filter',\n // Notes (sheets-note) — the small cell-corner indicator + popup.\n 'sheet.mutation.update-note',\n 'sheet.mutation.remove-note',\n // Conditional formatting (sheets-conditional-formatting). Mutations\n // are self-contained — `add` carries the full rule, `set` carries\n // the patched rule, `delete` / `move` carry rule ids. Univer's\n // `ConditionalFormattingRuleModel` consumes them and triggers a\n // canvas re-render so highlighted cells update on peers. Existing\n // rules in a downloaded seed already load via the workbook's\n // resource channel; the mutations cover deltas during the session.\n 'sheet.mutation.add-conditional-rule',\n 'sheet.mutation.set-conditional-rule',\n 'sheet.mutation.delete-conditional-rule',\n 'sheet.mutation.move-conditional-rule',\n // Data validation (data-validation core). NB: this package uses the\n // `data-validation.mutation.*` prefix, not `sheet.mutation.*`.\n // Mutation handlers live in @univerjs/data-validation; the\n // sheets-data-validation plugin is the lazy-loaded integration our\n // MUTATION_TO_LAZY_GROUP map keys on.\n 'data-validation.mutation.addRule',\n 'data-validation.mutation.removeRule',\n 'data-validation.mutation.updateRule',\n // Drawings / images (sheets-drawing). Single all-purpose mutation\n // wraps add / remove / update via a JSON-1 op + an enum type. Params\n // can be large (embedded image blobs) — accept the bandwidth hit\n // until we move drawings to a side-channel resource model.\n 'sheet.mutation.set-drawing-apply',\n // Workbook / worksheet metadata. Each is rarely changed mid-session\n // but cheap to propagate when it does happen. Without these,\n // renaming the workbook or toggling gridlines silently stays\n // local-only — confusing in a shared room.\n // NOTE: `set-worksheet-right-to-left` is intentionally NOT here —\n // neither the command nor the mutation is registered in\n // @univerjs/sheets@0.22.1 (it's exported but never wired up by any\n // plugin), so nothing in our app can emit it. Add it back if a\n // future Univer bump registers it.\n 'sheet.mutation.set-workbook-name',\n 'sheet.mutation.set-worksheet-row-count',\n 'sheet.mutation.set-worksheet-column-count',\n 'sheet.mutation.toggle-gridlines',\n 'sheet.mutation.set-gridlines-color',\n]);\n\n/** Mutation ids for which the bridge captures undo params before the\n * redo runs. Used by the HistoryPanel's revert action. Restricted to\n * cell-level mutations because the existing Univer factories cover\n * them and they're the dominant case for \"undo my edit\"; structural\n * ops (insert-row / move-range / sort) need their own factories and\n * are out of scope for v1 revert.\n */\nexport const REVERTABLE_MUTATIONS: ReadonlySet<string> = new Set([\n 'sheet.mutation.set-range-values',\n]);\n\ntype MutationRecord = {\n kind?: 'op';\n /** Yjs client id of the emitter (string for portability via JSON). */\n c: string;\n /** Wall-clock at emit; diagnostic only. */\n t: number;\n /** Mutation id (e.g. `sheet.mutation.set-range-values`). */\n id: string;\n /** Mutation params, JSON-serializable. */\n p: unknown;\n /** Optional undo params — set for mutations in REVERTABLE_MUTATIONS,\n * computed via Univer's `*UndoMutationFactory` BEFORE the redo\n * runs (so it reads pre-edit state). The HistoryPanel's Revert\n * button feeds this back into `executeCommand(rec.id, rec.u)` to\n * restore the pre-edit values. Older log entries without `u`\n * predate this feature — their Revert button stays disabled. */\n u?: unknown;\n};\n\n/**\n * Snapshot entry written into the op log by the designated compactor\n * client (lowest awareness clientId). Replaces all prior entries; any\n * mutation records that come AFTER it in the array are post-compaction\n * incremental edits and replay normally.\n *\n * Pipeline Stage 6 — keeps long-lived rooms from accumulating an\n * unbounded op log. A 24-hour room with light editing could otherwise\n * grow to thousands of records, slowing every late join. Compaction\n * collapses it back to \"snapshot + a handful of recent ops\".\n */\ntype SnapshotRecord = {\n kind: 'snapshot';\n c: string;\n t: number;\n /** Full IWorkbookData. Yes, this is large for big workbooks — but\n * it ships once per compaction interval, not per mutation. The\n * trade-off vs. unbounded op-log growth is straightforward. */\n wb: IWorkbookData;\n};\n\ntype OpRecord = MutationRecord | SnapshotRecord;\n\nexport type BridgeHandle = {\n /** Underlying Yjs document — exposed so tests / devtools can introspect. */\n doc: Y.Doc;\n /** Stop listening and detach. */\n dispose: () => void;\n /**\n * How many remote mutations have thrown during replay since the\n * bridge started. Each one is a candidate divergence — the local\n * state didn't accept the peer's change, so the two state vectors\n * are now off by at least that mutation. The CollabDriver\n * subscribes (see `subscribeReplayFailures`) so the indicator can\n * warn the user before they discover it the hard way.\n */\n getReplayFailures: () => number;\n /**\n * Subscribe to replay-failure count changes. Fires after every\n * increment with the new total. Returns a teardown that unhooks\n * the subscriber. No initial-value fire — caller can read\n * `getReplayFailures()` once at subscribe time if they need it.\n */\n subscribeReplayFailures: (cb: (count: number) => void) => () => void;\n /**\n * Snapshot of the dead-letter ring buffer — mutations that\n * exhausted retries (transient class) or failed immediately\n * (permanent class). Capped at DEAD_LETTER_CAP entries; oldest\n * evicts on overflow. UI consumes this to render the per-failure\n * detail panel.\n */\n getReplayDeadLetter: () => readonly ReplayFailureRecord[];\n /**\n * Subscribe to dead-letter changes. Fires after every push with a\n * fresh array (reference change — React state updates see it).\n * Returns a teardown.\n */\n subscribeReplayDeadLetter: (cb: (entries: readonly ReplayFailureRecord[]) => void) => () => void;\n};\n\nexport type BridgeOptions = {\n /**\n * `view` clients only RECEIVE remote updates — local mutations don't\n * append to the log, so peers never see them. Client-side gate only;\n * a determined user can run the bridge in write mode by editing the\n * URL. Server-side enforcement is a follow-up hardening pass.\n */\n role?: 'view' | 'write';\n /**\n * Provider's Yjs awareness. Used (a) to determine which peer is the\n * designated compactor (lowest known clientId — deterministic and\n * race-free) and (b) so view-only clients don't try to compact. If\n * omitted, compaction is disabled.\n */\n awareness?: Awareness;\n /**\n * Hand a fresh workbook snapshot to the host when a compaction\n * record arrives from a peer. The bridge can't call\n * `replaceWorkbook` directly (it lives in React state); the host\n * (`CollabDriver`) wires this through.\n *\n * MAY return a promise. Replay of subsequent op-log entries is\n * paused until the promise resolves — without that, mutations\n * land on the OLD unit before Univer's async unit-swap completes,\n * which silently forks state on late joiners.\n */\n onSnapshotReceived?: (wb: IWorkbookData) => void | Promise<void>;\n};\n\n/**\n * Compaction thresholds. We only attempt to compact when the log has\n * grown past `COMPACT_OPS_THRESHOLD` AND at least\n * `COMPACT_MIN_INTERVAL_MS` has elapsed since the last compaction.\n * The interval guard prevents two designated-writer candidates from\n * racing the compaction; the ops threshold avoids compacting a quiet\n * room over and over.\n */\nconst COMPACT_OPS_THRESHOLD = 200;\nconst COMPACT_MIN_INTERVAL_MS = 60_000;\nconst COMPACT_CHECK_INTERVAL_MS = 30_000;\n\nexport function startBridge(api: FUniver, doc: Y.Doc, opts: BridgeOptions = {}): BridgeHandle {\n const role = opts.role ?? 'write';\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const injector = (api as any)._injector as { get: (token: unknown) => unknown } | undefined;\n if (!injector) {\n throw new Error('[collab] FUniver injector not accessible — Univer too old?');\n }\n const cmdSvc = injector.get(ICommandService) as {\n onMutationExecutedForCollab: (l: (info: ICommandInfo, options?: IExecutionOptions) => void) => {\n dispose: () => void;\n };\n beforeCommandExecuted: (l: (info: ICommandInfo, options?: IExecutionOptions) => void) => {\n dispose: () => void;\n };\n executeCommand: (id: string, params: unknown, options?: IExecutionOptions) => Promise<unknown>;\n };\n\n const log = doc.getArray<OpRecord>(LOG_KEY);\n const myClientId = String(doc.clientID);\n // One-shot guard for the __splitChunk__ regression watchdog below.\n let splitChunkWarned = false;\n\n // Replay-failure tracking — surfaces silent divergences to the UI.\n // Every time a remote mutation throws on apply, this counter ticks\n // and any subscribers (CollabDriver) get the new total. Local writes\n // never tick this (they're applied by Univer before we even append\n // to the log).\n let replayFailures = 0;\n const replayFailureSubscribers = new Set<(count: number) => void>();\n let deadLetter: readonly ReplayFailureRecord[] = [];\n const deadLetterSubscribers = new Set<(entries: readonly ReplayFailureRecord[]) => void>();\n const noteReplayFailure = (rec: ReplayFailureRecord) => {\n replayFailures += 1;\n deadLetter = pushDeadLetter(deadLetter, rec);\n for (const cb of replayFailureSubscribers) {\n try {\n cb(replayFailures);\n } catch (err) {\n console.warn('[collab] replay-failure subscriber threw', err);\n }\n }\n for (const cb of deadLetterSubscribers) {\n try {\n cb(deadLetter);\n } catch (err) {\n console.warn('[collab] dead-letter subscriber threw', err);\n }\n }\n };\n // Undo params keyed by JSON.stringify(params) so we can pair them up\n // when the matching `onMutationExecutedForCollab` fires moments later.\n // Cleared after each pairing — there's no eviction policy because the\n // window between before-execute and after-execute is microseconds.\n const pendingUndo = new Map<string, unknown>();\n // Capture undo params BEFORE the redo runs. The factory walks the\n // current cell state and produces a redo-shaped object that would\n // restore those cells. Only set-range-values for v1; other types fall\n // through with no `u` field — the HistoryPanel disables Revert.\n const subBeforeDispose = cmdSvc.beforeCommandExecuted((info, options) => {\n if (role === 'view') return;\n if (options?.fromCollab) return;\n if (!REVERTABLE_MUTATIONS.has(info.id)) return;\n try {\n // The factory's first arg is described as \"accessor\" — Univer's\n // accessor IS the injector for our purposes (both expose .get).\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const undo = SetRangeValuesUndoMutationFactory(injector as any, info.params as any);\n pendingUndo.set(JSON.stringify(info.params), undo);\n } catch (err) {\n // Pre-edit state was unreadable (workbook missing, sheet gone,\n // etc.) — skip. The history entry just won't be revertable.\n console.warn('[collab] failed to capture undo params for', info.id, err);\n }\n });\n\n // Local → Yjs: append every synced mutation to the log. Skipped for\n // view-role clients — their local edits never leave their browser.\n //\n // We BATCH the appends across a microtask window so a paste / sort\n // that emits many mutations doesn't trigger one Yjs encode per\n // mutation — that path was the main contributor to \"large action\n // takes 3–5 s\" on big workbooks. Single Y.Array.push with N entries\n // is one transaction, one encode, one WS frame.\n let pending: OpRecord[] = [];\n let flushScheduled = false;\n const flush = () => {\n flushScheduled = false;\n if (pending.length === 0) return;\n const batch = pending;\n pending = [];\n // doc.transact wraps the push in a single transaction so subscribers\n // see one change event for the whole batch.\n doc.transact(() => {\n log.push(batch);\n });\n };\n const subDispose = cmdSvc.onMutationExecutedForCollab((info, options) => {\n if (role === 'view') return;\n if (options?.fromCollab) return;\n if (!SYNCED_MUTATIONS.has(info.id)) return;\n // Univer 0.22.x doesn't use the chunked-mutation protocol that\n // earlier versions (and the CLAUDE.md hard rule) referenced. If a\n // future upgrade reintroduces __splitChunk__, mutations split\n // across multiple emissions will silently corrupt on peers because\n // our op-log doesn't reassemble them. Warn loudly the FIRST time\n // we see one so an upgrade regression surfaces in the console\n // instead of as a mysterious paste-corruption bug.\n if (!splitChunkWarned && hasSplitChunkMarker(info.params)) {\n splitChunkWarned = true;\n console.warn(\n '[collab] mutation \"%s\" carries __splitChunk__ — Univer reintroduced chunked mutations; bridge needs reassembly logic. See docs/COLLAB-FIXES.md issue 8.',\n info.id,\n );\n }\n // Pair with the undo params we captured at beforeCommandExecuted\n // (only set for REVERTABLE_MUTATIONS). The before-hook ran a few\n // microseconds ago with the SAME params object — match by\n // stringified key.\n const key = JSON.stringify(info.params);\n const undoParams = pendingUndo.get(key);\n pendingUndo.delete(key);\n pending.push({\n c: myClientId,\n t: Date.now(),\n id: info.id,\n // Univer mutation params are already JSON-friendly (numbers, strings,\n // plain objects). If something carries a Map / Set / cyclic ref we'll\n // discover it via a runtime error; that's the signal to drop the\n // mutation from SYNCED_MUTATIONS.\n p: info.params as unknown,\n ...(undoParams !== undefined ? { u: undoParams } : {}),\n });\n if (!flushScheduled) {\n flushScheduled = true;\n // queueMicrotask runs after the current command completes but\n // before the browser paints — keeps the bridge low-latency while\n // letting a multi-mutation command (paste, sort, fill) coalesce.\n queueMicrotask(flush);\n }\n });\n\n // Replay tracking: how many entries we've already executed locally so we\n // don't double-apply on incremental updates. On connect, replay everything\n // we haven't seen — that's how late joiners catch up.\n let appliedCount = 0;\n // Single-flight guard: when a snapshot record needs an async workbook\n // swap, subsequent records have to wait for the swap to land — otherwise\n // they execute against the old unit id and silently fork state. We also\n // want a single observer callback at a time so re-entrant Yjs events\n // don't interleave half-applied loops.\n let replayInFlight: Promise<void> | null = null;\n\n const replayPending = (): Promise<void> => {\n if (replayInFlight) return replayInFlight;\n // CRITICAL: assign `replayInFlight = p` BEFORE invoking the async\n // IIFE. The previous version was:\n // replayInFlight = (async () => { try { ... } finally { replayInFlight = null; } })();\n // For an empty log the IIFE body has no `await` and runs\n // synchronously — the `finally` set `replayInFlight = null`\n // BEFORE the outer `replayInFlight = ...promise...` assignment,\n // which then OVERWROTE the null with the freshly-resolved promise.\n // Result: `replayInFlight` stayed truthy forever and every\n // subsequent `replayPending()` returned immediately without\n // doing anything — remote mutations sat in the Yjs log untouched.\n // Tracker: docs/COLLAB-FIXES.md issue #29.\n let resolveOuter!: () => void;\n const p = new Promise<void>((r) => {\n resolveOuter = r;\n });\n replayInFlight = p;\n void (async () => {\n try {\n // Loop until we catch up. `log.length` may grow while we're awaiting\n // a snapshot apply, so re-read on each pass.\n\n while (true) {\n const total = log.length;\n // Stage 6 compaction shrinks the log atomically. If our cursor\n // is past the new end, reset to 0 and replay the snapshot record\n // (which is always at position 0 right after compaction).\n if (appliedCount > total) appliedCount = 0;\n if (appliedCount >= total) {\n break;\n }\n const rec = log.get(appliedCount);\n appliedCount += 1;\n if (!rec) continue;\n if (rec.c === myClientId) continue; // our own write — Univer already ran it\n if (rec.kind === 'snapshot') {\n // Compaction record from a peer — replace the local workbook\n // with the snapshot. Without `onSnapshotReceived` wired (e.g.\n // in unit tests that drive the bridge directly), skip the\n // record; the next post-snapshot mutations may still apply\n // cleanly if state is close enough.\n //\n // CRITICAL: await the handler. Univer's unit swap is async, and\n // continuing the loop before the new unit is wired into the\n // facade means rewriteUnitId() reads the OLD active unit and\n // every subsequent mutation targets a stale workbook.\n if (opts.onSnapshotReceived) {\n try {\n await opts.onSnapshotReceived(rec.wb);\n } catch (err) {\n console.warn('[collab] failed to apply compaction snapshot', err);\n }\n } else {\n console.warn(\n '[collab] received compaction snapshot but no handler — workbook may diverge',\n );\n }\n continue;\n }\n // Each browser creates its workbook with its OWN random unit id, so\n // raw replay would target the sender's unit (which doesn't exist\n // here) — rewrite to our local active unit. Sheet ids (`sheet-1`)\n // are already deterministic across the room.\n const params = rewriteUnitId(api, rec.p, rec.id);\n // Univer's ActiveWorksheetController unconditionally switches\n // the active sheet on every insert-sheet mutation — there's no\n // `fromCollab` opt-out inside Univer. Save our current active\n // sheet around the replay and restore it after the next tick\n // so peers don't get yanked to whichever sheet someone else\n // just created.\n const sheetBefore =\n rec.id === 'sheet.mutation.insert-sheet' ? captureActiveSheetId(api) : null;\n // Lazy-plugin gate: if this mutation belongs to a plugin we\n // haven't mounted yet (CF, tables, filter, notes,\n // hyperlinks), the mutation handler is missing and the\n // change drops silently. AWAIT plugin load before executing.\n // For mutations not in the map, this resolves to undefined\n // and the executeCommand fires immediately.\n const lazyGroup = MUTATION_TO_LAZY_GROUP[rec.id];\n // Fire-and-forget; ordering is preserved by Univer's command bus\n // serialising its own dispatch.\n //\n // Failure handling is two-class (see replay-retry.ts):\n // - TRANSIENT (dynamic-import chunk-load failures) → retry\n // with 300/900/2700 ms backoff. The lazy-plugin gate is\n // the common source: a network flap during webpack chunk\n // fetch rejects the import; retries land cleanly once\n // connectivity recovers.\n // - PERMANENT (malformed params, unknown command id, range\n // out-of-bounds) → dead-letter immediately. Retrying\n // just re-throws the same stack.\n //\n // Final failure (after retries exhausted OR permanent on\n // first throw) increments `replayFailures` AND appends to\n // the dead-letter ring buffer for the UI to render.\n const attempt = () =>\n (lazyGroup ? ensurePluginByName(lazyGroup) : Promise.resolve()).then(() =>\n cmdSvc.executeCommand(rec.id, params, { fromCollab: true }),\n );\n void withRetry(\n attempt,\n TRANSIENT_RETRY_DELAYS_MS,\n (err) => classifyReplayError(err) === 'transient',\n )\n .then(() => {\n if (sheetBefore) restoreActiveSheetId(api, sheetBefore);\n })\n .catch((err: unknown) => {\n const cls = classifyReplayError(err);\n const message = err instanceof Error ? err.message : String(err);\n console.warn(\n '[collab] replay failed for',\n rec.id,\n '(class:',\n cls + ',',\n 'gave up)',\n err,\n );\n const now = Date.now();\n const failure: ReplayFailureRecord = {\n id: rec.id,\n params: rec.p,\n lastError: message,\n // Permanent = 1 attempt; transient = 1 + N retries\n // configured in TRANSIENT_RETRY_DELAYS_MS.\n attempts: cls === 'transient' ? 1 + TRANSIENT_RETRY_DELAYS_MS.length : 1,\n firstFailedAt: now,\n lastFailedAt: now,\n classification: cls,\n };\n noteReplayFailure(failure);\n if (sheetBefore) restoreActiveSheetId(api, sheetBefore);\n });\n }\n } finally {\n // Only clear if we're still the in-flight token. A future\n // re-entrant guard scheme might let multiple flights coexist;\n // this check keeps us correct under that.\n if (replayInFlight === p) replayInFlight = null;\n resolveOuter();\n }\n })();\n return p;\n };\n\n const observer = (event: Y.YArrayEvent<OpRecord>) => {\n void event;\n void replayPending();\n };\n log.observe(observer);\n\n // Cover the initial-state case: when the bridge mounts after Yjs has\n // already synced the existing log (provider was connected before us),\n // observe() won't fire — we'd miss everything. Replay synchronously\n // once on mount to catch up.\n void replayPending();\n\n // ── Stage 6: periodic compaction by the designated writer ────────\n // Only one client in the room compacts at a time — the one with the\n // lowest known clientId. The interval guard prevents an over-eager\n // compactor from churning. View-only clients never compact.\n //\n // Seed `lastCompactedAt` to `now` so the first auto-compaction\n // observes the full COMPACT_MIN_INTERVAL_MS cooldown. Without this\n // seed, a fresh session that immediately crosses the op threshold\n // (e.g. a quick paste of >200 cells, or the e2e harness) would see\n // an instant first compaction before the test could observe the\n // pre-compaction log. The explicit `__bridgeForceCompact` path\n // bypasses this guard, so the test still works.\n let lastCompactedAt = Date.now();\n // Both scheduling paths declared in outer scope so the dispose\n // closure below can clean up either one.\n let intervalHandle: ReturnType<typeof setInterval> | null = null;\n let idleHandle: number | null = null;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const cic = (globalThis as any).cancelIdleCallback as undefined | ((id: number) => void);\n if (role !== 'view' && opts.awareness) {\n const awareness = opts.awareness;\n const tryCompact = (): void => {\n try {\n if (log.length < COMPACT_OPS_THRESHOLD) return;\n if (Date.now() - lastCompactedAt < COMPACT_MIN_INTERVAL_MS) return;\n // Designated writer = lowest clientId currently in awareness.\n // Math.min over the awareness keys, then compare to ours.\n const keys = Array.from(awareness.getStates().keys()) as number[];\n if (keys.length === 0) return;\n const designated = Math.min(...keys);\n if (designated !== doc.clientID) return;\n // Snapshot the live workbook.\n const wb = api.getActiveWorkbook();\n if (!wb) return;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const snap = (wb as any).save() as IWorkbookData;\n const snapshotRec: SnapshotRecord = {\n kind: 'snapshot',\n c: myClientId,\n t: Date.now(),\n wb: snap,\n };\n const opsBefore = log.length;\n // Atomic swap: clear then append. Yjs serializes the whole\n // transaction so subscribers see one consistent change.\n doc.transact(() => {\n log.delete(0, log.length);\n log.push([snapshotRec]);\n });\n // We just rewrote the log; our cursor must stay PAST the\n // snapshot (we already have its state). The replayer's\n // appliedCount > length reset would otherwise re-apply our\n // own snapshot which is a no-op but pointless.\n appliedCount = 1;\n lastCompactedAt = Date.now();\n console.info('[collab] op-log compacted: %d ops → 1 snapshot record', opsBefore);\n } catch (err) {\n console.warn('[collab] compaction attempt failed', err);\n }\n };\n // Schedule `tryCompact` via requestIdleCallback so the heavy\n // `wb.save()` only runs when the main thread is genuinely idle —\n // never mid-keystroke or mid-paste. The browser gives us a\n // deadline; if it expires before we'd start, we skip and wait\n // for the next tick. Fall back to a plain setInterval in\n // environments without rIC (Safari < 18, some test runners).\n //\n // `wb.save()` itself can't move to a Web Worker (the Univer\n // workbook is a main-thread object graph), so the realistic\n // optimisation is \"don't run it when the user is busy\".\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const ric = (globalThis as any).requestIdleCallback as\n | undefined\n | ((\n cb: (d: { didTimeout: boolean; timeRemaining: () => number }) => void,\n opts?: { timeout: number },\n ) => number);\n if (typeof ric === 'function') {\n const scheduleNext = () => {\n idleHandle = ric(\n (deadline) => {\n // Only run if we have at least ~5 ms to spare (typical\n // empty-workbook save is <1 ms, big ones a few ms;\n // anything longer should defer to the next idle window).\n if (deadline.didTimeout || deadline.timeRemaining() > 5) {\n tryCompact();\n }\n scheduleNext();\n },\n { timeout: COMPACT_CHECK_INTERVAL_MS },\n );\n };\n scheduleNext();\n } else {\n intervalHandle = setInterval(tryCompact, COMPACT_CHECK_INTERVAL_MS);\n intervalHandle.unref?.();\n }\n\n // Diagnostic sinks for the compaction e2e — lets a test trigger\n // compaction without waiting for the 30 s interval and read the live\n // log length. Installed unconditionally: now that the bridge ships\n // from the SDK bundle (not the app's Vite source), there's no reliable\n // `import.meta.env.DEV` to gate on, and these are the same class of\n // read-only/devtools escape hatch the host already exposes via\n // `__univerAPI` / `__hocuspocusProvider`. No secrets; `__bridgeForceCompact`\n // only triggers the compaction that runs on its own anyway.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (globalThis as any).__bridgeLogLength = () => log.length;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (globalThis as any).__bridgeForceCompact = () => {\n // Bypass the COMPACT_MIN_INTERVAL_MS guard so the test\n // doesn't have to sleep a minute.\n lastCompactedAt = 0;\n tryCompact();\n };\n }\n\n return {\n doc,\n dispose: () => {\n subDispose.dispose();\n subBeforeDispose.dispose();\n // Flush any pending batch so an edit-then-leave race doesn't drop\n // the last keystroke on the floor.\n if (pending.length > 0) flush();\n log.unobserve(observer);\n pendingUndo.clear();\n replayFailureSubscribers.clear();\n deadLetterSubscribers.clear();\n // Two scheduling paths in setup above — clean up whichever ran.\n if (intervalHandle) clearInterval(intervalHandle);\n if (idleHandle !== null && typeof cic === 'function') cic(idleHandle);\n },\n getReplayFailures: () => replayFailures,\n subscribeReplayFailures: (cb) => {\n replayFailureSubscribers.add(cb);\n return () => {\n replayFailureSubscribers.delete(cb);\n };\n },\n getReplayDeadLetter: () => deadLetter,\n subscribeReplayDeadLetter: (cb) => {\n deadLetterSubscribers.add(cb);\n return () => {\n deadLetterSubscribers.delete(cb);\n };\n },\n };\n}\n\n/**\n * Substitute the active local workbook's unit id into a mutation's\n * `unitId` fields — top-level AND nested ones (e.g. `range.unitId`,\n * `source.unitId`, `target.unitId`) — so cross-peer mutations target\n * our local workbook. Sheet-level `subUnitId` keys (`sheet-1`, …)\n * are deterministic from the emptyWorkbook snapshot, so they pass\n * through unchanged.\n *\n * Returns a structurally cloned params object so we don't mutate the\n * Yjs record (Y.Array entries are frozen plain objects). Walks objects\n * and arrays recursively; stops at non-plain values (strings, numbers,\n * dates, etc.).\n *\n * Performance: most mutations have shallow params — the recursive walk\n * adds microseconds. Per-cell value maps stay shallow because they're\n * indexed by stringified row/col, not nested objects.\n */\nfunction rewriteUnitId(api: FUniver, params: unknown, mutationId?: string): unknown {\n const wb = api.getActiveWorkbook();\n if (!wb) return params;\n const localUnitId = wb.getId();\n // Capture the sender's unitId BEFORE deepRewriteUnitId swaps it —\n // drawing mutations need it to patch the json1 op path (which\n // carries unitId in position [0] of a positional array, out of\n // deepRewriteUnitId's reach since it only rewrites object KEYS\n // named `unitId`). See bridge-helpers.ts → rewriteJson1OpPathUnitId.\n //\n // Stream F1 fix: without this, set-drawing-apply replays on a\n // joiner with the OWNER's unitId still embedded in the op,\n // json1.type.apply walks a path that doesn't exist locally,\n // throws a bare \"Error\" with no message, classifier lands it as\n // PERMANENT, and the drawing silently fails to propagate.\n let senderUnitId: string | undefined;\n if (mutationId === 'sheet.mutation.set-drawing-apply' && params && typeof params === 'object') {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const u = (params as any).unitId;\n if (typeof u === 'string') senderUnitId = u;\n }\n const rewritten = deepRewriteUnitId(params, localUnitId) as unknown;\n if (!senderUnitId || senderUnitId === localUnitId) return rewritten;\n // Drawing mutations only — patch the op's positional path[0].\n if (rewritten && typeof rewritten === 'object') {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const r = rewritten as any;\n const fixedOp = rewriteJson1OpPathUnitId(r.op, senderUnitId, localUnitId);\n if (fixedOp !== r.op) {\n return { ...r, op: fixedOp };\n }\n }\n return rewritten;\n}\n\n/**\n * Save the local active sheet id before replaying a `fromCollab`\n * mutation that Univer's controllers may use as a side-channel signal\n * to switch sheets (notably `insert-sheet` — see\n * ActiveWorksheetController in @univerjs/sheets). Returning `null`\n * means \"couldn't read, don't try to restore\".\n */\nfunction captureActiveSheetId(api: FUniver): string | null {\n try {\n const wb = api.getActiveWorkbook();\n if (!wb) return null;\n const sheet = wb.getActiveSheet();\n if (!sheet) return null;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const id = (sheet as any).getSheetId?.() ?? (sheet as any).getId?.() ?? null;\n return typeof id === 'string' ? id : null;\n } catch {\n return null;\n }\n}\n\nfunction restoreActiveSheetId(api: FUniver, sheetId: string): void {\n try {\n const wb = api.getActiveWorkbook();\n if (!wb) return;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const current = (wb.getActiveSheet() as any)?.getSheetId?.();\n if (current === sheetId) return; // nothing to do\n const sheets = wb.getSheets();\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const target = sheets.find((s: any) => s.getSheetId?.() === sheetId);\n if (!target) return; // sheet got deleted in the meantime — leave Univer's choice alone\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (wb as any).setActiveSheet?.(target);\n } catch (err) {\n console.warn('[collab] failed to restore active sheet after remote insert-sheet', err);\n }\n}\n\n/**\n * Cheap probe for the `__splitChunk__` marker — a flag Univer used in\n * earlier versions to indicate a mutation was one chunk of a larger\n * operation (large paste, copy-worksheet). Univer 0.22.x doesn't emit\n * it, but if a future upgrade reintroduces it our op-log replay would\n * silently corrupt because we don't reassemble chunks. Watchdog logs\n * a warning the first time it sees one so the regression is loud.\n *\n * Walks one level deep — Univer carried the marker on the top-level\n * params object historically. Deeper nesting would be a different\n * shape and warrants a different fix.\n */\nfunction hasSplitChunkMarker(params: unknown): boolean {\n if (!params || typeof params !== 'object') return false;\n\n return Object.prototype.hasOwnProperty.call(params, '__splitChunk__');\n}\n","/**\n * Pure helpers extracted from bridge.ts so they're testable without\n * pulling Univer / Yjs / Vite-only globals into a Node test runner.\n *\n * Anything stateless and Univer-free that bridge.ts needs should live\n * here. Stateful bridge logic (Yjs observers, command service hooks,\n * compaction) stays in bridge.ts itself.\n */\n\n/**\n * Substitute `localUnitId` for every `unitId` string property anywhere\n * in `value` — including nested objects and arrays — and return a\n * structurally cloned copy. Returns the input by reference when no\n * change is needed, so callers can identity-compare before re-encoding.\n *\n * Walks plain objects and arrays only. Class instances are left alone\n * because Univer mutation params are required to be JSON-friendly.\n */\n/**\n * Drawing mutations (sheets-drawing + drawing plugins) carry an\n * `op` field that's a json1 patch — a positional array whose first\n * element is the unitId. `deepRewriteUnitId` only rewrites the\n * `unitId` KEY in objects; the json1 path is a bare array of strings\n * so the unitId at position 0 stays as the OWNER's id and the apply\n * fails on the joiner with no useful message (\"Error\" at\n * json1.type.apply).\n *\n * This walks the op shape — single JSONOp (array of mixed strings +\n * numbers + a final mutation component object) OR JSONOpList (array\n * of JSONOps) — and substitutes the leading unitId where it matches.\n * Returns a fresh structure on change so peers can identity-compare.\n *\n * Scope deliberately narrow: only checks element [0] of each op\n * (where the unitId always lives in our path schema). A deeper\n * path-rewrite would invent semantics the Univer source doesn't\n * document.\n */\nexport function rewriteJson1OpPathUnitId(\n op: unknown,\n oldUnitId: string,\n newUnitId: string,\n): unknown {\n if (oldUnitId === newUnitId) return op;\n if (!Array.isArray(op)) return op;\n // Distinguish single JSONOp (path...component) from JSONOpList\n // (array of JSONOps). A JSONOp's elements are strings, numbers, or\n // a single trailing component object. A JSONOpList's elements are\n // themselves arrays.\n const looksLikeOpList = op.length > 0 && Array.isArray(op[0]);\n if (looksLikeOpList) {\n let changed = false;\n const next = op.map((entry) => {\n const r = rewriteJson1OpPathUnitId(entry, oldUnitId, newUnitId);\n if (r !== entry) changed = true;\n return r;\n });\n return changed ? next : op;\n }\n // Single JSONOp — substitute element [0] if it matches.\n if (op[0] === oldUnitId) {\n const next = [...op];\n next[0] = newUnitId;\n return next;\n }\n return op;\n}\n\nexport function deepRewriteUnitId(value: unknown, localUnitId: string): unknown {\n if (Array.isArray(value)) {\n let changed = false;\n const next = value.map((item) => {\n const r = deepRewriteUnitId(item, localUnitId);\n if (r !== item) changed = true;\n return r;\n });\n return changed ? next : value;\n }\n if (value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const obj = value as Record<string, any>;\n let changed = false;\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(obj)) {\n const v = obj[key];\n if (key === 'unitId' && typeof v === 'string' && v !== localUnitId) {\n out[key] = localUnitId;\n changed = true;\n } else {\n const r = deepRewriteUnitId(v, localUnitId);\n if (r !== v) changed = true;\n out[key] = r;\n }\n }\n return changed ? out : value;\n }\n return value;\n}\n","import type { Univer, IWorkbookData } from '@univerjs/core';\n\n/**\n * Lazy plugin loading — pipeline Stage 4. The cheap plugins (render,\n * formula, sheets, sheets-ui, numfmt, docs) stay eager because every\n * workbook needs them. The heavy / feature-specific plugins (CF, DV,\n * hyperlink, table, note, thread-comment, drawing, sort, filter,\n * find-replace) load on demand:\n *\n * 1. Eagerly when a snapshot is about to mount and we detect the\n * plugin's resource key on it (e.g. `SHEET_CONDITIONAL_FORMATTING_PLUGIN`\n * in `data.resources`). This is the safety net: missing this would\n * silently drop plugin data on file open.\n * 2. Lazily when the user reaches for the feature — opens the Data\n * tab (sort/filter), hits Ctrl+F (find-replace), uses the Insert\n * tab (drawing), etc. The shell hooks `ensurePlugin(...)` into\n * these triggers and awaits before the action runs.\n *\n * Each loader returns the plugins-to-register in the correct order\n * (base before UI, same as `plugins.ts`). Registration order matters\n * in Univer; the loaders bundle a small group together to keep that\n * locality explicit.\n */\n\nexport type LazyPluginGroup =\n | 'cf'\n | 'dv'\n | 'hyperlink'\n | 'note'\n | 'table'\n | 'threadComment'\n | 'drawing'\n | 'sort'\n | 'filter'\n | 'findReplace';\n\ntype Loader = () => Promise<Array<[unknown, unknown?]>>;\n\nconst LOADERS: Record<LazyPluginGroup, Loader> = {\n cf: async () => {\n const [base, ui] = await Promise.all([\n import('@univerjs/sheets-conditional-formatting'),\n import('@univerjs/sheets-conditional-formatting-ui'),\n ]);\n return [\n [base.UniverSheetsConditionalFormattingPlugin],\n [ui.UniverSheetsConditionalFormattingUIPlugin],\n ];\n },\n dv: async () => {\n const [base, ui] = await Promise.all([\n import('@univerjs/sheets-data-validation'),\n import('@univerjs/sheets-data-validation-ui'),\n ]);\n return [\n [base.UniverSheetsDataValidationPlugin],\n [ui.UniverSheetsDataValidationUIPlugin],\n ];\n },\n hyperlink: async () => {\n const [base, ui] = await Promise.all([\n import('@univerjs/sheets-hyper-link'),\n import('@univerjs/sheets-hyper-link-ui'),\n ]);\n return [\n [base.UniverSheetsHyperLinkPlugin],\n [ui.UniverSheetsHyperLinkUIPlugin],\n ];\n },\n note: async () => {\n const [base, ui] = await Promise.all([\n import('@univerjs/sheets-note'),\n import('@univerjs/sheets-note-ui'),\n ]);\n return [[base.UniverSheetsNotePlugin], [ui.UniverSheetsNoteUIPlugin]];\n },\n table: async () => {\n const [base, ui] = await Promise.all([\n import('@univerjs/sheets-table'),\n import('@univerjs/sheets-table-ui'),\n ]);\n return [[base.UniverSheetsTablePlugin], [ui.UniverSheetsTableUIPlugin]];\n },\n threadComment: async () => {\n const [tc, tcUi, sheetsTc, sheetsTcUi] = await Promise.all([\n import('@univerjs/thread-comment'),\n import('@univerjs/thread-comment-ui'),\n import('@univerjs/sheets-thread-comment'),\n import('@univerjs/sheets-thread-comment-ui'),\n ]);\n return [\n [tc.UniverThreadCommentPlugin],\n [tcUi.UniverThreadCommentUIPlugin],\n [sheetsTc.UniverSheetsThreadCommentPlugin],\n [sheetsTcUi.UniverSheetsThreadCommentUIPlugin],\n ];\n },\n drawing: async () => {\n const [d, dUi, sd, sdUi] = await Promise.all([\n import('@univerjs/drawing'),\n import('@univerjs/drawing-ui'),\n import('@univerjs/sheets-drawing'),\n import('@univerjs/sheets-drawing-ui'),\n // Side-effect imports: install FWorksheet.insertImage / getImages /\n // updateImages on the facade prototype. Without these, code that\n // reaches in via the FUniver facade (e2e specs, future shell glue)\n // sees an undefined method even though the plugin is registered.\n import('@univerjs/sheets-drawing/facade'),\n import('@univerjs/sheets-drawing-ui/facade'),\n ]);\n return [\n [d.UniverDrawingPlugin],\n [dUi.UniverDrawingUIPlugin],\n [sd.UniverSheetsDrawingPlugin],\n [sdUi.UniverSheetsDrawingUIPlugin],\n ];\n },\n sort: async () => {\n const [base, ui] = await Promise.all([\n import('@univerjs/sheets-sort'),\n import('@univerjs/sheets-sort-ui'),\n ]);\n return [[base.UniverSheetsSortPlugin], [ui.UniverSheetsSortUIPlugin]];\n },\n filter: async () => {\n const [base, ui] = await Promise.all([\n import('@univerjs/sheets-filter'),\n import('@univerjs/sheets-filter-ui'),\n ]);\n return [[base.UniverSheetsFilterPlugin], [ui.UniverSheetsFilterUIPlugin]];\n },\n findReplace: async () => {\n const [base, sheets] = await Promise.all([\n import('@univerjs/find-replace'),\n import('@univerjs/sheets-find-replace'),\n ]);\n return [[base.UniverFindReplacePlugin], [sheets.UniverSheetsFindReplacePlugin]];\n },\n};\n\n/**\n * Map from a Univer `data.resources[].name` to the lazy group that owns\n * it. Used by `eagerLoadForSnapshot` to pre-register plugins whose\n * state already lives on the workbook so file-open never silently\n * drops a CF rule / table / drawing / etc.\n */\nconst RESOURCE_NAME_TO_GROUP: Record<string, LazyPluginGroup> = {\n SHEET_CONDITIONAL_FORMATTING_PLUGIN: 'cf',\n SHEET_DATA_VALIDATION_PLUGIN: 'dv',\n SHEET_HYPER_LINK_PLUGIN: 'hyperlink',\n SHEET_NOTE_PLUGIN: 'note',\n SHEET_TABLE_PLUGIN: 'table',\n SHEET_THREAD_COMMENT_BASE_PLUGIN: 'threadComment',\n SHEET_DRAWING_PLUGIN: 'drawing',\n SHEET_SORT_PLUGIN: 'sort',\n SHEET_FILTER_PLUGIN: 'filter',\n};\n\nconst loaded = new Set<LazyPluginGroup>();\nconst inflight = new Map<LazyPluginGroup, Promise<void>>();\n\n/**\n * Module-level reference to the live Univer instance, set by\n * `UniverSheet.tsx` immediately after `new Univer()`. Lets shell code\n * call `ensurePluginByName(group)` without plumbing the Univer\n * instance through React context (the FUniver facade doesn't expose\n * its host). Cleared on dispose so callers fail loudly if they hit\n * the lazy path after teardown.\n */\nlet currentUniver: Univer | null = null;\n\nexport function setUniverForLazyLoad(univer: Univer | null): void {\n currentUniver = univer;\n}\n\n/**\n * Idempotent: subsequent calls for the same group resolve immediately\n * if the plugin is already loaded; concurrent calls share the same\n * in-flight promise so we never double-register.\n */\nexport function ensurePlugin(univer: Univer, group: LazyPluginGroup): Promise<void> {\n if (loaded.has(group)) return Promise.resolve();\n const existing = inflight.get(group);\n if (existing) return existing;\n const loader = LOADERS[group];\n if (!loader) return Promise.resolve();\n const p = loader().then((plugins) => {\n for (const [PluginCtor, config] of plugins) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n univer.registerPlugin(PluginCtor as any, config);\n }\n loaded.add(group);\n inflight.delete(group);\n });\n inflight.set(group, p);\n return p;\n}\n\n/**\n * Shell-friendly variant of `ensurePlugin` that pulls the Univer\n * instance from the module-level holder set by `UniverSheet.tsx`. Use\n * this anywhere we only have the FUniver facade (toolbar callbacks,\n * panel handlers). Returns a resolved promise if the holder is empty —\n * the worst case is a no-op, never a throw.\n */\nexport function ensurePluginByName(group: LazyPluginGroup): Promise<void> {\n if (loaded.has(group)) return Promise.resolve();\n if (!currentUniver) return Promise.resolve();\n return ensurePlugin(currentUniver, group);\n}\n\n/**\n * Walk a snapshot for plugin-owned resources + side-channel hyperlinks\n * and eagerly load every group that's referenced. Returns a promise\n * the caller MUST await before `createUnit` — Univer's resource manager\n * silently discards keys for plugins that aren't yet registered.\n */\nexport async function eagerLoadForSnapshot(univer: Univer, snapshot: IWorkbookData): Promise<void> {\n const groups = new Set<LazyPluginGroup>();\n const resources = snapshot.resources ?? [];\n for (const r of resources) {\n const g = RESOURCE_NAME_TO_GROUP[r.name];\n if (g) groups.add(g);\n }\n // Hyperlinks live inline in cell.p (Stage 5) — the hyperlink plugin\n // still owns click handling / context-menu actions, so eager-load\n // when any cell has a HYPERLINK customRange.\n if (snapshotHasHyperlinks(snapshot)) groups.add('hyperlink');\n await Promise.all(Array.from(groups).map((g) => ensurePlugin(univer, g)));\n}\n\n/**\n * Idle-load every remaining lazy group AFTER Univer is mounted. The\n * bundle split (each group ships as its own chunk) is the persistent\n * boot-time win — the initial paint doesn't pay for them. This\n * idle-load just ensures they're all eventually registered so a user\n * clicking \"Insert > Table\" doesn't hit a no-op.\n *\n * Loads in parallel via `requestIdleCallback` so they don't compete\n * with the first paint, falling back to `setTimeout(0)` in browsers\n * without rIC (Safari).\n */\nexport function idleLoadAll(univer: Univer): void {\n const groups = Object.keys(LOADERS) as LazyPluginGroup[];\n schedule(() => {\n for (const g of groups) {\n void ensurePlugin(univer, g);\n }\n });\n}\n\nfunction schedule(fn: () => void): void {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const ric = (globalThis as any).requestIdleCallback as\n | ((cb: () => void, opts?: { timeout: number }) => number)\n | undefined;\n if (ric) ric(fn, { timeout: 500 });\n else setTimeout(fn, 0);\n}\n\nfunction snapshotHasHyperlinks(snapshot: IWorkbookData): boolean {\n const sheetOrder = snapshot.sheetOrder ?? [];\n for (const sid of sheetOrder) {\n const sheet = snapshot.sheets?.[sid];\n if (!sheet?.cellData) continue;\n const cellData = sheet.cellData as Record<\n string,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n Record<string, { p?: any }>\n >;\n for (const r of Object.keys(cellData)) {\n const row = cellData[r];\n for (const c of Object.keys(row)) {\n const ranges = row[c]?.p?.body?.customRanges ?? [];\n if (ranges.some((cr: { rangeType?: number }) => cr.rangeType === 0)) return true;\n }\n }\n }\n return false;\n}\n","/**\n * Replay-failure retry classifier + scheduler.\n *\n * Pure helpers used by the Yjs bridge to recover from transient\n * mutation-replay failures (most often dynamic-import chunk-load\n * errors during the lazy-plugin gate) without conflating them with\n * permanent failures (malformed mutation params, unknown command id).\n *\n * Decoupled from bridge.ts so we can unit-test the classification +\n * backoff logic without standing up a full Yjs doc + Univer engine.\n *\n * See docs/PRODUCTION_PIPELINE.md → Stream A1 for design context.\n */\n\n/** Maximum retry attempts for transient failures. */\nexport const TRANSIENT_RETRY_DELAYS_MS = [300, 900, 2700] as const;\n\n/** Maximum entries kept in the dead-letter ring buffer. */\nexport const DEAD_LETTER_CAP = 20;\n\nexport type ReplayClassification = 'transient' | 'permanent';\n\nexport interface ReplayFailureRecord {\n /** Mutation id (e.g. 'sheet.mutation.set-range-values'). */\n id: string;\n /** Mutation params at time of failure (unfiltered; may be large). */\n params: unknown;\n /** Last error's message (string-coerced for log/UI safety). */\n lastError: string;\n /** Total attempts made (1 = first try, no retries). */\n attempts: number;\n /** ms-since-epoch of the FIRST failure for this record. */\n firstFailedAt: number;\n /** ms-since-epoch of the most recent failure for this record. */\n lastFailedAt: number;\n /** Whether the classifier called this transient or permanent. */\n classification: ReplayClassification;\n}\n\n/**\n * Decide whether a replay error is worth retrying.\n *\n * Transient class: dynamic-import / chunk-load failures. These come\n * from the lazy-plugin gate fetching a webpack chunk over the\n * network; a flap on the user's connection drops the fetch and the\n * lazy `import()` rejects with one of several distinct shapes:\n *\n * - Webpack 5: `ChunkLoadError` with name === 'ChunkLoadError'\n * - Webpack 4 / vite: 'Loading chunk N failed.'\n * - Vite / native ESM: 'Failed to fetch dynamically imported module'\n * - Native fetch under offline: 'NetworkError when attempting to fetch'\n *\n * All four resolve on retry once the network recovers. Everything\n * else (executeCommand rejections from bad params, missing handlers,\n * out-of-bounds ranges) is permanent — retrying just burns the same\n * stack trace N more times.\n *\n * Conservative bias: when in doubt, classify permanent. A false\n * negative (transient → permanent) costs us a dead-letter entry the\n * user can re-trigger by reloading; a false positive (permanent →\n * transient) wastes 4 s of retries on a known-broken mutation.\n */\nexport function classifyReplayError(err: unknown): ReplayClassification {\n if (err == null) return 'permanent';\n\n // Error instances: check name first (ChunkLoadError sets this even\n // though it's not on stock Error), then message.\n const e = err as { name?: unknown; message?: unknown };\n const name = typeof e.name === 'string' ? e.name : '';\n const message = typeof e.message === 'string' ? e.message : String(err);\n\n if (name === 'ChunkLoadError') return 'transient';\n\n const lower = message.toLowerCase();\n if (lower.includes('loading chunk') && lower.includes('failed')) {\n return 'transient';\n }\n if (lower.includes('failed to fetch dynamically imported')) {\n return 'transient';\n }\n if (lower.includes('networkerror when attempting to fetch')) {\n return 'transient';\n }\n // Generic 'network request failed' from some bundler error wrappers.\n if (lower.includes('network request failed')) return 'transient';\n\n return 'permanent';\n}\n\n/**\n * Run `task` with up to `delays.length` retries. The first attempt is\n * immediate; each subsequent attempt waits `delays[i-1]` ms.\n *\n * Resolves with the task's value on the first success. Rejects with\n * the LAST error after all attempts fail.\n *\n * `shouldRetry` is called between attempts. If it returns false, the\n * loop bails immediately with the current error — this lets the\n * caller switch to permanent-failure handling mid-sequence (e.g. if\n * a follow-up error reveals the real problem was permanent all along).\n */\nexport async function withRetry<T>(\n task: () => Promise<T>,\n delays: readonly number[],\n shouldRetry: (err: unknown, attemptsSoFar: number) => boolean = () => true,\n sleep: (ms: number) => Promise<void> = (ms) => new Promise((r) => setTimeout(r, ms)),\n): Promise<T> {\n let lastErr: unknown;\n for (let i = 0; i <= delays.length; i += 1) {\n try {\n return await task();\n } catch (err) {\n lastErr = err;\n const attempts = i + 1;\n if (i >= delays.length) break;\n if (!shouldRetry(err, attempts)) break;\n await sleep(delays[i]);\n }\n }\n throw lastErr;\n}\n\n/**\n * Ring buffer for dead-letter records. Append-only; oldest entry\n * evicts on overflow. Caller owns the backing array — we return a\n * new array on push so React state updates can see the change by\n * reference.\n */\nexport function pushDeadLetter(\n buffer: readonly ReplayFailureRecord[],\n rec: ReplayFailureRecord,\n cap: number = DEAD_LETTER_CAP,\n): ReplayFailureRecord[] {\n const next =\n buffer.length >= cap ? [...buffer.slice(buffer.length - cap + 1), rec] : [...buffer, rec];\n return next;\n}\n","/**\n * Pure WS-URL composition for attachCollab. Kept in its own module — free of\n * any `@univerjs` / `@hocuspocus` imports — so it's unit-testable under\n * `node:test` without dragging in the Univer ESM graph.\n */\n\n/** Build the room WS URL: `<server>?room=<id>[&p=<pw>]&role=<role>`. */\nexport function buildWsUrl(\n server: string,\n room: string,\n role: 'view' | 'write',\n password?: string,\n): string {\n const sep = server.includes('?') ? '&' : '?';\n return (\n `${server}${sep}room=${encodeURIComponent(room)}` +\n `${password ? `&p=${encodeURIComponent(password)}` : ''}` +\n `&role=${encodeURIComponent(role)}`\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import * as Y from 'yjs';
|
|
2
|
+
import { HocuspocusProvider } from '@hocuspocus/provider';
|
|
3
|
+
import { IWorkbookData } from '@univerjs/core';
|
|
4
|
+
import { FUniver } from '@univerjs/core/facade';
|
|
5
|
+
import { C as CasualSheetsAPI } from './api-BI2VLYQ6.cjs';
|
|
6
|
+
|
|
7
|
+
type ReplayClassification = 'transient' | 'permanent';
|
|
8
|
+
interface ReplayFailureRecord {
|
|
9
|
+
/** Mutation id (e.g. 'sheet.mutation.set-range-values'). */
|
|
10
|
+
id: string;
|
|
11
|
+
/** Mutation params at time of failure (unfiltered; may be large). */
|
|
12
|
+
params: unknown;
|
|
13
|
+
/** Last error's message (string-coerced for log/UI safety). */
|
|
14
|
+
lastError: string;
|
|
15
|
+
/** Total attempts made (1 = first try, no retries). */
|
|
16
|
+
attempts: number;
|
|
17
|
+
/** ms-since-epoch of the FIRST failure for this record. */
|
|
18
|
+
firstFailedAt: number;
|
|
19
|
+
/** ms-since-epoch of the most recent failure for this record. */
|
|
20
|
+
lastFailedAt: number;
|
|
21
|
+
/** Whether the classifier called this transient or permanent. */
|
|
22
|
+
classification: ReplayClassification;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Decide whether a replay error is worth retrying.
|
|
26
|
+
*
|
|
27
|
+
* Transient class: dynamic-import / chunk-load failures. These come
|
|
28
|
+
* from the lazy-plugin gate fetching a webpack chunk over the
|
|
29
|
+
* network; a flap on the user's connection drops the fetch and the
|
|
30
|
+
* lazy `import()` rejects with one of several distinct shapes:
|
|
31
|
+
*
|
|
32
|
+
* - Webpack 5: `ChunkLoadError` with name === 'ChunkLoadError'
|
|
33
|
+
* - Webpack 4 / vite: 'Loading chunk N failed.'
|
|
34
|
+
* - Vite / native ESM: 'Failed to fetch dynamically imported module'
|
|
35
|
+
* - Native fetch under offline: 'NetworkError when attempting to fetch'
|
|
36
|
+
*
|
|
37
|
+
* All four resolve on retry once the network recovers. Everything
|
|
38
|
+
* else (executeCommand rejections from bad params, missing handlers,
|
|
39
|
+
* out-of-bounds ranges) is permanent — retrying just burns the same
|
|
40
|
+
* stack trace N more times.
|
|
41
|
+
*
|
|
42
|
+
* Conservative bias: when in doubt, classify permanent. A false
|
|
43
|
+
* negative (transient → permanent) costs us a dead-letter entry the
|
|
44
|
+
* user can re-trigger by reloading; a false positive (permanent →
|
|
45
|
+
* transient) wastes 4 s of retries on a known-broken mutation.
|
|
46
|
+
*/
|
|
47
|
+
declare function classifyReplayError(err: unknown): ReplayClassification;
|
|
48
|
+
|
|
49
|
+
type Awareness = {
|
|
50
|
+
getStates(): Map<number, any>;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Allowlist of mutation ids we sync. Listed explicitly to keep
|
|
54
|
+
* undocumented / version-volatile mutations out of the log — anything
|
|
55
|
+
* not here just stays local. Easier to add new ids than to debug a
|
|
56
|
+
* silent corruption from a mutation that secretly references local
|
|
57
|
+
* state (selections, render skeletons, etc.).
|
|
58
|
+
*/
|
|
59
|
+
declare const SYNCED_MUTATIONS: ReadonlySet<string>;
|
|
60
|
+
/** Mutation ids for which the bridge captures undo params before the
|
|
61
|
+
* redo runs. Used by the HistoryPanel's revert action. Restricted to
|
|
62
|
+
* cell-level mutations because the existing Univer factories cover
|
|
63
|
+
* them and they're the dominant case for "undo my edit"; structural
|
|
64
|
+
* ops (insert-row / move-range / sort) need their own factories and
|
|
65
|
+
* are out of scope for v1 revert.
|
|
66
|
+
*/
|
|
67
|
+
declare const REVERTABLE_MUTATIONS: ReadonlySet<string>;
|
|
68
|
+
type BridgeHandle = {
|
|
69
|
+
/** Underlying Yjs document — exposed so tests / devtools can introspect. */
|
|
70
|
+
doc: Y.Doc;
|
|
71
|
+
/** Stop listening and detach. */
|
|
72
|
+
dispose: () => void;
|
|
73
|
+
/**
|
|
74
|
+
* How many remote mutations have thrown during replay since the
|
|
75
|
+
* bridge started. Each one is a candidate divergence — the local
|
|
76
|
+
* state didn't accept the peer's change, so the two state vectors
|
|
77
|
+
* are now off by at least that mutation. The CollabDriver
|
|
78
|
+
* subscribes (see `subscribeReplayFailures`) so the indicator can
|
|
79
|
+
* warn the user before they discover it the hard way.
|
|
80
|
+
*/
|
|
81
|
+
getReplayFailures: () => number;
|
|
82
|
+
/**
|
|
83
|
+
* Subscribe to replay-failure count changes. Fires after every
|
|
84
|
+
* increment with the new total. Returns a teardown that unhooks
|
|
85
|
+
* the subscriber. No initial-value fire — caller can read
|
|
86
|
+
* `getReplayFailures()` once at subscribe time if they need it.
|
|
87
|
+
*/
|
|
88
|
+
subscribeReplayFailures: (cb: (count: number) => void) => () => void;
|
|
89
|
+
/**
|
|
90
|
+
* Snapshot of the dead-letter ring buffer — mutations that
|
|
91
|
+
* exhausted retries (transient class) or failed immediately
|
|
92
|
+
* (permanent class). Capped at DEAD_LETTER_CAP entries; oldest
|
|
93
|
+
* evicts on overflow. UI consumes this to render the per-failure
|
|
94
|
+
* detail panel.
|
|
95
|
+
*/
|
|
96
|
+
getReplayDeadLetter: () => readonly ReplayFailureRecord[];
|
|
97
|
+
/**
|
|
98
|
+
* Subscribe to dead-letter changes. Fires after every push with a
|
|
99
|
+
* fresh array (reference change — React state updates see it).
|
|
100
|
+
* Returns a teardown.
|
|
101
|
+
*/
|
|
102
|
+
subscribeReplayDeadLetter: (cb: (entries: readonly ReplayFailureRecord[]) => void) => () => void;
|
|
103
|
+
};
|
|
104
|
+
type BridgeOptions = {
|
|
105
|
+
/**
|
|
106
|
+
* `view` clients only RECEIVE remote updates — local mutations don't
|
|
107
|
+
* append to the log, so peers never see them. Client-side gate only;
|
|
108
|
+
* a determined user can run the bridge in write mode by editing the
|
|
109
|
+
* URL. Server-side enforcement is a follow-up hardening pass.
|
|
110
|
+
*/
|
|
111
|
+
role?: 'view' | 'write';
|
|
112
|
+
/**
|
|
113
|
+
* Provider's Yjs awareness. Used (a) to determine which peer is the
|
|
114
|
+
* designated compactor (lowest known clientId — deterministic and
|
|
115
|
+
* race-free) and (b) so view-only clients don't try to compact. If
|
|
116
|
+
* omitted, compaction is disabled.
|
|
117
|
+
*/
|
|
118
|
+
awareness?: Awareness;
|
|
119
|
+
/**
|
|
120
|
+
* Hand a fresh workbook snapshot to the host when a compaction
|
|
121
|
+
* record arrives from a peer. The bridge can't call
|
|
122
|
+
* `replaceWorkbook` directly (it lives in React state); the host
|
|
123
|
+
* (`CollabDriver`) wires this through.
|
|
124
|
+
*
|
|
125
|
+
* MAY return a promise. Replay of subsequent op-log entries is
|
|
126
|
+
* paused until the promise resolves — without that, mutations
|
|
127
|
+
* land on the OLD unit before Univer's async unit-swap completes,
|
|
128
|
+
* which silently forks state on late joiners.
|
|
129
|
+
*/
|
|
130
|
+
onSnapshotReceived?: (wb: IWorkbookData) => void | Promise<void>;
|
|
131
|
+
};
|
|
132
|
+
declare function startBridge(api: FUniver, doc: Y.Doc, opts?: BridgeOptions): BridgeHandle;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* attachCollab — opt-in real-time co-editing for `<CasualSheets>`.
|
|
136
|
+
*
|
|
137
|
+
* The editor ships **collab-unaware**: it boots, edits, and persists with no
|
|
138
|
+
* knowledge of rooms or peers. A host that wants co-editing calls
|
|
139
|
+
* `attachCollab(api, { room, server })` once after `onReady` and gets back a
|
|
140
|
+
* detach handle. That's the entire public surface — everything else (presence
|
|
141
|
+
* UI, password prompts, room preflight, reconnect banners) is the host's to
|
|
142
|
+
* build on top of the returned `provider` / `doc`.
|
|
143
|
+
*
|
|
144
|
+
* Transport: Yjs + Hocuspocus, exactly as the reference host (`apps/web`) uses
|
|
145
|
+
* it. The non-negotiable Univer hooks live in `./bridge` — it subscribes to
|
|
146
|
+
* `ICommandService.onMutationExecutedForCollab`, applies remote mutations with
|
|
147
|
+
* `IExecutionOptions.fromCollab` (echo-loop prevention), and guards
|
|
148
|
+
* `params.__splitChunk__`.
|
|
149
|
+
*
|
|
150
|
+
* Persistence note: Yjs/Hocuspocus is the **realtime transport only**. The
|
|
151
|
+
* authoritative document is still saved by the host via WOPI / its own backend
|
|
152
|
+
* (the save/exit event contract) — collab does not turn the SDK into a store.
|
|
153
|
+
*/
|
|
154
|
+
|
|
155
|
+
/** Either the SDK's imperative API (`onReady`) or the bare FUniver facade.
|
|
156
|
+
* Collab only needs the facade, so a host that holds the raw FUniver (the
|
|
157
|
+
* reference app does) can attach without constructing a CasualSheetsAPI. */
|
|
158
|
+
type CollabAttachable = CasualSheetsAPI | FUniver;
|
|
159
|
+
/** `write` peers broadcast their edits; `view` peers only receive. The
|
|
160
|
+
* client-side gate is belt-and-braces — real enforcement is the server's
|
|
161
|
+
* `role` check on the WS upgrade. */
|
|
162
|
+
type CollabRole = 'view' | 'write';
|
|
163
|
+
/** Coarse connection state mapped from Hocuspocus's provider status. */
|
|
164
|
+
type CollabConnectionStatus = 'connecting' | 'live' | 'offline';
|
|
165
|
+
interface AttachCollabOptions {
|
|
166
|
+
/** Room / document id. Becomes the Hocuspocus document name. */
|
|
167
|
+
room: string;
|
|
168
|
+
/** Base WebSocket URL of the collab server, e.g. `wss://host/yjs`. */
|
|
169
|
+
server: string;
|
|
170
|
+
/** Room password, if the server gates the room. Sent on the WS URL as
|
|
171
|
+
* `p=…` so the upgrade handler can validate before the protocol handshake. */
|
|
172
|
+
password?: string;
|
|
173
|
+
/** Auth token for the Hocuspocus handshake. The provider only sends its
|
|
174
|
+
* auth submessage when this is truthy; servers with an `onAuthenticate`
|
|
175
|
+
* hook keep the connection queued without it. Defaults to `'anon'` (the
|
|
176
|
+
* reference server's hook only reads the `role` query param). */
|
|
177
|
+
token?: string;
|
|
178
|
+
/** `view` joins read-only. Defaults to `'write'`. */
|
|
179
|
+
role?: CollabRole;
|
|
180
|
+
/**
|
|
181
|
+
* Called when a peer's compaction snapshot arrives — the host swaps the
|
|
182
|
+
* workbook (typically `api.loadSnapshot(wb)`). MAY return a promise; the
|
|
183
|
+
* bridge pauses op-log replay until it resolves so later mutations don't
|
|
184
|
+
* land on the pre-swap unit.
|
|
185
|
+
*/
|
|
186
|
+
onSnapshot?: (wb: IWorkbookData) => void | Promise<void>;
|
|
187
|
+
/** Connection-status transitions — drive a status pill / offline banner. */
|
|
188
|
+
onStatus?: (status: CollabConnectionStatus) => void;
|
|
189
|
+
}
|
|
190
|
+
interface CollabHandle {
|
|
191
|
+
/** The underlying Yjs document — introspection / devtools / extra maps. */
|
|
192
|
+
readonly doc: Y.Doc;
|
|
193
|
+
/** The Hocuspocus provider — `awareness` for presence, `on('status')`, etc. */
|
|
194
|
+
readonly provider: HocuspocusProvider;
|
|
195
|
+
/** The mutation bridge — replay-failure diagnostics live here. */
|
|
196
|
+
readonly bridge: BridgeHandle;
|
|
197
|
+
/** Last known connection status. */
|
|
198
|
+
status(): CollabConnectionStatus;
|
|
199
|
+
/** Detach: tear down bridge + provider + doc. Idempotent. */
|
|
200
|
+
detach(): void;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Attach real-time collab to a live editor. Call once after `onReady`.
|
|
204
|
+
* Returns a {@link CollabHandle}; call `.detach()` to leave the room
|
|
205
|
+
* (and always before the editor unmounts).
|
|
206
|
+
*/
|
|
207
|
+
declare function attachCollab(api: CollabAttachable, opts: AttachCollabOptions): CollabHandle;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Pure helpers extracted from bridge.ts so they're testable without
|
|
211
|
+
* pulling Univer / Yjs / Vite-only globals into a Node test runner.
|
|
212
|
+
*
|
|
213
|
+
* Anything stateless and Univer-free that bridge.ts needs should live
|
|
214
|
+
* here. Stateful bridge logic (Yjs observers, command service hooks,
|
|
215
|
+
* compaction) stays in bridge.ts itself.
|
|
216
|
+
*/
|
|
217
|
+
/**
|
|
218
|
+
* Substitute `localUnitId` for every `unitId` string property anywhere
|
|
219
|
+
* in `value` — including nested objects and arrays — and return a
|
|
220
|
+
* structurally cloned copy. Returns the input by reference when no
|
|
221
|
+
* change is needed, so callers can identity-compare before re-encoding.
|
|
222
|
+
*
|
|
223
|
+
* Walks plain objects and arrays only. Class instances are left alone
|
|
224
|
+
* because Univer mutation params are required to be JSON-friendly.
|
|
225
|
+
*/
|
|
226
|
+
/**
|
|
227
|
+
* Drawing mutations (sheets-drawing + drawing plugins) carry an
|
|
228
|
+
* `op` field that's a json1 patch — a positional array whose first
|
|
229
|
+
* element is the unitId. `deepRewriteUnitId` only rewrites the
|
|
230
|
+
* `unitId` KEY in objects; the json1 path is a bare array of strings
|
|
231
|
+
* so the unitId at position 0 stays as the OWNER's id and the apply
|
|
232
|
+
* fails on the joiner with no useful message ("Error" at
|
|
233
|
+
* json1.type.apply).
|
|
234
|
+
*
|
|
235
|
+
* This walks the op shape — single JSONOp (array of mixed strings +
|
|
236
|
+
* numbers + a final mutation component object) OR JSONOpList (array
|
|
237
|
+
* of JSONOps) — and substitutes the leading unitId where it matches.
|
|
238
|
+
* Returns a fresh structure on change so peers can identity-compare.
|
|
239
|
+
*
|
|
240
|
+
* Scope deliberately narrow: only checks element [0] of each op
|
|
241
|
+
* (where the unitId always lives in our path schema). A deeper
|
|
242
|
+
* path-rewrite would invent semantics the Univer source doesn't
|
|
243
|
+
* document.
|
|
244
|
+
*/
|
|
245
|
+
declare function rewriteJson1OpPathUnitId(op: unknown, oldUnitId: string, newUnitId: string): unknown;
|
|
246
|
+
declare function deepRewriteUnitId(value: unknown, localUnitId: string): unknown;
|
|
247
|
+
|
|
248
|
+
export { type AttachCollabOptions, type BridgeHandle, type BridgeOptions, type CollabAttachable, type CollabConnectionStatus, type CollabHandle, type CollabRole, REVERTABLE_MUTATIONS, type ReplayClassification, type ReplayFailureRecord, SYNCED_MUTATIONS, attachCollab, classifyReplayError, deepRewriteUnitId, rewriteJson1OpPathUnitId, startBridge };
|