@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.
package/CHANGELOG.md CHANGED
@@ -1,13 +1,12 @@
1
1
  # Changelog
2
2
 
3
- ## 0.8.23
3
+ ## 0.8.25
4
4
  ### C9: comment anchors that follow concurrent edits
5
5
 
6
6
  - **Docs:** `addCommentAnchorActions(body, start, end, id)` anchors a passage with a `COMMENT` custom range (Univer's own builder, so the op has the link family's shape C0 proved); `commentAnchorsOf(body)` reads every anchor back (split pieces joined, Univer's inclusive end as `[start, end)`); `removeCommentAnchorActions` takes a cancelled draft's anchor out; `commentPartAnchor` / `anchorIdOfPartAnchor` are the thread's `part_anchor` (`anchor:<id>`, quote as `label`). Fuzz on the C0 corpora: anchor × every captured family and anchor × anchor, 0 divergences in 180,000 checks at 2,000 seeds; red check without a transform.
7
7
  - **Sheets:** `installSheetCommentAnchors(univer)` registers `matrx.mutation.set-comment-anchor`, the `SHEET_MATRX_COMMENT_ANCHORS_PLUGIN` resource and a listener that moves anchors through every applied row/column insert, remove and move (and drops them with their row, column or sheet). `transformCommentAnchor` (own file) is called first by `transformMutation`; the registry declares the id (family `anchor`), and the census exempts our own ids from the Univer scan. Fuzz: 40,000 checks, 0 divergences (it caught the same anchor id on two sheets being treated as one); two headless seats: B5 becomes B6 when a peer inserts a row above, survives save and reload, and is gone on both seats when its row is deleted. The fold installs the anchors too.
8
8
  - New export `./sheets/comment-anchors`. Consumer action: call `installSheetCommentAnchors(univer)` before creating a workbook unit that co-edits comments.
9
9
 
10
- ## 0.8.22
11
10
  ### C5c: Sheets co-editing — merge divergence fixed, number formats, counts and sheets travel
12
11
 
13
12
  - **Merge divergence across chained row/column deletes, fixed at its cause.** Univer 1.0.4's structural commands only fix the merges that intersect the COMMAND's range on the other axis (any `-by-range` call with a partial range, an API call) and only while its ref-range registration is current (never refreshed after a content load or a remove-merge). A cut merge it skipped stayed put on the sender while every transform moved it, so seats ended a column apart (the session fuzz's merge at column -1 vs 0). Capture now completes the command (`mergeFixupsOwed`): the cut merges still on the sheet are removed right before the structural mutation and re-added at their model position right after it, in the traveling group and on the sender alike. Multi-seat session fuzz: 0 hash mismatches (was 2–18 per run).
@@ -21,19 +20,6 @@
21
20
  - `SHEETS_COLLAB_PROTO` = 2: a room opened by this build is raised to proto 2 (when its checkpoint is at head) and older tabs open it read-only, so seats with and without merge completion never share a room.
22
21
  - Consumer action: none.
23
22
 
24
- ## 0.8.21
25
- ### C5c: Sheets co-editing — merge divergence fixed, number formats, counts and sheets travel
26
-
27
- - **Merge divergence across chained row/column deletes, fixed at its cause.** Univer 1.0.4's structural commands only fix the merges that intersect the COMMAND's range on the other axis (any `-by-range` call with a partial range, an API call) and only while its ref-range registration is current (never refreshed after a content load or a remove-merge). A cut merge it skipped stayed put on the sender while every transform moved it, so seats ended a column apart (the session fuzz's merge at column -1 vs 0). Capture now completes the command (`mergeFixupsOwed`): the cut merges still on the sheet are removed right before the structural mutation and re-added at their model position right after it, in the traveling group and on the sender alike. Multi-seat session fuzz: 0 hash mismatches (was 2–18 per run).
28
- - **Number formats travel** (`sheet.mutation.set.numfmt` / `remove.numfmt`, family `numfmt`): typing a date or a currency no longer rolls back. Ranges follow row/column inserts, removes and moves cell by cell; against another write of the style key `n` (a numfmt, a style with `n`, a style clear, a cell delete) the later-committed op wins.
29
- - **Row/column counts travel** (`set-worksheet-row-count` / `-column-count`, family `count`): a concurrent insert/remove on that axis shifts the count; two counts at once, the larger wins. "Refresh from table" growing a sheet is no longer rolled back.
30
- - **Sheets: add, remove, order travel** (family `sheet`): the sheet order is transformed as one more axis. Capture records a removed sheet's index and how many sheets are left (`annotateSheetOps`, side-car `sheetIndex`, `sheetsLeft`); an insert-sheet's styles are inlined (`inlineSheetStyles`). An edit to a sheet removed at the same time is dropped and its author told; a formula naming a removed sheet, and two removes that would leave no sheet, are barriers.
31
- - **Selections: a seat's own row/column/count/sheet change keeps its selection inside the sheet** (API calls set no selection; the editor crashed with "Range is out of bounds"); a remote count/sheet change clamps it too.
32
- - **Transform speed:** the group transform walks the OT grid iteratively; untouched cell ops pass through by identity (no overlap, no formula, change elsewhere); barrier checks are cached per op; undo-stack rebase reuses each list's traveling shape and compares by identity.
33
- - **Tests:** new chained TP1 fuzz (`sheet-transform-chain-fuzz.test.ts`, 3–5 real commands per side, plus capture fidelity); the multi-seat session fuzz is seed-repeatable (`createSheetsOtAdapter` hook `stateHash`, a synchronous hash in tests) and asserts zero incidents. `SHEET_TRANSFORMER_VERSION` = `c5c-1`.
34
- - `SHEETS_COLLAB_PROTO` = 2: a room opened by this build is raised to proto 2 (when its checkpoint is at head) and older tabs open it read-only, so seats with and without merge completion never share a room.
35
- - Consumer action: none.
36
-
37
23
  ## 0.8.20
38
24
 
39
25
  SOURCE CHANGED WITH NO RELEASE NOTE. This release shipped source changes that nobody described in an Unreleased entry,
@@ -60,6 +60,7 @@ __export(collab_exports, {
60
60
  isNoopDocsOp: () => isNoopDocsOp,
61
61
  moveSelectionsThrough: () => moveSelectionsThrough,
62
62
  normalizeDocsOp: () => normalizeDocsOp,
63
+ offlineTextOfGroups: () => offlineTextOfGroups,
63
64
  openDocsOtRoom: () => openDocsOtRoom,
64
65
  openSheetsOtRoom: () => openSheetsOtRoom,
65
66
  rebaseSheetUndoStack: () => rebaseSheetUndoStack,
@@ -1370,7 +1371,9 @@ function openDocsOtRoom(o) {
1370
1371
  ...o.presence ? { presence: o.presence } : {},
1371
1372
  onNotice: (n) => notice(n),
1372
1373
  ...o.onIncident ? { onIncident: o.onIncident } : {},
1373
- ...o.onStatus ? { onStatus: o.onStatus } : {}
1374
+ ...o.onStatus ? { onStatus: o.onStatus } : {},
1375
+ ...o.offline ? { offline: o.offline } : {},
1376
+ ...o.onOffline ? { onOffline: o.onOffline } : {}
1374
1377
  });
1375
1378
  const isSaver = () => {
1376
1379
  if (!o.presence) return true;
@@ -1638,7 +1641,9 @@ function documentOtFactory(o) {
1638
1641
  replace: ({ snapshot, stateHash, clientId }) => (0, import_collab3.collabReplace)(client, { roomId: o.documentId, snapshot, stateHash, origin: "restore", clientId }),
1639
1642
  ...o.onNotice ? { onNotice: o.onNotice } : {},
1640
1643
  ...o.onIncident ? { onIncident: o.onIncident } : {},
1641
- ...o.onStatus ? { onStatus: o.onStatus } : {}
1644
+ ...o.onStatus ? { onStatus: o.onStatus } : {},
1645
+ offline: (0, import_collab3.browserOfflineOptions)(`${o.documentId}:${o.uid}`),
1646
+ ...o.onOffline ? { onOffline: o.onOffline } : {}
1642
1647
  });
1643
1648
  await room.session.ready;
1644
1649
  let lastKey = "";
@@ -3559,17 +3564,65 @@ function rebaseSheetUndoStack(items, remote, unitId) {
3559
3564
  function createSheetsOtAdapter(port, hooks = {}) {
3560
3565
  let queue = [];
3561
3566
  let draining = false;
3567
+ const lazy = typeof port.beforeUndo === "function";
3568
+ let pending = [];
3569
+ let pendingBase = 0;
3570
+ let applied = 0;
3571
+ let currentAt = /* @__PURE__ */ new WeakMap();
3572
+ const PENDING_CAP = 2e3;
3573
+ const now = () => typeof performance !== "undefined" ? performance.now() : Date.now();
3574
+ const stacksOfTargets = () => port.undoTargets().flatMap((t) => {
3575
+ const stacks = undoStacksOf2(t.service);
3576
+ return stacks ? [stacks.undo, stacks.redo].map((map) => ({ items: map.get(t.unitId), unitId: t.unitId, isUndo: map === stacks.undo })) : [];
3577
+ });
3578
+ const markNew = (items) => {
3579
+ for (let i = items.length - 1; i >= 0 && !currentAt.has(items[i]); i--) currentAt.set(items[i], applied);
3580
+ };
3581
+ const catchUpItem = (item, unitId) => {
3582
+ const from = currentAt.get(item) ?? applied;
3583
+ for (let k = from - pendingBase; k < pending.length; k++) if (rebaseSheetUndoStack([item], pending[k], unitId) > 0) return false;
3584
+ currentAt.set(item, applied);
3585
+ return true;
3586
+ };
3587
+ const catchUp = () => {
3588
+ if (!pending.length) return;
3589
+ const t0 = now();
3590
+ let dropped = 0;
3591
+ for (const { items, unitId } of stacksOfTargets()) {
3592
+ if (!items?.length) continue;
3593
+ for (let i = items.length - 1; i >= 0; i--)
3594
+ if (!catchUpItem(items[i], unitId)) {
3595
+ items.splice(i, 1);
3596
+ dropped++;
3597
+ }
3598
+ }
3599
+ pending = [];
3600
+ pendingBase = applied;
3601
+ hooks.onUndoWork?.(now() - t0, "catch_up");
3602
+ if (dropped) hooks.onUndoRebased?.(dropped);
3603
+ };
3604
+ if (lazy) port.beforeUndo(catchUp);
3562
3605
  const applyNow = (op) => {
3563
3606
  for (const m of op) port.applyRemote(m);
3607
+ const t0 = now();
3564
3608
  let dropped = 0;
3565
- for (const t of port.undoTargets()) {
3566
- const stacks = undoStacksOf2(t.service);
3567
- if (!stacks) continue;
3568
- for (const map of [stacks.undo, stacks.redo]) {
3569
- const items = map.get(t.unitId);
3570
- if (items?.length) dropped += rebaseSheetUndoStack(items, op, t.unitId);
3609
+ if (!lazy) {
3610
+ for (const { items, unitId } of stacksOfTargets()) if (items?.length) dropped += rebaseSheetUndoStack(items, op, unitId);
3611
+ } else if (!isNoopSheetOp(op)) {
3612
+ for (const { items, unitId, isUndo } of stacksOfTargets()) {
3613
+ if (!items?.length) continue;
3614
+ markNew(items);
3615
+ const top = items[items.length - 1];
3616
+ if (isUndo && (!catchUpItem(top, unitId) || rebaseSheetUndoStack([top], op, unitId) > 0)) {
3617
+ items.pop();
3618
+ dropped++;
3619
+ } else if (isUndo) currentAt.set(top, applied + 1);
3571
3620
  }
3621
+ pending.push(op);
3622
+ applied++;
3623
+ if (pending.length > PENDING_CAP) catchUp();
3572
3624
  }
3625
+ hooks.onUndoWork?.(now() - t0, "remote");
3573
3626
  hooks.onUndoRebased?.(dropped);
3574
3627
  port.markDirty();
3575
3628
  };
@@ -3607,6 +3660,9 @@ function createSheetsOtAdapter(port, hooks = {}) {
3607
3660
  stacks?.undo.delete(t.unitId);
3608
3661
  stacks?.redo.delete(t.unitId);
3609
3662
  }
3663
+ pending = [];
3664
+ pendingBase = applied;
3665
+ currentAt = /* @__PURE__ */ new WeakMap();
3610
3666
  },
3611
3667
  snapshot: () => port.snapshot(),
3612
3668
  async hash() {
@@ -3693,7 +3749,9 @@ function openSheetsOtRoom(o) {
3693
3749
  ...o.presence ? { presence: o.presence } : {},
3694
3750
  onNotice: (n) => notice(n),
3695
3751
  ...o.onIncident ? { onIncident: o.onIncident } : {},
3696
- ...o.onStatus ? { onStatus: o.onStatus } : {}
3752
+ ...o.onStatus ? { onStatus: o.onStatus } : {},
3753
+ ...o.offline ? { offline: o.offline } : {},
3754
+ ...o.onOffline ? { onOffline: o.onOffline } : {}
3697
3755
  });
3698
3756
  const isSaver = () => {
3699
3757
  if (!o.presence) return true;
@@ -3724,6 +3782,65 @@ function openSheetsOtRoom(o) {
3724
3782
  };
3725
3783
  }
3726
3784
 
3785
+ // src/univer/collab/sheets-ot/plugin-teardown.ts
3786
+ var perSheetList = (data) => Object.entries(data).flatMap(([sheet, list]) => Array.isArray(list) ? [[sheet, list.filter((x) => !!x && typeof x === "object")]] : []);
3787
+ var PLUGIN_TEARDOWNS = [
3788
+ {
3789
+ resource: "SHEET_CONDITIONAL_FORMATTING_PLUGIN",
3790
+ removals: (unitId, data) => perSheetList(data).flatMap(
3791
+ ([subUnitId, rules]) => rules.filter((r) => typeof r.cfId === "string").map((r) => ({ id: "sheet.mutation.delete-conditional-rule", params: { unitId, subUnitId, cfId: r.cfId } }))
3792
+ )
3793
+ },
3794
+ {
3795
+ resource: "SHEET_DATA_VALIDATION_PLUGIN",
3796
+ removals: (unitId, data) => perSheetList(data).flatMap(([subUnitId, rules]) => {
3797
+ const ids = rules.map((r) => r.uid).filter((u) => typeof u === "string");
3798
+ return ids.length ? [{ id: "data-validation.mutation.removeRule", params: { unitId, subUnitId, ruleId: ids, source: "command" } }] : [];
3799
+ })
3800
+ },
3801
+ {
3802
+ resource: "SHEET_NOTE_PLUGIN",
3803
+ removals: (unitId, data) => Object.entries(data).flatMap(
3804
+ ([sheetId, rows]) => Object.entries(rows ?? {}).flatMap(
3805
+ ([row, cols]) => Object.entries(cols ?? {}).map(([col, note]) => ({
3806
+ id: "sheet.mutation.remove-note",
3807
+ params: { unitId, sheetId, row: Number(row), col: Number(col), ...note && typeof note.id === "string" ? { noteId: note.id } : {}, silent: true }
3808
+ }))
3809
+ )
3810
+ )
3811
+ }
3812
+ ];
3813
+ function resourcesOf(saved) {
3814
+ const out = /* @__PURE__ */ new Map();
3815
+ const list = saved?.resources;
3816
+ for (const r of Array.isArray(list) ? list : []) {
3817
+ if (!r?.name) continue;
3818
+ try {
3819
+ const data = typeof r.data === "string" ? r.data ? JSON.parse(r.data) : null : r.data;
3820
+ if (data && typeof data === "object") out.set(r.name, data);
3821
+ } catch {
3822
+ }
3823
+ }
3824
+ return out;
3825
+ }
3826
+ function releaseUnitPluginState(cs, unitId, saved) {
3827
+ const resources = resourcesOf(saved);
3828
+ let ran = 0;
3829
+ for (const t of PLUGIN_TEARDOWNS) {
3830
+ const data = resources.get(t.resource);
3831
+ if (!data) continue;
3832
+ for (const m of t.removals(unitId, data)) {
3833
+ try {
3834
+ cs.syncExecuteCommand(m.id, m.params, { onlyLocal: true });
3835
+ ran++;
3836
+ } catch (err) {
3837
+ console.warn(`[sheets-ot] could not release ${t.resource} state before a reload`, err);
3838
+ }
3839
+ }
3840
+ }
3841
+ return ran;
3842
+ }
3843
+
3727
3844
  // src/univer/collab/sheets-ot/univer-port.ts
3728
3845
  var COMMAND = 0;
3729
3846
  var MUTATION = 2;
@@ -3733,6 +3850,8 @@ var REMOVE_MERGE = "sheet.mutation.remove-worksheet-merge";
3733
3850
  var SHEET_NAME = "sheet.mutation.set-worksheet-name";
3734
3851
  var INSERT_SHEET = "sheet.mutation.insert-sheet";
3735
3852
  var RESHAPE = /* @__PURE__ */ new Set(["sheet.mutation.set-worksheet-row-count", "sheet.mutation.set-worksheet-column-count", "sheet.mutation.remove-sheet", INSERT_SHEET]);
3853
+ var UNDO = "univer.command.undo";
3854
+ var REDO = "univer.command.redo";
3736
3855
  var DEPTH_GUARD = /* @__PURE__ */ Symbol.for("ai-matrx.office.sheets.command-depth-guard");
3737
3856
  var json = (v) => v === void 0 ? v : JSON.parse(JSON.stringify(v));
3738
3857
  var flagged = (options) => {
@@ -3742,6 +3861,7 @@ var flagged = (options) => {
3742
3861
  function univerSheetsOtPort(o) {
3743
3862
  const cs = o.commandService;
3744
3863
  const listeners = /* @__PURE__ */ new Set();
3864
+ const undoWatchers = /* @__PURE__ */ new Set();
3745
3865
  const idle = [];
3746
3866
  const pre = /* @__PURE__ */ new WeakMap();
3747
3867
  const tracker = { stack: [], settle: () => settle() };
@@ -3798,6 +3918,7 @@ function univerSheetsOtPort(o) {
3798
3918
  if (tracker.stack.length === 0 && !group) for (const fn of idle.splice(0)) fn();
3799
3919
  }
3800
3920
  const before = cs.beforeCommandExecuted((info, options) => {
3921
+ if (info.id === UNDO || info.id === REDO) for (const fn of [...undoWatchers]) fn();
3801
3922
  if (info.type === COMMAND) {
3802
3923
  if (tracker.stack.length === 0 && !group) group = openGroup(info.id);
3803
3924
  tracker.stack.push(info.id);
@@ -4043,6 +4164,8 @@ function univerSheetsOtPort(o) {
4043
4164
  group = null;
4044
4165
  applying = true;
4045
4166
  try {
4167
+ const u = unitId();
4168
+ if (u) releaseUnitPluginState(cs, u, o.saveUnit?.(u) ?? null);
4046
4169
  o.load(snapshot);
4047
4170
  } finally {
4048
4171
  applying = false;
@@ -4053,8 +4176,13 @@ function univerSheetsOtPort(o) {
4053
4176
  const service = o.undoRedo?.();
4054
4177
  return u && service ? [{ service, unitId: u }] : [];
4055
4178
  },
4179
+ beforeUndo(fn) {
4180
+ undoWatchers.add(fn);
4181
+ return () => void undoWatchers.delete(fn);
4182
+ },
4056
4183
  markDirty: () => o.markDirty(),
4057
4184
  dispose() {
4185
+ undoWatchers.clear();
4058
4186
  before.dispose();
4059
4187
  after.dispose();
4060
4188
  collab.dispose();
@@ -4132,6 +4260,53 @@ function moveSelectionsThrough(selections, m, maxRows, maxCols) {
4132
4260
  return selections.map((s) => ({ ...s, range: box(s.range, s.range.rangeType), ...s.primary ? { primary: box(s.primary) } : {} }));
4133
4261
  }
4134
4262
 
4263
+ // src/univer/collab/offline-text.ts
4264
+ var colName3 = (c) => {
4265
+ let n = c;
4266
+ let s = "";
4267
+ do {
4268
+ s = String.fromCharCode(65 + n % 26) + s;
4269
+ n = Math.floor(n / 26) - 1;
4270
+ } while (n >= 0);
4271
+ return s;
4272
+ };
4273
+ function collectDocText(v, out) {
4274
+ if (Array.isArray(v)) {
4275
+ for (const x of v) collectDocText(x, out);
4276
+ return;
4277
+ }
4278
+ if (!v || typeof v !== "object") return;
4279
+ const o = v;
4280
+ if (typeof o.dataStream === "string" && o.dataStream.length) out.push(o.dataStream);
4281
+ for (const k of Object.keys(o)) if (k !== "dataStream") collectDocText(o[k], out);
4282
+ }
4283
+ function collectCells(cellValue, out) {
4284
+ if (!cellValue || typeof cellValue !== "object") return;
4285
+ for (const [r, cols] of Object.entries(cellValue)) {
4286
+ if (!cols || typeof cols !== "object") continue;
4287
+ for (const [c, cell] of Object.entries(cols)) {
4288
+ const x = cell;
4289
+ const val = x?.f ?? x?.v;
4290
+ if (val !== void 0 && val !== null && val !== "") out.push(`${colName3(Number(c))}${Number(r) + 1}: ${String(val)}`);
4291
+ }
4292
+ }
4293
+ }
4294
+ function offlineTextOfGroups(buffers) {
4295
+ const docs = [];
4296
+ const cells = [];
4297
+ for (const groups of buffers) {
4298
+ for (const g of groups) {
4299
+ for (const m of g.mutations) {
4300
+ const p = m.params;
4301
+ if (p && "cellValue" in p) collectCells(p.cellValue, cells);
4302
+ else collectDocText(p, docs);
4303
+ }
4304
+ }
4305
+ }
4306
+ const text = docs.join("").replace(/\r/g, "\n").replace(/\u0000/g, "");
4307
+ return [text, cells.join("\n")].filter((s) => s.length > 0).join("\n");
4308
+ }
4309
+
4135
4310
  // src/sheets/workbook-ot-room.ts
4136
4311
  var Y3 = __toESM(require("yjs"), 1);
4137
4312
  var import_awareness3 = require("y-protocols/awareness");
@@ -4234,6 +4409,7 @@ async function startWorkbookOtRoom(port, o) {
4234
4409
  await provider.connect();
4235
4410
  publishAwareness();
4236
4411
  const client = db4();
4412
+ const offline = (0, import_collab6.browserOfflineOptions)(`${o.workbookId}:${o.uid}`);
4237
4413
  const room = openSheetsOtRoom({
4238
4414
  port,
4239
4415
  clientId: o.clientId,
@@ -4254,7 +4430,9 @@ async function startWorkbookOtRoom(port, o) {
4254
4430
  replace: ({ snapshot, stateHash, clientId }) => (0, import_collab6.collabReplace)(client, { roomId: o.workbookId, snapshot, stateHash, origin: "restore", clientId }),
4255
4431
  ...o.onNotice ? { onNotice: o.onNotice } : {},
4256
4432
  ...o.onIncident ? { onIncident: o.onIncident } : {},
4257
- ...o.onStatus ? { onStatus: o.onStatus } : {}
4433
+ ...o.onStatus ? { onStatus: o.onStatus } : {},
4434
+ ...offline ? { offline } : {},
4435
+ ...o.onOffline ? { onOffline: o.onOffline } : {}
4258
4436
  });
4259
4437
  await room.session.ready;
4260
4438
  return {