@ai-matrx/office 0.8.12 → 0.8.13

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 } from '@ai-matrx/data/realtime/collab';
3
+ import { OpSessionNotice, OpSession, OpTransport, OpPresence, OpSessionIncident, OpSessionStatus, AwarenessState, CollabRpcClient, collabReplace } 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
 
@@ -336,6 +336,12 @@ interface DocsOtPort {
336
336
  }>;
337
337
  /** Ask for a save (the record's one coalesced save). */
338
338
  markDirty(): void;
339
+ /** This seat's text selection in the first view (body offsets), for presence (C8). Absent when no view can say. */
340
+ selection?(): Array<{
341
+ startOffset: number;
342
+ endOffset: number;
343
+ segmentId?: string;
344
+ }> | null;
339
345
  }
340
346
  type DocsOtNotice = OpSessionNotice<DocsOp> | {
341
347
  kind: "not_shared";
@@ -399,6 +405,20 @@ declare function undoStacksOf(service: unknown): {
399
405
  declare function rebaseUndoStack(items: UndoItem[], remote: DocsOp): number;
400
406
  declare function openDocsOtRoom(o: OpenDocsOtRoomOptions): DocsOtRoom;
401
407
 
408
+ interface CaretRange {
409
+ start: number;
410
+ end: number;
411
+ }
412
+ /** One other seat's caret, ready to draw. */
413
+ interface RemoteCaret {
414
+ /** The seat (Yjs client id as a string): two tabs of one person are two carets. */
415
+ key: string;
416
+ uid: string;
417
+ name: string;
418
+ color: string;
419
+ ranges: CaretRange[];
420
+ }
421
+
402
422
  /**
403
423
  * THE DOCUMENT MODEL — the engine of one cloud document per tab.
404
424
  *
@@ -447,6 +467,8 @@ type DocumentOtFactory = (port: DocsOtPort, onAwareness: (awareness: DocumentAwa
447
467
  interface DocumentAwareness {
448
468
  states: Map<number, AwarenessState>;
449
469
  selfUid: string;
470
+ /** Other seats' carets and selections (ordered-log rooms only, C8), already moved through every edit applied since. */
471
+ carets?: RemoteCaret[];
450
472
  }
451
473
 
452
474
  /** `collab.ot_rooms` (§12.2): which documents without a room open one. */
@@ -467,4 +489,51 @@ interface DocumentOtFactoryOptions {
467
489
  /** The `DocumentModel.startOtRoom` factory for one document. */
468
490
  declare function documentOtFactory(o: DocumentOtFactoryOptions): DocumentOtFactory;
469
491
 
470
- 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 OpenDocsOtRoomOptions, type OtRoomsKnob, RICH_TEXT_EDITING, SENDER_VIEW_PARAMS, WorkbookCollabSession, type WorkbookCollabSessionOptions, applyDocsOp, canonicalDocsSnapshot, canonicalDocsString, canonicalKeys, composeDocsOps, docsStateHash, documentOtFactory, documentUsesOrderedRoom, dropEmptyStyleRetains, invertDocsOp, isCellEditorScratch, isNoopDocsOp, normalizeDocsOp, openDocsOtRoom, rebaseUndoStack, stampCovered, stampDeterministicIds, stripSenderView, takeCovered, transformDocsOp, transformTextX, undoStacksOf };
492
+ /**
493
+ * Restore a saved version of a document (COLLAB-DESIGN §6.2, §20): THE one path for History, so a document with a
494
+ * co-editing room is never restored behind its room's back.
495
+ *
496
+ * - Open in this tab: save the restored row, then the open editor swaps it in (`showSavedSnapshot`); an ordered-log
497
+ * room sends it through `collab_replace` itself (`DocumentOtHandle.replace`).
498
+ * - Not open in this tab but the document has a room (another tab or person has it open, or its log is ahead of its
499
+ * checkpoint): `collab_replace` writes the snapshot row, the `replace` op, the checkpoint and the frame in one
500
+ * transaction, so every open seat reloads. A snapshot row alone left those seats stale.
501
+ * - No room: a snapshot row; the next open loads it.
502
+ */
503
+
504
+ type DocumentRestoreResult = {
505
+ via: "open_editor";
506
+ snapshotId: string;
507
+ } | {
508
+ via: "room";
509
+ rev: number;
510
+ snapshotId: string;
511
+ } | {
512
+ via: "saved";
513
+ snapshotId: string;
514
+ };
515
+ interface DocumentRestoreDeps {
516
+ client: () => CollabRpcClient;
517
+ roomExists: (client: CollabRpcClient, documentId: string) => Promise<boolean>;
518
+ replace: typeof collabReplace;
519
+ stateHash: (snapshot: unknown) => Promise<string>;
520
+ hasOpenEditor: (documentId: string) => boolean;
521
+ showInOpenEditor: (documentId: string, row: {
522
+ id: string;
523
+ snapshot: unknown;
524
+ }) => Promise<boolean>;
525
+ saveRow: (args: {
526
+ documentId: string;
527
+ snapshot: unknown;
528
+ label: string | null;
529
+ }) => Promise<{
530
+ id: string;
531
+ }>;
532
+ }
533
+ declare function restoreDocumentSnapshot(args: {
534
+ documentId: string;
535
+ snapshot: unknown;
536
+ label: string | null;
537
+ }, deps?: Partial<DocumentRestoreDeps>): Promise<DocumentRestoreResult>;
538
+
539
+ 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 OtRoomsKnob, RICH_TEXT_EDITING, SENDER_VIEW_PARAMS, WorkbookCollabSession, type WorkbookCollabSessionOptions, applyDocsOp, canonicalDocsSnapshot, canonicalDocsString, canonicalKeys, composeDocsOps, docsStateHash, documentOtFactory, documentUsesOrderedRoom, dropEmptyStyleRetains, invertDocsOp, isCellEditorScratch, isNoopDocsOp, normalizeDocsOp, openDocsOtRoom, rebaseUndoStack, restoreDocumentSnapshot, stampCovered, stampDeterministicIds, stripSenderView, takeCovered, transformDocsOp, transformTextX, undoStacksOf };
@@ -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 } from '@ai-matrx/data/realtime/collab';
3
+ import { OpSessionNotice, OpSession, OpTransport, OpPresence, OpSessionIncident, OpSessionStatus, AwarenessState, CollabRpcClient, collabReplace } 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
 
@@ -336,6 +336,12 @@ interface DocsOtPort {
336
336
  }>;
337
337
  /** Ask for a save (the record's one coalesced save). */
338
338
  markDirty(): void;
339
+ /** This seat's text selection in the first view (body offsets), for presence (C8). Absent when no view can say. */
340
+ selection?(): Array<{
341
+ startOffset: number;
342
+ endOffset: number;
343
+ segmentId?: string;
344
+ }> | null;
339
345
  }
340
346
  type DocsOtNotice = OpSessionNotice<DocsOp> | {
341
347
  kind: "not_shared";
@@ -399,6 +405,20 @@ declare function undoStacksOf(service: unknown): {
399
405
  declare function rebaseUndoStack(items: UndoItem[], remote: DocsOp): number;
400
406
  declare function openDocsOtRoom(o: OpenDocsOtRoomOptions): DocsOtRoom;
401
407
 
408
+ interface CaretRange {
409
+ start: number;
410
+ end: number;
411
+ }
412
+ /** One other seat's caret, ready to draw. */
413
+ interface RemoteCaret {
414
+ /** The seat (Yjs client id as a string): two tabs of one person are two carets. */
415
+ key: string;
416
+ uid: string;
417
+ name: string;
418
+ color: string;
419
+ ranges: CaretRange[];
420
+ }
421
+
402
422
  /**
403
423
  * THE DOCUMENT MODEL — the engine of one cloud document per tab.
404
424
  *
@@ -447,6 +467,8 @@ type DocumentOtFactory = (port: DocsOtPort, onAwareness: (awareness: DocumentAwa
447
467
  interface DocumentAwareness {
448
468
  states: Map<number, AwarenessState>;
449
469
  selfUid: string;
470
+ /** Other seats' carets and selections (ordered-log rooms only, C8), already moved through every edit applied since. */
471
+ carets?: RemoteCaret[];
450
472
  }
451
473
 
452
474
  /** `collab.ot_rooms` (§12.2): which documents without a room open one. */
@@ -467,4 +489,51 @@ interface DocumentOtFactoryOptions {
467
489
  /** The `DocumentModel.startOtRoom` factory for one document. */
468
490
  declare function documentOtFactory(o: DocumentOtFactoryOptions): DocumentOtFactory;
469
491
 
470
- 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 OpenDocsOtRoomOptions, type OtRoomsKnob, RICH_TEXT_EDITING, SENDER_VIEW_PARAMS, WorkbookCollabSession, type WorkbookCollabSessionOptions, applyDocsOp, canonicalDocsSnapshot, canonicalDocsString, canonicalKeys, composeDocsOps, docsStateHash, documentOtFactory, documentUsesOrderedRoom, dropEmptyStyleRetains, invertDocsOp, isCellEditorScratch, isNoopDocsOp, normalizeDocsOp, openDocsOtRoom, rebaseUndoStack, stampCovered, stampDeterministicIds, stripSenderView, takeCovered, transformDocsOp, transformTextX, undoStacksOf };
492
+ /**
493
+ * Restore a saved version of a document (COLLAB-DESIGN §6.2, §20): THE one path for History, so a document with a
494
+ * co-editing room is never restored behind its room's back.
495
+ *
496
+ * - Open in this tab: save the restored row, then the open editor swaps it in (`showSavedSnapshot`); an ordered-log
497
+ * room sends it through `collab_replace` itself (`DocumentOtHandle.replace`).
498
+ * - Not open in this tab but the document has a room (another tab or person has it open, or its log is ahead of its
499
+ * checkpoint): `collab_replace` writes the snapshot row, the `replace` op, the checkpoint and the frame in one
500
+ * transaction, so every open seat reloads. A snapshot row alone left those seats stale.
501
+ * - No room: a snapshot row; the next open loads it.
502
+ */
503
+
504
+ type DocumentRestoreResult = {
505
+ via: "open_editor";
506
+ snapshotId: string;
507
+ } | {
508
+ via: "room";
509
+ rev: number;
510
+ snapshotId: string;
511
+ } | {
512
+ via: "saved";
513
+ snapshotId: string;
514
+ };
515
+ interface DocumentRestoreDeps {
516
+ client: () => CollabRpcClient;
517
+ roomExists: (client: CollabRpcClient, documentId: string) => Promise<boolean>;
518
+ replace: typeof collabReplace;
519
+ stateHash: (snapshot: unknown) => Promise<string>;
520
+ hasOpenEditor: (documentId: string) => boolean;
521
+ showInOpenEditor: (documentId: string, row: {
522
+ id: string;
523
+ snapshot: unknown;
524
+ }) => Promise<boolean>;
525
+ saveRow: (args: {
526
+ documentId: string;
527
+ snapshot: unknown;
528
+ label: string | null;
529
+ }) => Promise<{
530
+ id: string;
531
+ }>;
532
+ }
533
+ declare function restoreDocumentSnapshot(args: {
534
+ documentId: string;
535
+ snapshot: unknown;
536
+ label: string | null;
537
+ }, deps?: Partial<DocumentRestoreDeps>): Promise<DocumentRestoreResult>;
538
+
539
+ 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 OtRoomsKnob, RICH_TEXT_EDITING, SENDER_VIEW_PARAMS, WorkbookCollabSession, type WorkbookCollabSessionOptions, applyDocsOp, canonicalDocsSnapshot, canonicalDocsString, canonicalKeys, composeDocsOps, docsStateHash, documentOtFactory, documentUsesOrderedRoom, dropEmptyStyleRetains, invertDocsOp, isCellEditorScratch, isNoopDocsOp, normalizeDocsOp, openDocsOtRoom, rebaseUndoStack, restoreDocumentSnapshot, stampCovered, stampDeterministicIds, stripSenderView, takeCovered, transformDocsOp, transformTextX, undoStacksOf };
@@ -900,10 +900,10 @@ function canonicalRuns(runs) {
900
900
  }
901
901
  return out;
902
902
  }
903
- function foldPieces(items, keyOf) {
903
+ function foldPieces(items, keyOf2) {
904
904
  const groups = /* @__PURE__ */ new Map();
905
905
  for (const it of items ?? []) {
906
- const k = keyOf(it);
906
+ const k = keyOf2(it);
907
907
  const g = groups.get(k) ?? [];
908
908
  g.push(it);
909
909
  groups.set(k, g);
@@ -1220,6 +1220,120 @@ import {
1220
1220
  supabaseOpTransport
1221
1221
  } from "@ai-matrx/data/realtime/collab";
1222
1222
 
1223
+ // src/docs/document-model.ts
1224
+ import { TextX as TextX2 } from "@univerjs/core";
1225
+
1226
+ // src/univer/openEditorContent.ts
1227
+ var replacers = /* @__PURE__ */ new Map();
1228
+ var keyOf = (kind, recordId) => `${kind}:${recordId}`;
1229
+ function hasOpenEditorContent(kind, recordId) {
1230
+ return replacers.has(keyOf(kind, recordId));
1231
+ }
1232
+ async function showSavedSnapshot(kind, recordId, row) {
1233
+ const replace = replacers.get(keyOf(kind, recordId));
1234
+ if (!replace) return false;
1235
+ await replace(row);
1236
+ return true;
1237
+ }
1238
+
1239
+ // src/docs/document-model.ts
1240
+ function moveRangesThroughBodyEdit(ranges, actions) {
1241
+ const a = actions;
1242
+ if (!Array.isArray(a) || a[0] !== "body") return null;
1243
+ const sub = a[1];
1244
+ if (!sub || sub.et !== "text-x" || !Array.isArray(sub.e)) return null;
1245
+ const e = sub.e;
1246
+ return ranges.map((r) => {
1247
+ const collapsed = r.startOffset === r.endOffset;
1248
+ const startOffset = TextX2.transformPosition(e, r.startOffset, collapsed);
1249
+ return { startOffset, endOffset: collapsed ? startOffset : TextX2.transformPosition(e, r.endOffset, true) };
1250
+ });
1251
+ }
1252
+
1253
+ // src/docs/remote-carets.ts
1254
+ var CARET_HEARTBEAT_MS = 2e3;
1255
+ var CARET_STALE_MS = 12e3;
1256
+ var CARET_MAX_RANGES = 4;
1257
+ function publishedCaretFromSelection(sel, now) {
1258
+ if (!sel || sel.length === 0) return null;
1259
+ if (sel.some((r) => r.segmentId)) return null;
1260
+ const ranges = sel.slice(0, CARET_MAX_RANGES).map((r) => ({ start: Math.min(r.startOffset, r.endOffset), end: Math.max(r.startOffset, r.endOffset) }));
1261
+ return { ranges, at: now };
1262
+ }
1263
+ var sameRanges = (a, b) => a.length === b.length && a.every((r, i) => r.start === b[i].start && r.end === b[i].end);
1264
+ function readCaret(raw) {
1265
+ const c = raw?.caret;
1266
+ if (!c || !Array.isArray(c.ranges) || typeof c.at !== "number") return null;
1267
+ const ranges = [];
1268
+ for (const r of c.ranges.slice(0, CARET_MAX_RANGES)) {
1269
+ const s = r?.start;
1270
+ const e = r?.end;
1271
+ if (typeof s !== "number" || typeof e !== "number" || !Number.isFinite(s) || !Number.isFinite(e) || s < 0 || e < 0) return null;
1272
+ ranges.push({ start: s, end: e });
1273
+ }
1274
+ return ranges.length ? { ranges, at: c.at } : null;
1275
+ }
1276
+ var RemoteCaretBook = class {
1277
+ constructor(selfClientId, now = Date.now) {
1278
+ this.selfClientId = selfClientId;
1279
+ this.now = now;
1280
+ }
1281
+ selfClientId;
1282
+ now;
1283
+ carets = /* @__PURE__ */ new Map();
1284
+ /** Take the room's awareness states: new or republished carets replace ours, seats that left are dropped. Returns whether anything changed. */
1285
+ ingest(states) {
1286
+ let changed = false;
1287
+ const seen = /* @__PURE__ */ new Set();
1288
+ states.forEach((raw, id) => {
1289
+ if (id === this.selfClientId) return;
1290
+ const s = raw;
1291
+ const caret = readCaret(raw);
1292
+ if (!s || typeof s.uid !== "string" || !caret) return;
1293
+ const key = String(id);
1294
+ seen.add(key);
1295
+ const prev = this.carets.get(key);
1296
+ const name = typeof s.name === "string" && s.name ? s.name : "Someone";
1297
+ const color = typeof s.color === "string" && s.color ? s.color : "#3b82f6";
1298
+ if (prev && prev.at === caret.at && prev.name === name && prev.color === color) return;
1299
+ this.carets.set(key, { key, uid: s.uid, name, color, ranges: caret.ranges, at: caret.at, seenAt: this.now() });
1300
+ changed = true;
1301
+ });
1302
+ for (const key of [...this.carets.keys()]) {
1303
+ if (!seen.has(key)) {
1304
+ this.carets.delete(key);
1305
+ changed = true;
1306
+ }
1307
+ }
1308
+ return changed;
1309
+ }
1310
+ /** Move every caret through one body edit that was just applied (`actions` of a rich-text mutation). Returns whether any moved. */
1311
+ shiftThrough(actions) {
1312
+ let moved = false;
1313
+ for (const c of this.carets.values()) {
1314
+ const next = moveRangesThroughBodyEdit(c.ranges.map((r) => ({ startOffset: r.start, endOffset: r.end })), actions);
1315
+ if (!next) return false;
1316
+ const ranges = next.map((r) => ({ start: r.startOffset, end: r.endOffset }));
1317
+ if (!sameRanges(ranges, c.ranges)) {
1318
+ c.ranges = ranges;
1319
+ moved = true;
1320
+ }
1321
+ }
1322
+ return moved;
1323
+ }
1324
+ /** The carets to draw now (stale ones left out). */
1325
+ list() {
1326
+ const t = this.now();
1327
+ const out = [];
1328
+ for (const c of this.carets.values()) {
1329
+ if (t - c.seenAt > CARET_STALE_MS) continue;
1330
+ out.push({ key: c.key, uid: c.uid, name: c.name, color: c.color, ranges: c.ranges.map((r) => ({ ...r })) });
1331
+ }
1332
+ out.sort((a, b) => a.key.localeCompare(b.key));
1333
+ return out;
1334
+ }
1335
+ };
1336
+
1223
1337
  // src/docs/document-service.ts
1224
1338
  import { requireOrganizationContext } from "@ai-matrx/api";
1225
1339
  import { getClaimsUser } from "@ai-matrx/auth";
@@ -1292,11 +1406,11 @@ function pickColor2(uid) {
1292
1406
  return palette[Math.abs(h) % palette.length] ?? "#ec4899";
1293
1407
  }
1294
1408
  function documentOtFactory(o) {
1295
- return async (port, onAwareness) => {
1409
+ return async (basePort, onAwareness) => {
1296
1410
  const latest = await getLatestDocumentSnapshot(o.documentId);
1297
1411
  if (!latest.success) throw new Error(latest.error);
1298
1412
  if (!latest.data) {
1299
- const mounted = port.snapshot();
1413
+ const mounted = basePort.snapshot();
1300
1414
  if (mounted) {
1301
1415
  const saved = await saveDocumentSnapshot({ documentId: o.documentId, snapshot: mounted, origin: "autosave" });
1302
1416
  if (!saved.success) throw new Error(saved.error);
@@ -1305,7 +1419,31 @@ function documentOtFactory(o) {
1305
1419
  const doc = new Y2.Doc();
1306
1420
  const awareness = new Awareness2(doc);
1307
1421
  awareness.setLocalState({ uid: o.uid, clientId: o.clientId, name: o.displayName ?? o.uid.slice(0, 8), color: pickColor2(o.uid), ts: Date.now() });
1308
- const publishAwareness = () => onAwareness({ states: new Map(awareness.getStates()), selfUid: o.uid });
1422
+ const carets = new RemoteCaretBook(awareness.clientID);
1423
+ const emit = () => onAwareness({ states: new Map(awareness.getStates()), selfUid: o.uid, carets: carets.list() });
1424
+ const publishAwareness = () => {
1425
+ carets.ingest(awareness.getStates());
1426
+ emit();
1427
+ };
1428
+ const bodyActions = (params) => {
1429
+ const p = params;
1430
+ return p && !p.segmentId ? p.actions : void 0;
1431
+ };
1432
+ const port = {
1433
+ ...basePort,
1434
+ applyRemote(id, params) {
1435
+ basePort.applyRemote(id, params);
1436
+ const a = bodyActions(params);
1437
+ if (a !== void 0 && carets.shiftThrough(a)) emit();
1438
+ },
1439
+ onLocal: (listener) => basePort.onLocal((m) => {
1440
+ if (m.id === RICH_TEXT_EDITING) {
1441
+ const a = bodyActions(m.params);
1442
+ if (a !== void 0 && carets.shiftThrough(a)) emit();
1443
+ }
1444
+ listener(m);
1445
+ })
1446
+ };
1309
1447
  awareness.on("change", publishAwareness);
1310
1448
  const provider = new SupabaseYjsProvider({
1311
1449
  workbookId: o.documentId,
@@ -1341,11 +1479,31 @@ function documentOtFactory(o) {
1341
1479
  ...o.onStatus ? { onStatus: o.onStatus } : {}
1342
1480
  });
1343
1481
  await room.session.ready;
1482
+ let lastKey = "";
1483
+ let lastAt = 0;
1484
+ const publishCaret = () => {
1485
+ if (typeof document !== "undefined" && document.hidden) return;
1486
+ let sel = null;
1487
+ try {
1488
+ sel = port.selection?.() ?? null;
1489
+ } catch {
1490
+ sel = null;
1491
+ }
1492
+ const t = Date.now();
1493
+ const c = publishedCaretFromSelection(sel, t);
1494
+ const key = c ? JSON.stringify(c.ranges) : "";
1495
+ if (key === lastKey && (key === "" || t - lastAt < CARET_HEARTBEAT_MS)) return;
1496
+ lastKey = key;
1497
+ lastAt = t;
1498
+ awareness.setLocalStateField("caret", c);
1499
+ };
1500
+ const caretTimer = setInterval(publishCaret, 120);
1344
1501
  return {
1345
1502
  isSaver: () => room.isSaver(),
1346
1503
  write: (reason) => room.write(reason),
1347
1504
  replace: (snapshot) => room.replace(snapshot),
1348
1505
  stop: () => {
1506
+ clearInterval(caretTimer);
1349
1507
  room.stop();
1350
1508
  awareness.off("change", publishAwareness);
1351
1509
  provider.disconnect();
@@ -1355,6 +1513,43 @@ function documentOtFactory(o) {
1355
1513
  };
1356
1514
  };
1357
1515
  }
1516
+
1517
+ // src/docs/document-restore.ts
1518
+ import { collabReplace as collabReplace2, collabRoomExists as collabRoomExists2 } from "@ai-matrx/data/realtime/collab";
1519
+ var defaultDeps = {
1520
+ client: () => getDocsHost().getSupabase(),
1521
+ roomExists: collabRoomExists2,
1522
+ replace: collabReplace2,
1523
+ stateHash: (snapshot) => docsStateHash(snapshot),
1524
+ hasOpenEditor: (documentId) => hasOpenEditorContent("document", documentId),
1525
+ showInOpenEditor: (documentId, row) => showSavedSnapshot("document", documentId, row),
1526
+ async saveRow(args) {
1527
+ const res = await saveDocumentSnapshot({ documentId: args.documentId, snapshot: args.snapshot, origin: "restored", label: args.label });
1528
+ if (!res.success) throw new Error(res.error);
1529
+ return { id: res.data.id };
1530
+ }
1531
+ };
1532
+ async function restoreDocumentSnapshot(args, deps = {}) {
1533
+ const d = { ...defaultDeps, ...deps };
1534
+ if (d.hasOpenEditor(args.documentId)) {
1535
+ const row2 = await d.saveRow(args);
1536
+ await d.showInOpenEditor(args.documentId, { id: row2.id, snapshot: args.snapshot });
1537
+ return { via: "open_editor", snapshotId: row2.id };
1538
+ }
1539
+ const client = d.client();
1540
+ if (await d.roomExists(client, args.documentId)) {
1541
+ const next = stampDeterministicIds(structuredClone(args.snapshot ?? {}));
1542
+ const out = await d.replace(client, {
1543
+ roomId: args.documentId,
1544
+ snapshot: next,
1545
+ stateHash: await d.stateHash(next),
1546
+ origin: "restore"
1547
+ });
1548
+ return { via: "room", rev: out.rev, snapshotId: out.ref };
1549
+ }
1550
+ const row = await d.saveRow(args);
1551
+ return { via: "saved", snapshotId: row.id };
1552
+ }
1358
1553
  export {
1359
1554
  COLLAB_COVERED_KEY,
1360
1555
  DOCS_COLLAB_PROTO,
@@ -1378,6 +1573,7 @@ export {
1378
1573
  normalizeDocsOp,
1379
1574
  openDocsOtRoom,
1380
1575
  rebaseUndoStack,
1576
+ restoreDocumentSnapshot,
1381
1577
  stampCovered,
1382
1578
  stampDeterministicIds,
1383
1579
  stripSenderView,