@ai-matrx/office 0.8.23 → 0.8.25

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.
@@ -1,6 +1,6 @@
1
1
  import * as Y from 'yjs';
2
2
  import { Awareness } from 'y-protocols/awareness';
3
- import { OpSessionNotice, OpSession, OpTransport, OpPresence, OpSessionIncident, OpSessionStatus, AwarenessState, CollabRpcClient, collabReplace, OpDocumentAdapter } from '@ai-matrx/data/realtime/collab';
3
+ import { OpSessionNotice, OpSession, OpTransport, OpPresence, OpSessionIncident, OpSessionStatus, OpOfflineOptions, OpOfflineState, AwarenessState, CollabRpcClient, collabReplace, OpDocumentAdapter, CollabGroup } from '@ai-matrx/data/realtime/collab';
4
4
  import { IDocumentData } from '@univerjs/core';
5
5
  import { CommitReason } from '@ai-matrx/kit/working-copy';
6
6
 
@@ -362,6 +362,10 @@ interface OpenDocsOtRoomOptions {
362
362
  onNotice?: (n: DocsOtNotice) => void;
363
363
  onIncident?: (i: OpSessionIncident) => void;
364
364
  onStatus?: (s: OpSessionStatus) => void;
365
+ /** C10: unsent edits persist on this device under a lease (the host builds it: `browserOfflineOptions`). */
366
+ offline?: OpOfflineOptions | null;
367
+ /** C10: the chrome's offline state. */
368
+ onOffline?: (s: OpOfflineState) => void;
365
369
  /** Watch IME composition here (default: `document`). */
366
370
  compositionTarget?: Pick<Document, "addEventListener" | "removeEventListener"> | null;
367
371
  }
@@ -481,6 +485,8 @@ interface DocumentOtFactoryOptions {
481
485
  onNotice?: (n: DocsOtNotice) => void;
482
486
  onIncident?: (i: OpSessionIncident) => void;
483
487
  onStatus?: (s: OpSessionStatus) => void;
488
+ /** C10: the chrome's offline state (unsent edits are kept on this device, one tab per document and person owns them). */
489
+ onOffline?: (s: OpOfflineState) => void;
484
490
  }
485
491
  /** The `DocumentModel.startOtRoom` factory for one document. */
486
492
  declare function documentOtFactory(o: DocumentOtFactoryOptions): DocumentOtFactory;
@@ -599,6 +605,12 @@ interface SheetsOtPort {
599
605
  service: unknown;
600
606
  unitId: string;
601
607
  }>;
608
+ /**
609
+ * Run `fn` right before this seat takes an undo or redo (synchronously, before the command reads its stack). With
610
+ * it the room rebases the undo stack lazily: a remote op only queues, and an undo catches its stack up first.
611
+ * Without it every remote op rebases the whole stack at once.
612
+ */
613
+ beforeUndo?(fn: () => void): () => void;
602
614
  /** Ask for a save (the record's one coalesced save). */
603
615
  markDirty(): void;
604
616
  }
@@ -651,6 +663,8 @@ declare function rebaseSheetUndoStack(items: UndoItem[], remote: SheetOp, unitId
651
663
  interface SheetsOtAdapterHooks {
652
664
  /** Called once per applied remote op that rebased this seat's undo stacks (for tests and logs). */
653
665
  onUndoRebased?: (dropped: number) => void;
666
+ /** Called with the milliseconds of undo bookkeeping each remote op (and each catch-up) cost (tests, logs). */
667
+ onUndoWork?: (ms: number, kind: "remote" | "catch_up") => void;
654
668
  /**
655
669
  * The state hash of a snapshot (default `sheetsStateHash`, Web Crypto). A seeded test passes a synchronous one:
656
670
  * Web Crypto resolves on a worker thread, outside a virtual clock, so the same seed would not replay.
@@ -683,6 +697,10 @@ interface OpenSheetsOtRoomOptions {
683
697
  onNotice?: (n: SheetsOtNotice) => void;
684
698
  onIncident?: (i: OpSessionIncident) => void;
685
699
  onStatus?: (s: OpSessionStatus) => void;
700
+ /** C10: unsent edits persist on this device under a lease (the host builds it: `browserOfflineOptions`). */
701
+ offline?: OpOfflineOptions | null;
702
+ /** C10: the chrome's offline state. */
703
+ onOffline?: (s: OpOfflineState) => void;
686
704
  }
687
705
  interface SheetsOtRoom {
688
706
  session: OpSession<SheetOp>;
@@ -821,6 +839,15 @@ declare const canonicalSheetString: (snapshot: unknown) => string;
821
839
  /** SHA-256 (hex) of the canonical form. Web Crypto, so the browser and Node compute the same hash. */
822
840
  declare function sheetsStateHash(snapshot: unknown): Promise<string>;
823
841
 
842
+ /**
843
+ * "Copy my offline text" (C10): the words a person typed while offline, pulled out of the wire groups of a buffer
844
+ * the room refused (changed protocol, document trashed). Format-light on purpose: Docs inserts carry their text in
845
+ * `body.dataStream`; Sheets writes carry cell values in `cellValue`. Anything else contributes nothing, and the
846
+ * caller says so when nothing could be read (the record stays on the device regardless).
847
+ */
848
+
849
+ declare function offlineTextOfGroups(buffers: CollabGroup[][]): string;
850
+
824
851
  /** `collab.ot_rooms` (§12.2): which workbooks without a room open one. */
825
852
  type WorkbookOtRoomsKnob = "off" | "new" | "all";
826
853
  /** Whether this workbook opens on the ordered log: its room exists, or the knob opens one. */
@@ -836,6 +863,8 @@ interface WorkbookOtRoomOptions {
836
863
  onNotice?: (n: SheetsOtNotice) => void;
837
864
  onIncident?: (i: OpSessionIncident) => void;
838
865
  onStatus?: (s: OpSessionStatus) => void;
866
+ /** C10: the chrome's offline state (unsent edits are kept on this device, one tab per workbook and person owns them). */
867
+ onOffline?: (s: OpOfflineState) => void;
839
868
  }
840
869
  interface WorkbookOtRoomHandle {
841
870
  session: OpSession<SheetOp>;
@@ -857,4 +886,4 @@ declare function startWorkbookOtRoom(port: SheetsOtPort & {
857
886
  dispose?: () => void;
858
887
  }, o: WorkbookOtRoomOptions): Promise<WorkbookOtRoomHandle>;
859
888
 
860
- export { COLLAB_COVERED_KEY, type CollabBase, type CollabMutationInfo, type CollabProviderLike, type CommandServiceLike, DOCS_COLLAB_PROTO, DOCS_TRANSFORM_VERSION, DOC_LOCAL_KEYS, type DocsLocalMutation, type DocsLost, type DocsOp, type DocsOtNotice, type DocsOtPort, type DocsOtRoom, type DocsTransformResult, type DocumentOtFactoryOptions, type DocumentRestoreDeps, type DocumentRestoreResult, type OpenDocsOtRoomOptions, type OpenSheetsOtRoomOptions, type OtRoomsKnob, RICH_TEXT_EDITING, SENDER_VIEW_PARAMS, SHEETS_COLLAB_PROTO, SHEETS_EDIT_GROUP, type SheetOp, type SheetSelection, type SheetsCommandService, type SheetsLocalEvent, type SheetsLocalGroup, type SheetsNotShared, type SheetsOtAdapter, type SheetsOtAdapterHooks, type SheetsOtNotice, type SheetsOtPort, type SheetsOtRoom, type SheetsWorkbookLike, type SheetsWorksheetLike, type UniverSheetsOtPortOptions, WorkbookCollabSession, type WorkbookCollabSessionOptions, type WorkbookOtRoomHandle, type WorkbookOtRoomOptions, type WorkbookOtRoomsKnob, applyDocsOp, asLoadedDocsSnapshot, canonicalDocsSnapshot, canonicalDocsString, canonicalKeys, canonicalSheet, canonicalSheetString, composeDocsOps, createSheetsOtAdapter, decodeSheetOp, docsStateHash, documentOtFactory, documentUsesOrderedRoom, dropEmptyStyleRetains, encodeSheetOp, invertDocsOp, isCellEditorScratch, isNoopDocsOp, moveSelectionsThrough, normalizeDocsOp, openDocsOtRoom, openSheetsOtRoom, rebaseSheetUndoStack, rebaseUndoStack, restoreDocumentSnapshot, sheetsStateHash, stampCovered, stampDeterministicIds, startWorkbookOtRoom, stripSenderView, takeCovered, transformDocsOp, transformSheetOp, transformTextX, undoStacksOf, univerSheetsOtPort, workbookUsesOrderedRoom };
889
+ export { COLLAB_COVERED_KEY, type CollabBase, type CollabMutationInfo, type CollabProviderLike, type CommandServiceLike, DOCS_COLLAB_PROTO, DOCS_TRANSFORM_VERSION, DOC_LOCAL_KEYS, type DocsLocalMutation, type DocsLost, type DocsOp, type DocsOtNotice, type DocsOtPort, type DocsOtRoom, type DocsTransformResult, type DocumentOtFactoryOptions, type DocumentRestoreDeps, type DocumentRestoreResult, type OpenDocsOtRoomOptions, type OpenSheetsOtRoomOptions, type OtRoomsKnob, RICH_TEXT_EDITING, SENDER_VIEW_PARAMS, SHEETS_COLLAB_PROTO, SHEETS_EDIT_GROUP, type SheetOp, type SheetSelection, type SheetsCommandService, type SheetsLocalEvent, type SheetsLocalGroup, type SheetsNotShared, type SheetsOtAdapter, type SheetsOtAdapterHooks, type SheetsOtNotice, type SheetsOtPort, type SheetsOtRoom, type SheetsWorkbookLike, type SheetsWorksheetLike, type UniverSheetsOtPortOptions, WorkbookCollabSession, type WorkbookCollabSessionOptions, type WorkbookOtRoomHandle, type WorkbookOtRoomOptions, type WorkbookOtRoomsKnob, applyDocsOp, asLoadedDocsSnapshot, canonicalDocsSnapshot, canonicalDocsString, canonicalKeys, canonicalSheet, canonicalSheetString, composeDocsOps, createSheetsOtAdapter, decodeSheetOp, docsStateHash, documentOtFactory, documentUsesOrderedRoom, dropEmptyStyleRetains, encodeSheetOp, invertDocsOp, isCellEditorScratch, isNoopDocsOp, moveSelectionsThrough, normalizeDocsOp, offlineTextOfGroups, openDocsOtRoom, openSheetsOtRoom, rebaseSheetUndoStack, rebaseUndoStack, restoreDocumentSnapshot, sheetsStateHash, stampCovered, stampDeterministicIds, startWorkbookOtRoom, stripSenderView, takeCovered, transformDocsOp, transformSheetOp, transformTextX, undoStacksOf, univerSheetsOtPort, workbookUsesOrderedRoom };
@@ -1,6 +1,6 @@
1
1
  import * as Y from 'yjs';
2
2
  import { Awareness } from 'y-protocols/awareness';
3
- import { OpSessionNotice, OpSession, OpTransport, OpPresence, OpSessionIncident, OpSessionStatus, AwarenessState, CollabRpcClient, collabReplace, OpDocumentAdapter } from '@ai-matrx/data/realtime/collab';
3
+ import { OpSessionNotice, OpSession, OpTransport, OpPresence, OpSessionIncident, OpSessionStatus, OpOfflineOptions, OpOfflineState, AwarenessState, CollabRpcClient, collabReplace, OpDocumentAdapter, CollabGroup } from '@ai-matrx/data/realtime/collab';
4
4
  import { IDocumentData } from '@univerjs/core';
5
5
  import { CommitReason } from '@ai-matrx/kit/working-copy';
6
6
 
@@ -362,6 +362,10 @@ interface OpenDocsOtRoomOptions {
362
362
  onNotice?: (n: DocsOtNotice) => void;
363
363
  onIncident?: (i: OpSessionIncident) => void;
364
364
  onStatus?: (s: OpSessionStatus) => void;
365
+ /** C10: unsent edits persist on this device under a lease (the host builds it: `browserOfflineOptions`). */
366
+ offline?: OpOfflineOptions | null;
367
+ /** C10: the chrome's offline state. */
368
+ onOffline?: (s: OpOfflineState) => void;
365
369
  /** Watch IME composition here (default: `document`). */
366
370
  compositionTarget?: Pick<Document, "addEventListener" | "removeEventListener"> | null;
367
371
  }
@@ -481,6 +485,8 @@ interface DocumentOtFactoryOptions {
481
485
  onNotice?: (n: DocsOtNotice) => void;
482
486
  onIncident?: (i: OpSessionIncident) => void;
483
487
  onStatus?: (s: OpSessionStatus) => void;
488
+ /** C10: the chrome's offline state (unsent edits are kept on this device, one tab per document and person owns them). */
489
+ onOffline?: (s: OpOfflineState) => void;
484
490
  }
485
491
  /** The `DocumentModel.startOtRoom` factory for one document. */
486
492
  declare function documentOtFactory(o: DocumentOtFactoryOptions): DocumentOtFactory;
@@ -599,6 +605,12 @@ interface SheetsOtPort {
599
605
  service: unknown;
600
606
  unitId: string;
601
607
  }>;
608
+ /**
609
+ * Run `fn` right before this seat takes an undo or redo (synchronously, before the command reads its stack). With
610
+ * it the room rebases the undo stack lazily: a remote op only queues, and an undo catches its stack up first.
611
+ * Without it every remote op rebases the whole stack at once.
612
+ */
613
+ beforeUndo?(fn: () => void): () => void;
602
614
  /** Ask for a save (the record's one coalesced save). */
603
615
  markDirty(): void;
604
616
  }
@@ -651,6 +663,8 @@ declare function rebaseSheetUndoStack(items: UndoItem[], remote: SheetOp, unitId
651
663
  interface SheetsOtAdapterHooks {
652
664
  /** Called once per applied remote op that rebased this seat's undo stacks (for tests and logs). */
653
665
  onUndoRebased?: (dropped: number) => void;
666
+ /** Called with the milliseconds of undo bookkeeping each remote op (and each catch-up) cost (tests, logs). */
667
+ onUndoWork?: (ms: number, kind: "remote" | "catch_up") => void;
654
668
  /**
655
669
  * The state hash of a snapshot (default `sheetsStateHash`, Web Crypto). A seeded test passes a synchronous one:
656
670
  * Web Crypto resolves on a worker thread, outside a virtual clock, so the same seed would not replay.
@@ -683,6 +697,10 @@ interface OpenSheetsOtRoomOptions {
683
697
  onNotice?: (n: SheetsOtNotice) => void;
684
698
  onIncident?: (i: OpSessionIncident) => void;
685
699
  onStatus?: (s: OpSessionStatus) => void;
700
+ /** C10: unsent edits persist on this device under a lease (the host builds it: `browserOfflineOptions`). */
701
+ offline?: OpOfflineOptions | null;
702
+ /** C10: the chrome's offline state. */
703
+ onOffline?: (s: OpOfflineState) => void;
686
704
  }
687
705
  interface SheetsOtRoom {
688
706
  session: OpSession<SheetOp>;
@@ -821,6 +839,15 @@ declare const canonicalSheetString: (snapshot: unknown) => string;
821
839
  /** SHA-256 (hex) of the canonical form. Web Crypto, so the browser and Node compute the same hash. */
822
840
  declare function sheetsStateHash(snapshot: unknown): Promise<string>;
823
841
 
842
+ /**
843
+ * "Copy my offline text" (C10): the words a person typed while offline, pulled out of the wire groups of a buffer
844
+ * the room refused (changed protocol, document trashed). Format-light on purpose: Docs inserts carry their text in
845
+ * `body.dataStream`; Sheets writes carry cell values in `cellValue`. Anything else contributes nothing, and the
846
+ * caller says so when nothing could be read (the record stays on the device regardless).
847
+ */
848
+
849
+ declare function offlineTextOfGroups(buffers: CollabGroup[][]): string;
850
+
824
851
  /** `collab.ot_rooms` (§12.2): which workbooks without a room open one. */
825
852
  type WorkbookOtRoomsKnob = "off" | "new" | "all";
826
853
  /** Whether this workbook opens on the ordered log: its room exists, or the knob opens one. */
@@ -836,6 +863,8 @@ interface WorkbookOtRoomOptions {
836
863
  onNotice?: (n: SheetsOtNotice) => void;
837
864
  onIncident?: (i: OpSessionIncident) => void;
838
865
  onStatus?: (s: OpSessionStatus) => void;
866
+ /** C10: the chrome's offline state (unsent edits are kept on this device, one tab per workbook and person owns them). */
867
+ onOffline?: (s: OpOfflineState) => void;
839
868
  }
840
869
  interface WorkbookOtRoomHandle {
841
870
  session: OpSession<SheetOp>;
@@ -857,4 +886,4 @@ declare function startWorkbookOtRoom(port: SheetsOtPort & {
857
886
  dispose?: () => void;
858
887
  }, o: WorkbookOtRoomOptions): Promise<WorkbookOtRoomHandle>;
859
888
 
860
- export { COLLAB_COVERED_KEY, type CollabBase, type CollabMutationInfo, type CollabProviderLike, type CommandServiceLike, DOCS_COLLAB_PROTO, DOCS_TRANSFORM_VERSION, DOC_LOCAL_KEYS, type DocsLocalMutation, type DocsLost, type DocsOp, type DocsOtNotice, type DocsOtPort, type DocsOtRoom, type DocsTransformResult, type DocumentOtFactoryOptions, type DocumentRestoreDeps, type DocumentRestoreResult, type OpenDocsOtRoomOptions, type OpenSheetsOtRoomOptions, type OtRoomsKnob, RICH_TEXT_EDITING, SENDER_VIEW_PARAMS, SHEETS_COLLAB_PROTO, SHEETS_EDIT_GROUP, type SheetOp, type SheetSelection, type SheetsCommandService, type SheetsLocalEvent, type SheetsLocalGroup, type SheetsNotShared, type SheetsOtAdapter, type SheetsOtAdapterHooks, type SheetsOtNotice, type SheetsOtPort, type SheetsOtRoom, type SheetsWorkbookLike, type SheetsWorksheetLike, type UniverSheetsOtPortOptions, WorkbookCollabSession, type WorkbookCollabSessionOptions, type WorkbookOtRoomHandle, type WorkbookOtRoomOptions, type WorkbookOtRoomsKnob, applyDocsOp, asLoadedDocsSnapshot, canonicalDocsSnapshot, canonicalDocsString, canonicalKeys, canonicalSheet, canonicalSheetString, composeDocsOps, createSheetsOtAdapter, decodeSheetOp, docsStateHash, documentOtFactory, documentUsesOrderedRoom, dropEmptyStyleRetains, encodeSheetOp, invertDocsOp, isCellEditorScratch, isNoopDocsOp, moveSelectionsThrough, normalizeDocsOp, openDocsOtRoom, openSheetsOtRoom, rebaseSheetUndoStack, rebaseUndoStack, restoreDocumentSnapshot, sheetsStateHash, stampCovered, stampDeterministicIds, startWorkbookOtRoom, stripSenderView, takeCovered, transformDocsOp, transformSheetOp, transformTextX, undoStacksOf, univerSheetsOtPort, workbookUsesOrderedRoom };
889
+ export { COLLAB_COVERED_KEY, type CollabBase, type CollabMutationInfo, type CollabProviderLike, type CommandServiceLike, DOCS_COLLAB_PROTO, DOCS_TRANSFORM_VERSION, DOC_LOCAL_KEYS, type DocsLocalMutation, type DocsLost, type DocsOp, type DocsOtNotice, type DocsOtPort, type DocsOtRoom, type DocsTransformResult, type DocumentOtFactoryOptions, type DocumentRestoreDeps, type DocumentRestoreResult, type OpenDocsOtRoomOptions, type OpenSheetsOtRoomOptions, type OtRoomsKnob, RICH_TEXT_EDITING, SENDER_VIEW_PARAMS, SHEETS_COLLAB_PROTO, SHEETS_EDIT_GROUP, type SheetOp, type SheetSelection, type SheetsCommandService, type SheetsLocalEvent, type SheetsLocalGroup, type SheetsNotShared, type SheetsOtAdapter, type SheetsOtAdapterHooks, type SheetsOtNotice, type SheetsOtPort, type SheetsOtRoom, type SheetsWorkbookLike, type SheetsWorksheetLike, type UniverSheetsOtPortOptions, WorkbookCollabSession, type WorkbookCollabSessionOptions, type WorkbookOtRoomHandle, type WorkbookOtRoomOptions, type WorkbookOtRoomsKnob, applyDocsOp, asLoadedDocsSnapshot, canonicalDocsSnapshot, canonicalDocsString, canonicalKeys, canonicalSheet, canonicalSheetString, composeDocsOps, createSheetsOtAdapter, decodeSheetOp, docsStateHash, documentOtFactory, documentUsesOrderedRoom, dropEmptyStyleRetains, encodeSheetOp, invertDocsOp, isCellEditorScratch, isNoopDocsOp, moveSelectionsThrough, normalizeDocsOp, offlineTextOfGroups, openDocsOtRoom, openSheetsOtRoom, rebaseSheetUndoStack, rebaseUndoStack, restoreDocumentSnapshot, sheetsStateHash, stampCovered, stampDeterministicIds, startWorkbookOtRoom, stripSenderView, takeCovered, transformDocsOp, transformSheetOp, transformTextX, undoStacksOf, univerSheetsOtPort, workbookUsesOrderedRoom };
@@ -1290,7 +1290,9 @@ function openDocsOtRoom(o) {
1290
1290
  ...o.presence ? { presence: o.presence } : {},
1291
1291
  onNotice: (n) => notice(n),
1292
1292
  ...o.onIncident ? { onIncident: o.onIncident } : {},
1293
- ...o.onStatus ? { onStatus: o.onStatus } : {}
1293
+ ...o.onStatus ? { onStatus: o.onStatus } : {},
1294
+ ...o.offline ? { offline: o.offline } : {},
1295
+ ...o.onOffline ? { onOffline: o.onOffline } : {}
1294
1296
  });
1295
1297
  const isSaver = () => {
1296
1298
  if (!o.presence) return true;
@@ -1328,6 +1330,7 @@ import * as Y2 from "yjs";
1328
1330
  import { Awareness as Awareness2 } from "y-protocols/awareness";
1329
1331
  import {
1330
1332
  awarenessOpPresence,
1333
+ browserOfflineOptions,
1331
1334
  claimCollaboratorColor as claimCollaboratorColor2,
1332
1335
  collabReplace,
1333
1336
  collabRoomExists,
@@ -1565,7 +1568,9 @@ function documentOtFactory(o) {
1565
1568
  replace: ({ snapshot, stateHash, clientId }) => collabReplace(client, { roomId: o.documentId, snapshot, stateHash, origin: "restore", clientId }),
1566
1569
  ...o.onNotice ? { onNotice: o.onNotice } : {},
1567
1570
  ...o.onIncident ? { onIncident: o.onIncident } : {},
1568
- ...o.onStatus ? { onStatus: o.onStatus } : {}
1571
+ ...o.onStatus ? { onStatus: o.onStatus } : {},
1572
+ offline: browserOfflineOptions(`${o.documentId}:${o.uid}`),
1573
+ ...o.onOffline ? { onOffline: o.onOffline } : {}
1569
1574
  });
1570
1575
  await room.session.ready;
1571
1576
  let lastKey = "";
@@ -3495,17 +3500,65 @@ function rebaseSheetUndoStack(items, remote, unitId) {
3495
3500
  function createSheetsOtAdapter(port, hooks = {}) {
3496
3501
  let queue = [];
3497
3502
  let draining = false;
3503
+ const lazy = typeof port.beforeUndo === "function";
3504
+ let pending = [];
3505
+ let pendingBase = 0;
3506
+ let applied = 0;
3507
+ let currentAt = /* @__PURE__ */ new WeakMap();
3508
+ const PENDING_CAP = 2e3;
3509
+ const now = () => typeof performance !== "undefined" ? performance.now() : Date.now();
3510
+ const stacksOfTargets = () => port.undoTargets().flatMap((t) => {
3511
+ const stacks = undoStacksOf2(t.service);
3512
+ return stacks ? [stacks.undo, stacks.redo].map((map) => ({ items: map.get(t.unitId), unitId: t.unitId, isUndo: map === stacks.undo })) : [];
3513
+ });
3514
+ const markNew = (items) => {
3515
+ for (let i = items.length - 1; i >= 0 && !currentAt.has(items[i]); i--) currentAt.set(items[i], applied);
3516
+ };
3517
+ const catchUpItem = (item, unitId) => {
3518
+ const from = currentAt.get(item) ?? applied;
3519
+ for (let k = from - pendingBase; k < pending.length; k++) if (rebaseSheetUndoStack([item], pending[k], unitId) > 0) return false;
3520
+ currentAt.set(item, applied);
3521
+ return true;
3522
+ };
3523
+ const catchUp = () => {
3524
+ if (!pending.length) return;
3525
+ const t0 = now();
3526
+ let dropped = 0;
3527
+ for (const { items, unitId } of stacksOfTargets()) {
3528
+ if (!items?.length) continue;
3529
+ for (let i = items.length - 1; i >= 0; i--)
3530
+ if (!catchUpItem(items[i], unitId)) {
3531
+ items.splice(i, 1);
3532
+ dropped++;
3533
+ }
3534
+ }
3535
+ pending = [];
3536
+ pendingBase = applied;
3537
+ hooks.onUndoWork?.(now() - t0, "catch_up");
3538
+ if (dropped) hooks.onUndoRebased?.(dropped);
3539
+ };
3540
+ if (lazy) port.beforeUndo(catchUp);
3498
3541
  const applyNow = (op) => {
3499
3542
  for (const m of op) port.applyRemote(m);
3543
+ const t0 = now();
3500
3544
  let dropped = 0;
3501
- for (const t of port.undoTargets()) {
3502
- const stacks = undoStacksOf2(t.service);
3503
- if (!stacks) continue;
3504
- for (const map of [stacks.undo, stacks.redo]) {
3505
- const items = map.get(t.unitId);
3506
- if (items?.length) dropped += rebaseSheetUndoStack(items, op, t.unitId);
3545
+ if (!lazy) {
3546
+ for (const { items, unitId } of stacksOfTargets()) if (items?.length) dropped += rebaseSheetUndoStack(items, op, unitId);
3547
+ } else if (!isNoopSheetOp(op)) {
3548
+ for (const { items, unitId, isUndo } of stacksOfTargets()) {
3549
+ if (!items?.length) continue;
3550
+ markNew(items);
3551
+ const top = items[items.length - 1];
3552
+ if (isUndo && (!catchUpItem(top, unitId) || rebaseSheetUndoStack([top], op, unitId) > 0)) {
3553
+ items.pop();
3554
+ dropped++;
3555
+ } else if (isUndo) currentAt.set(top, applied + 1);
3507
3556
  }
3557
+ pending.push(op);
3558
+ applied++;
3559
+ if (pending.length > PENDING_CAP) catchUp();
3508
3560
  }
3561
+ hooks.onUndoWork?.(now() - t0, "remote");
3509
3562
  hooks.onUndoRebased?.(dropped);
3510
3563
  port.markDirty();
3511
3564
  };
@@ -3543,6 +3596,9 @@ function createSheetsOtAdapter(port, hooks = {}) {
3543
3596
  stacks?.undo.delete(t.unitId);
3544
3597
  stacks?.redo.delete(t.unitId);
3545
3598
  }
3599
+ pending = [];
3600
+ pendingBase = applied;
3601
+ currentAt = /* @__PURE__ */ new WeakMap();
3546
3602
  },
3547
3603
  snapshot: () => port.snapshot(),
3548
3604
  async hash() {
@@ -3629,7 +3685,9 @@ function openSheetsOtRoom(o) {
3629
3685
  ...o.presence ? { presence: o.presence } : {},
3630
3686
  onNotice: (n) => notice(n),
3631
3687
  ...o.onIncident ? { onIncident: o.onIncident } : {},
3632
- ...o.onStatus ? { onStatus: o.onStatus } : {}
3688
+ ...o.onStatus ? { onStatus: o.onStatus } : {},
3689
+ ...o.offline ? { offline: o.offline } : {},
3690
+ ...o.onOffline ? { onOffline: o.onOffline } : {}
3633
3691
  });
3634
3692
  const isSaver = () => {
3635
3693
  if (!o.presence) return true;
@@ -3660,6 +3718,65 @@ function openSheetsOtRoom(o) {
3660
3718
  };
3661
3719
  }
3662
3720
 
3721
+ // src/univer/collab/sheets-ot/plugin-teardown.ts
3722
+ var perSheetList = (data) => Object.entries(data).flatMap(([sheet, list]) => Array.isArray(list) ? [[sheet, list.filter((x) => !!x && typeof x === "object")]] : []);
3723
+ var PLUGIN_TEARDOWNS = [
3724
+ {
3725
+ resource: "SHEET_CONDITIONAL_FORMATTING_PLUGIN",
3726
+ removals: (unitId, data) => perSheetList(data).flatMap(
3727
+ ([subUnitId, rules]) => rules.filter((r) => typeof r.cfId === "string").map((r) => ({ id: "sheet.mutation.delete-conditional-rule", params: { unitId, subUnitId, cfId: r.cfId } }))
3728
+ )
3729
+ },
3730
+ {
3731
+ resource: "SHEET_DATA_VALIDATION_PLUGIN",
3732
+ removals: (unitId, data) => perSheetList(data).flatMap(([subUnitId, rules]) => {
3733
+ const ids = rules.map((r) => r.uid).filter((u) => typeof u === "string");
3734
+ return ids.length ? [{ id: "data-validation.mutation.removeRule", params: { unitId, subUnitId, ruleId: ids, source: "command" } }] : [];
3735
+ })
3736
+ },
3737
+ {
3738
+ resource: "SHEET_NOTE_PLUGIN",
3739
+ removals: (unitId, data) => Object.entries(data).flatMap(
3740
+ ([sheetId, rows]) => Object.entries(rows ?? {}).flatMap(
3741
+ ([row, cols]) => Object.entries(cols ?? {}).map(([col, note]) => ({
3742
+ id: "sheet.mutation.remove-note",
3743
+ params: { unitId, sheetId, row: Number(row), col: Number(col), ...note && typeof note.id === "string" ? { noteId: note.id } : {}, silent: true }
3744
+ }))
3745
+ )
3746
+ )
3747
+ }
3748
+ ];
3749
+ function resourcesOf(saved) {
3750
+ const out = /* @__PURE__ */ new Map();
3751
+ const list = saved?.resources;
3752
+ for (const r of Array.isArray(list) ? list : []) {
3753
+ if (!r?.name) continue;
3754
+ try {
3755
+ const data = typeof r.data === "string" ? r.data ? JSON.parse(r.data) : null : r.data;
3756
+ if (data && typeof data === "object") out.set(r.name, data);
3757
+ } catch {
3758
+ }
3759
+ }
3760
+ return out;
3761
+ }
3762
+ function releaseUnitPluginState(cs, unitId, saved) {
3763
+ const resources = resourcesOf(saved);
3764
+ let ran = 0;
3765
+ for (const t of PLUGIN_TEARDOWNS) {
3766
+ const data = resources.get(t.resource);
3767
+ if (!data) continue;
3768
+ for (const m of t.removals(unitId, data)) {
3769
+ try {
3770
+ cs.syncExecuteCommand(m.id, m.params, { onlyLocal: true });
3771
+ ran++;
3772
+ } catch (err) {
3773
+ console.warn(`[sheets-ot] could not release ${t.resource} state before a reload`, err);
3774
+ }
3775
+ }
3776
+ }
3777
+ return ran;
3778
+ }
3779
+
3663
3780
  // src/univer/collab/sheets-ot/univer-port.ts
3664
3781
  var COMMAND = 0;
3665
3782
  var MUTATION = 2;
@@ -3669,6 +3786,8 @@ var REMOVE_MERGE = "sheet.mutation.remove-worksheet-merge";
3669
3786
  var SHEET_NAME = "sheet.mutation.set-worksheet-name";
3670
3787
  var INSERT_SHEET = "sheet.mutation.insert-sheet";
3671
3788
  var RESHAPE = /* @__PURE__ */ new Set(["sheet.mutation.set-worksheet-row-count", "sheet.mutation.set-worksheet-column-count", "sheet.mutation.remove-sheet", INSERT_SHEET]);
3789
+ var UNDO = "univer.command.undo";
3790
+ var REDO = "univer.command.redo";
3672
3791
  var DEPTH_GUARD = /* @__PURE__ */ Symbol.for("ai-matrx.office.sheets.command-depth-guard");
3673
3792
  var json = (v) => v === void 0 ? v : JSON.parse(JSON.stringify(v));
3674
3793
  var flagged = (options) => {
@@ -3678,6 +3797,7 @@ var flagged = (options) => {
3678
3797
  function univerSheetsOtPort(o) {
3679
3798
  const cs = o.commandService;
3680
3799
  const listeners = /* @__PURE__ */ new Set();
3800
+ const undoWatchers = /* @__PURE__ */ new Set();
3681
3801
  const idle = [];
3682
3802
  const pre = /* @__PURE__ */ new WeakMap();
3683
3803
  const tracker = { stack: [], settle: () => settle() };
@@ -3734,6 +3854,7 @@ function univerSheetsOtPort(o) {
3734
3854
  if (tracker.stack.length === 0 && !group) for (const fn of idle.splice(0)) fn();
3735
3855
  }
3736
3856
  const before = cs.beforeCommandExecuted((info, options) => {
3857
+ if (info.id === UNDO || info.id === REDO) for (const fn of [...undoWatchers]) fn();
3737
3858
  if (info.type === COMMAND) {
3738
3859
  if (tracker.stack.length === 0 && !group) group = openGroup(info.id);
3739
3860
  tracker.stack.push(info.id);
@@ -3979,6 +4100,8 @@ function univerSheetsOtPort(o) {
3979
4100
  group = null;
3980
4101
  applying = true;
3981
4102
  try {
4103
+ const u = unitId();
4104
+ if (u) releaseUnitPluginState(cs, u, o.saveUnit?.(u) ?? null);
3982
4105
  o.load(snapshot);
3983
4106
  } finally {
3984
4107
  applying = false;
@@ -3989,8 +4112,13 @@ function univerSheetsOtPort(o) {
3989
4112
  const service = o.undoRedo?.();
3990
4113
  return u && service ? [{ service, unitId: u }] : [];
3991
4114
  },
4115
+ beforeUndo(fn) {
4116
+ undoWatchers.add(fn);
4117
+ return () => void undoWatchers.delete(fn);
4118
+ },
3992
4119
  markDirty: () => o.markDirty(),
3993
4120
  dispose() {
4121
+ undoWatchers.clear();
3994
4122
  before.dispose();
3995
4123
  after.dispose();
3996
4124
  collab.dispose();
@@ -4068,11 +4196,59 @@ function moveSelectionsThrough(selections, m, maxRows, maxCols) {
4068
4196
  return selections.map((s) => ({ ...s, range: box(s.range, s.range.rangeType), ...s.primary ? { primary: box(s.primary) } : {} }));
4069
4197
  }
4070
4198
 
4199
+ // src/univer/collab/offline-text.ts
4200
+ var colName3 = (c) => {
4201
+ let n = c;
4202
+ let s = "";
4203
+ do {
4204
+ s = String.fromCharCode(65 + n % 26) + s;
4205
+ n = Math.floor(n / 26) - 1;
4206
+ } while (n >= 0);
4207
+ return s;
4208
+ };
4209
+ function collectDocText(v, out) {
4210
+ if (Array.isArray(v)) {
4211
+ for (const x of v) collectDocText(x, out);
4212
+ return;
4213
+ }
4214
+ if (!v || typeof v !== "object") return;
4215
+ const o = v;
4216
+ if (typeof o.dataStream === "string" && o.dataStream.length) out.push(o.dataStream);
4217
+ for (const k of Object.keys(o)) if (k !== "dataStream") collectDocText(o[k], out);
4218
+ }
4219
+ function collectCells(cellValue, out) {
4220
+ if (!cellValue || typeof cellValue !== "object") return;
4221
+ for (const [r, cols] of Object.entries(cellValue)) {
4222
+ if (!cols || typeof cols !== "object") continue;
4223
+ for (const [c, cell] of Object.entries(cols)) {
4224
+ const x = cell;
4225
+ const val = x?.f ?? x?.v;
4226
+ if (val !== void 0 && val !== null && val !== "") out.push(`${colName3(Number(c))}${Number(r) + 1}: ${String(val)}`);
4227
+ }
4228
+ }
4229
+ }
4230
+ function offlineTextOfGroups(buffers) {
4231
+ const docs = [];
4232
+ const cells = [];
4233
+ for (const groups of buffers) {
4234
+ for (const g of groups) {
4235
+ for (const m of g.mutations) {
4236
+ const p = m.params;
4237
+ if (p && "cellValue" in p) collectCells(p.cellValue, cells);
4238
+ else collectDocText(p, docs);
4239
+ }
4240
+ }
4241
+ }
4242
+ const text = docs.join("").replace(/\r/g, "\n").replace(/\u0000/g, "");
4243
+ return [text, cells.join("\n")].filter((s) => s.length > 0).join("\n");
4244
+ }
4245
+
4071
4246
  // src/sheets/workbook-ot-room.ts
4072
4247
  import * as Y3 from "yjs";
4073
4248
  import { Awareness as Awareness3 } from "y-protocols/awareness";
4074
4249
  import {
4075
4250
  awarenessOpPresence as awarenessOpPresence2,
4251
+ browserOfflineOptions as browserOfflineOptions2,
4076
4252
  claimCollaboratorColor as claimCollaboratorColor3,
4077
4253
  collabReplace as collabReplace3,
4078
4254
  collabRoomExists as collabRoomExists3,
@@ -4177,6 +4353,7 @@ async function startWorkbookOtRoom(port, o) {
4177
4353
  await provider.connect();
4178
4354
  publishAwareness();
4179
4355
  const client = db4();
4356
+ const offline = browserOfflineOptions2(`${o.workbookId}:${o.uid}`);
4180
4357
  const room = openSheetsOtRoom({
4181
4358
  port,
4182
4359
  clientId: o.clientId,
@@ -4197,7 +4374,9 @@ async function startWorkbookOtRoom(port, o) {
4197
4374
  replace: ({ snapshot, stateHash, clientId }) => collabReplace3(client, { roomId: o.workbookId, snapshot, stateHash, origin: "restore", clientId }),
4198
4375
  ...o.onNotice ? { onNotice: o.onNotice } : {},
4199
4376
  ...o.onIncident ? { onIncident: o.onIncident } : {},
4200
- ...o.onStatus ? { onStatus: o.onStatus } : {}
4377
+ ...o.onStatus ? { onStatus: o.onStatus } : {},
4378
+ ...offline ? { offline } : {},
4379
+ ...o.onOffline ? { onOffline: o.onOffline } : {}
4201
4380
  });
4202
4381
  await room.session.ready;
4203
4382
  return {
@@ -4250,6 +4429,7 @@ export {
4250
4429
  isNoopDocsOp,
4251
4430
  moveSelectionsThrough,
4252
4431
  normalizeDocsOp,
4432
+ offlineTextOfGroups,
4253
4433
  openDocsOtRoom,
4254
4434
  openSheetsOtRoom,
4255
4435
  rebaseSheetUndoStack,