@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.
@@ -53,6 +53,7 @@ __export(collab_exports, {
53
53
  normalizeDocsOp: () => normalizeDocsOp,
54
54
  openDocsOtRoom: () => openDocsOtRoom,
55
55
  rebaseUndoStack: () => rebaseUndoStack,
56
+ restoreDocumentSnapshot: () => restoreDocumentSnapshot,
56
57
  stampCovered: () => stampCovered,
57
58
  stampDeterministicIds: () => stampDeterministicIds,
58
59
  stripSenderView: () => stripSenderView,
@@ -963,10 +964,10 @@ function canonicalRuns(runs) {
963
964
  }
964
965
  return out;
965
966
  }
966
- function foldPieces(items, keyOf) {
967
+ function foldPieces(items, keyOf2) {
967
968
  const groups = /* @__PURE__ */ new Map();
968
969
  for (const it of items ?? []) {
969
- const k = keyOf(it);
970
+ const k = keyOf2(it);
970
971
  const g = groups.get(k) ?? [];
971
972
  g.push(it);
972
973
  groups.set(k, g);
@@ -1277,6 +1278,120 @@ var Y2 = __toESM(require("yjs"), 1);
1277
1278
  var import_awareness2 = require("y-protocols/awareness");
1278
1279
  var import_collab3 = require("@ai-matrx/data/realtime/collab");
1279
1280
 
1281
+ // src/docs/document-model.ts
1282
+ var import_core2 = require("@univerjs/core");
1283
+
1284
+ // src/univer/openEditorContent.ts
1285
+ var replacers = /* @__PURE__ */ new Map();
1286
+ var keyOf = (kind, recordId) => `${kind}:${recordId}`;
1287
+ function hasOpenEditorContent(kind, recordId) {
1288
+ return replacers.has(keyOf(kind, recordId));
1289
+ }
1290
+ async function showSavedSnapshot(kind, recordId, row) {
1291
+ const replace = replacers.get(keyOf(kind, recordId));
1292
+ if (!replace) return false;
1293
+ await replace(row);
1294
+ return true;
1295
+ }
1296
+
1297
+ // src/docs/document-model.ts
1298
+ function moveRangesThroughBodyEdit(ranges, actions) {
1299
+ const a = actions;
1300
+ if (!Array.isArray(a) || a[0] !== "body") return null;
1301
+ const sub = a[1];
1302
+ if (!sub || sub.et !== "text-x" || !Array.isArray(sub.e)) return null;
1303
+ const e = sub.e;
1304
+ return ranges.map((r) => {
1305
+ const collapsed = r.startOffset === r.endOffset;
1306
+ const startOffset = import_core2.TextX.transformPosition(e, r.startOffset, collapsed);
1307
+ return { startOffset, endOffset: collapsed ? startOffset : import_core2.TextX.transformPosition(e, r.endOffset, true) };
1308
+ });
1309
+ }
1310
+
1311
+ // src/docs/remote-carets.ts
1312
+ var CARET_HEARTBEAT_MS = 2e3;
1313
+ var CARET_STALE_MS = 12e3;
1314
+ var CARET_MAX_RANGES = 4;
1315
+ function publishedCaretFromSelection(sel, now) {
1316
+ if (!sel || sel.length === 0) return null;
1317
+ if (sel.some((r) => r.segmentId)) return null;
1318
+ const ranges = sel.slice(0, CARET_MAX_RANGES).map((r) => ({ start: Math.min(r.startOffset, r.endOffset), end: Math.max(r.startOffset, r.endOffset) }));
1319
+ return { ranges, at: now };
1320
+ }
1321
+ var sameRanges = (a, b) => a.length === b.length && a.every((r, i) => r.start === b[i].start && r.end === b[i].end);
1322
+ function readCaret(raw) {
1323
+ const c = raw?.caret;
1324
+ if (!c || !Array.isArray(c.ranges) || typeof c.at !== "number") return null;
1325
+ const ranges = [];
1326
+ for (const r of c.ranges.slice(0, CARET_MAX_RANGES)) {
1327
+ const s = r?.start;
1328
+ const e = r?.end;
1329
+ if (typeof s !== "number" || typeof e !== "number" || !Number.isFinite(s) || !Number.isFinite(e) || s < 0 || e < 0) return null;
1330
+ ranges.push({ start: s, end: e });
1331
+ }
1332
+ return ranges.length ? { ranges, at: c.at } : null;
1333
+ }
1334
+ var RemoteCaretBook = class {
1335
+ constructor(selfClientId, now = Date.now) {
1336
+ this.selfClientId = selfClientId;
1337
+ this.now = now;
1338
+ }
1339
+ selfClientId;
1340
+ now;
1341
+ carets = /* @__PURE__ */ new Map();
1342
+ /** Take the room's awareness states: new or republished carets replace ours, seats that left are dropped. Returns whether anything changed. */
1343
+ ingest(states) {
1344
+ let changed = false;
1345
+ const seen = /* @__PURE__ */ new Set();
1346
+ states.forEach((raw, id) => {
1347
+ if (id === this.selfClientId) return;
1348
+ const s = raw;
1349
+ const caret = readCaret(raw);
1350
+ if (!s || typeof s.uid !== "string" || !caret) return;
1351
+ const key = String(id);
1352
+ seen.add(key);
1353
+ const prev = this.carets.get(key);
1354
+ const name = typeof s.name === "string" && s.name ? s.name : "Someone";
1355
+ const color = typeof s.color === "string" && s.color ? s.color : "#3b82f6";
1356
+ if (prev && prev.at === caret.at && prev.name === name && prev.color === color) return;
1357
+ this.carets.set(key, { key, uid: s.uid, name, color, ranges: caret.ranges, at: caret.at, seenAt: this.now() });
1358
+ changed = true;
1359
+ });
1360
+ for (const key of [...this.carets.keys()]) {
1361
+ if (!seen.has(key)) {
1362
+ this.carets.delete(key);
1363
+ changed = true;
1364
+ }
1365
+ }
1366
+ return changed;
1367
+ }
1368
+ /** Move every caret through one body edit that was just applied (`actions` of a rich-text mutation). Returns whether any moved. */
1369
+ shiftThrough(actions) {
1370
+ let moved = false;
1371
+ for (const c of this.carets.values()) {
1372
+ const next = moveRangesThroughBodyEdit(c.ranges.map((r) => ({ startOffset: r.start, endOffset: r.end })), actions);
1373
+ if (!next) return false;
1374
+ const ranges = next.map((r) => ({ start: r.startOffset, end: r.endOffset }));
1375
+ if (!sameRanges(ranges, c.ranges)) {
1376
+ c.ranges = ranges;
1377
+ moved = true;
1378
+ }
1379
+ }
1380
+ return moved;
1381
+ }
1382
+ /** The carets to draw now (stale ones left out). */
1383
+ list() {
1384
+ const t = this.now();
1385
+ const out = [];
1386
+ for (const c of this.carets.values()) {
1387
+ if (t - c.seenAt > CARET_STALE_MS) continue;
1388
+ out.push({ key: c.key, uid: c.uid, name: c.name, color: c.color, ranges: c.ranges.map((r) => ({ ...r })) });
1389
+ }
1390
+ out.sort((a, b) => a.key.localeCompare(b.key));
1391
+ return out;
1392
+ }
1393
+ };
1394
+
1280
1395
  // src/docs/document-service.ts
1281
1396
  var import_api = require("@ai-matrx/api");
1282
1397
  var import_auth = require("@ai-matrx/auth");
@@ -1349,11 +1464,11 @@ function pickColor2(uid) {
1349
1464
  return palette[Math.abs(h) % palette.length] ?? "#ec4899";
1350
1465
  }
1351
1466
  function documentOtFactory(o) {
1352
- return async (port, onAwareness) => {
1467
+ return async (basePort, onAwareness) => {
1353
1468
  const latest = await getLatestDocumentSnapshot(o.documentId);
1354
1469
  if (!latest.success) throw new Error(latest.error);
1355
1470
  if (!latest.data) {
1356
- const mounted = port.snapshot();
1471
+ const mounted = basePort.snapshot();
1357
1472
  if (mounted) {
1358
1473
  const saved = await saveDocumentSnapshot({ documentId: o.documentId, snapshot: mounted, origin: "autosave" });
1359
1474
  if (!saved.success) throw new Error(saved.error);
@@ -1362,7 +1477,31 @@ function documentOtFactory(o) {
1362
1477
  const doc = new Y2.Doc();
1363
1478
  const awareness = new import_awareness2.Awareness(doc);
1364
1479
  awareness.setLocalState({ uid: o.uid, clientId: o.clientId, name: o.displayName ?? o.uid.slice(0, 8), color: pickColor2(o.uid), ts: Date.now() });
1365
- const publishAwareness = () => onAwareness({ states: new Map(awareness.getStates()), selfUid: o.uid });
1480
+ const carets = new RemoteCaretBook(awareness.clientID);
1481
+ const emit = () => onAwareness({ states: new Map(awareness.getStates()), selfUid: o.uid, carets: carets.list() });
1482
+ const publishAwareness = () => {
1483
+ carets.ingest(awareness.getStates());
1484
+ emit();
1485
+ };
1486
+ const bodyActions = (params) => {
1487
+ const p = params;
1488
+ return p && !p.segmentId ? p.actions : void 0;
1489
+ };
1490
+ const port = {
1491
+ ...basePort,
1492
+ applyRemote(id, params) {
1493
+ basePort.applyRemote(id, params);
1494
+ const a = bodyActions(params);
1495
+ if (a !== void 0 && carets.shiftThrough(a)) emit();
1496
+ },
1497
+ onLocal: (listener) => basePort.onLocal((m) => {
1498
+ if (m.id === RICH_TEXT_EDITING) {
1499
+ const a = bodyActions(m.params);
1500
+ if (a !== void 0 && carets.shiftThrough(a)) emit();
1501
+ }
1502
+ listener(m);
1503
+ })
1504
+ };
1366
1505
  awareness.on("change", publishAwareness);
1367
1506
  const provider = new import_collab3.SupabaseYjsProvider({
1368
1507
  workbookId: o.documentId,
@@ -1398,11 +1537,31 @@ function documentOtFactory(o) {
1398
1537
  ...o.onStatus ? { onStatus: o.onStatus } : {}
1399
1538
  });
1400
1539
  await room.session.ready;
1540
+ let lastKey = "";
1541
+ let lastAt = 0;
1542
+ const publishCaret = () => {
1543
+ if (typeof document !== "undefined" && document.hidden) return;
1544
+ let sel = null;
1545
+ try {
1546
+ sel = port.selection?.() ?? null;
1547
+ } catch {
1548
+ sel = null;
1549
+ }
1550
+ const t = Date.now();
1551
+ const c = publishedCaretFromSelection(sel, t);
1552
+ const key = c ? JSON.stringify(c.ranges) : "";
1553
+ if (key === lastKey && (key === "" || t - lastAt < CARET_HEARTBEAT_MS)) return;
1554
+ lastKey = key;
1555
+ lastAt = t;
1556
+ awareness.setLocalStateField("caret", c);
1557
+ };
1558
+ const caretTimer = setInterval(publishCaret, 120);
1401
1559
  return {
1402
1560
  isSaver: () => room.isSaver(),
1403
1561
  write: (reason) => room.write(reason),
1404
1562
  replace: (snapshot) => room.replace(snapshot),
1405
1563
  stop: () => {
1564
+ clearInterval(caretTimer);
1406
1565
  room.stop();
1407
1566
  awareness.off("change", publishAwareness);
1408
1567
  provider.disconnect();
@@ -1412,4 +1571,41 @@ function documentOtFactory(o) {
1412
1571
  };
1413
1572
  };
1414
1573
  }
1574
+
1575
+ // src/docs/document-restore.ts
1576
+ var import_collab4 = require("@ai-matrx/data/realtime/collab");
1577
+ var defaultDeps = {
1578
+ client: () => getDocsHost().getSupabase(),
1579
+ roomExists: import_collab4.collabRoomExists,
1580
+ replace: import_collab4.collabReplace,
1581
+ stateHash: (snapshot) => docsStateHash(snapshot),
1582
+ hasOpenEditor: (documentId) => hasOpenEditorContent("document", documentId),
1583
+ showInOpenEditor: (documentId, row) => showSavedSnapshot("document", documentId, row),
1584
+ async saveRow(args) {
1585
+ const res = await saveDocumentSnapshot({ documentId: args.documentId, snapshot: args.snapshot, origin: "restored", label: args.label });
1586
+ if (!res.success) throw new Error(res.error);
1587
+ return { id: res.data.id };
1588
+ }
1589
+ };
1590
+ async function restoreDocumentSnapshot(args, deps = {}) {
1591
+ const d = { ...defaultDeps, ...deps };
1592
+ if (d.hasOpenEditor(args.documentId)) {
1593
+ const row2 = await d.saveRow(args);
1594
+ await d.showInOpenEditor(args.documentId, { id: row2.id, snapshot: args.snapshot });
1595
+ return { via: "open_editor", snapshotId: row2.id };
1596
+ }
1597
+ const client = d.client();
1598
+ if (await d.roomExists(client, args.documentId)) {
1599
+ const next = stampDeterministicIds(structuredClone(args.snapshot ?? {}));
1600
+ const out = await d.replace(client, {
1601
+ roomId: args.documentId,
1602
+ snapshot: next,
1603
+ stateHash: await d.stateHash(next),
1604
+ origin: "restore"
1605
+ });
1606
+ return { via: "room", rev: out.rev, snapshotId: out.ref };
1607
+ }
1608
+ const row = await d.saveRow(args);
1609
+ return { via: "saved", snapshotId: row.id };
1610
+ }
1415
1611
  //# sourceMappingURL=univer-collab.cjs.map