y-mxgraph 0.8.5 → 0.9.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/y-mxgraph.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const transform = require("./index-D3Hk2QcW.cjs");
3
+ const transform = require("./index-CqD1uLD3.cjs");
4
4
  const Y = require("yjs");
5
5
  const colord = require("colord");
6
6
  function _interopNamespaceDefault(e) {
@@ -93,9 +93,15 @@ const DIFF_UPDATE = "u";
93
93
  const docSnapshots = /* @__PURE__ */ new WeakMap();
94
94
  function insertAfterUnique(orderArr, id, previous, fallbackToEnd = false) {
95
95
  const currentIds = orderArr.toArray();
96
- let anchorPos = previous ? currentIds.indexOf(previous) : -1;
96
+ let anchorPos = previous != null ? currentIds.indexOf(previous) : -1;
97
97
  if (anchorPos === -1 && fallbackToEnd) anchorPos = currentIds.length - 1;
98
98
  let targetIndex = anchorPos + 1;
99
+ if (previous === "" && id !== "0" && id !== "1") {
100
+ const layerIndex = currentIds.indexOf("1");
101
+ if (layerIndex >= 0) {
102
+ targetIndex = layerIndex + 1;
103
+ }
104
+ }
99
105
  const existingIndex = currentIds.indexOf(id);
100
106
  if (existingIndex === -1) {
101
107
  orderArr.insert(targetIndex, [id]);
@@ -150,34 +156,41 @@ function pruneEmptyPatch(patch) {
150
156
  }
151
157
  function applyFilePatch(doc, patch, options) {
152
158
  doc.transact(() => {
159
+ var _a, _b;
153
160
  const mxfile = doc.getMap(transform.key$1);
154
161
  if (patch[DIFF_REMOVE]) {
155
162
  const diagramsMap = mxfile.get(transform.key$2);
156
- const orderArr = mxfile.get(
157
- transform.diagramOrderKey
158
- );
159
- ensureUniqueOrder(orderArr);
160
- const orderList = orderArr.toArray();
163
+ const orderArr = mxfile.get(transform.diagramOrderKey);
164
+ if (orderArr) ensureUniqueOrder(orderArr);
161
165
  const removeIds = patch[DIFF_REMOVE];
162
166
  if (removeIds && removeIds.length) {
163
- const indexList = removeIds.map((id) => orderList.indexOf(id)).filter((i) => i !== -1).sort((a, b) => b - a);
164
- indexList.forEach((idx) => orderArr.delete(idx, 1));
165
- removeIds.forEach((id) => diagramsMap.delete(id));
167
+ if (orderArr) {
168
+ const orderList = orderArr.toArray();
169
+ const indexList = removeIds.map((id) => orderList.indexOf(id)).filter((i) => i !== -1).sort((a, b) => b - a);
170
+ indexList.forEach((idx) => orderArr.delete(idx, 1));
171
+ }
172
+ if (diagramsMap) {
173
+ removeIds.forEach((id) => {
174
+ if (diagramsMap.has(id)) {
175
+ diagramsMap.delete(id);
176
+ }
177
+ });
178
+ }
166
179
  }
167
180
  }
168
181
  if (patch[DIFF_INSERT]) {
169
182
  const diagramsMap = mxfile.get(transform.key$2);
170
- const orderArr = mxfile.get(
171
- transform.diagramOrderKey
172
- );
173
- ensureUniqueOrder(orderArr);
174
- const currentOrder = orderArr.toArray();
175
- if (currentOrder.length === 0 && diagramsMap && diagramsMap.size > 0) {
176
- const allIds = Array.from(diagramsMap.keys());
177
- orderArr.push(allIds);
183
+ const orderArr = mxfile.get(transform.diagramOrderKey);
184
+ if (orderArr) {
185
+ ensureUniqueOrder(orderArr);
186
+ const currentOrder = orderArr.toArray();
187
+ if (currentOrder.length === 0 && diagramsMap && diagramsMap.size > 0) {
188
+ const allIds = Array.from(diagramsMap.keys());
189
+ orderArr.push(allIds);
190
+ }
191
+ ensureUniqueOrder(orderArr);
178
192
  }
179
- ensureUniqueOrder(orderArr);
180
- const existingIds = orderArr.toArray();
193
+ const existingIds = (_a = orderArr == null ? void 0 : orderArr.toArray()) != null ? _a : [];
181
194
  const existingIndex = /* @__PURE__ */ new Map();
182
195
  existingIds.forEach((id, idx) => existingIndex.set(id, idx));
183
196
  const inserts = patch[DIFF_INSERT].map((item, order) => {
@@ -188,7 +201,7 @@ function applyFilePatch(doc, patch, options) {
188
201
  );
189
202
  return {
190
203
  id: item.id,
191
- previous: item.previous || "",
204
+ previous: item.previous === void 0 ? null : item.previous,
192
205
  diagramElement,
193
206
  order
194
207
  };
@@ -230,16 +243,19 @@ function applyFilePatch(doc, patch, options) {
230
243
  return b.order - a.order;
231
244
  });
232
245
  for (const item of enriched) {
233
- diagramsMap.set(item.id, item.diagramElement);
234
- insertAfterUnique(orderArr, item.id, item.anchorId || null);
246
+ if (diagramsMap) {
247
+ diagramsMap.set(item.id, item.diagramElement);
248
+ }
249
+ if (orderArr) {
250
+ const anchorArg = item.anchorId === "" ? "" : (_b = item.anchorId) != null ? _b : null;
251
+ insertAfterUnique(orderArr, item.id, anchorArg);
252
+ }
235
253
  }
236
254
  }
237
255
  if (patch[DIFF_UPDATE]) {
238
256
  Object.keys(patch[DIFF_UPDATE]).forEach((id) => {
239
- const diagramsMap = mxfile.get(
240
- transform.key$2
241
- );
242
- const diagram = diagramsMap.get(id);
257
+ const diagramsMap = mxfile.get(transform.key$2);
258
+ const diagram = diagramsMap == null ? void 0 : diagramsMap.get(id);
243
259
  if (diagram) {
244
260
  const update = patch[DIFF_UPDATE][id];
245
261
  if ("name" in update) {
@@ -253,132 +269,151 @@ function applyFilePatch(doc, patch, options) {
253
269
  }
254
270
  if (update.cells) {
255
271
  const yMxGraphModel = diagram.get(transform.key);
256
- if (!yMxGraphModel) return;
272
+ if (!yMxGraphModel) {
273
+ console.warn(
274
+ "[y-mxgraph] applyFilePatch: yMxGraphModel not found for diagram, skipping cells update"
275
+ );
276
+ return;
277
+ }
257
278
  const cellsMap = yMxGraphModel.get(transform.key$3);
258
279
  const orderArr = yMxGraphModel.get(transform.mxCellOrderKey);
259
- if (!cellsMap || !orderArr) return;
260
- ensureUniqueOrder(orderArr);
280
+ if (!cellsMap && !orderArr) {
281
+ console.warn(
282
+ "[y-mxgraph] applyFilePatch: both cellsMap and orderArr missing, skipping cells update"
283
+ );
284
+ return;
285
+ }
286
+ if (orderArr) ensureUniqueOrder(orderArr);
261
287
  if (update.cells[DIFF_REMOVE] && update.cells[DIFF_REMOVE].length) {
262
- const orderIds = orderArr.toArray();
263
- const removeIndexList = update.cells[DIFF_REMOVE].map(
264
- (cid) => orderIds.indexOf(cid)
265
- ).filter((i) => i !== -1).sort((a, b) => b - a);
266
- removeIndexList.forEach((idx) => orderArr.delete(idx, 1));
267
- update.cells[DIFF_REMOVE].forEach((cid) => cellsMap.delete(cid));
288
+ if (orderArr) {
289
+ const orderIds = orderArr.toArray();
290
+ const removeIndexList = update.cells[DIFF_REMOVE].map(
291
+ (cid) => orderIds.indexOf(cid)
292
+ ).filter((i) => i !== -1).sort((a, b) => b - a);
293
+ removeIndexList.forEach((idx) => orderArr.delete(idx, 1));
294
+ }
295
+ if (cellsMap) {
296
+ update.cells[DIFF_REMOVE].forEach((cid) => {
297
+ if (cellsMap.has(cid)) {
298
+ cellsMap.delete(cid);
299
+ }
300
+ });
301
+ }
268
302
  }
269
303
  if (update.cells[DIFF_INSERT] && update.cells[DIFF_INSERT].length) {
270
304
  for (const item of update.cells[DIFF_INSERT]) {
271
305
  const id2 = item["id"];
272
306
  if (!id2) continue;
273
- const xmlElement = new Y__namespace.XmlElement("mxCell");
274
- Object.keys(item).forEach((key) => {
275
- if (key === "previous") return;
276
- xmlElement.setAttribute(key, item[key]);
277
- });
278
- cellsMap.set(id2, xmlElement);
279
- const previous = item["previous"];
280
- const parent = item["parent"];
281
- let anchorId = null;
282
- let fallbackToEnd = true;
283
- if (typeof previous !== "undefined") {
284
- if (previous === "") {
285
- if (parent) {
286
- anchorId = parent;
287
- fallbackToEnd = true;
288
- } else {
289
- anchorId = null;
307
+ if (cellsMap) {
308
+ const xmlElement = new Y__namespace.XmlElement("mxCell");
309
+ Object.keys(item).forEach((key) => {
310
+ if (key === "previous") return;
311
+ xmlElement.setAttribute(key, item[key]);
312
+ });
313
+ cellsMap.set(id2, xmlElement);
314
+ }
315
+ if (orderArr) {
316
+ const previous = item["previous"];
317
+ const parent = item["parent"];
318
+ let anchorId = null;
319
+ let fallbackToEnd = true;
320
+ if (typeof previous !== "undefined") {
321
+ if (previous === "") {
322
+ anchorId = "";
290
323
  fallbackToEnd = false;
324
+ } else {
325
+ anchorId = previous;
326
+ fallbackToEnd = true;
291
327
  }
292
- } else {
293
- anchorId = previous;
328
+ } else if (parent) {
329
+ anchorId = parent;
294
330
  fallbackToEnd = true;
295
331
  }
296
- } else if (parent) {
297
- anchorId = parent;
298
- fallbackToEnd = true;
332
+ insertAfterUnique(
333
+ orderArr,
334
+ id2,
335
+ anchorId,
336
+ fallbackToEnd
337
+ );
299
338
  }
300
- insertAfterUnique(
301
- orderArr,
302
- id2,
303
- anchorId,
304
- fallbackToEnd
305
- );
306
339
  }
307
340
  }
308
341
  if (update.cells[DIFF_UPDATE]) {
309
- Object.keys(update.cells[DIFF_UPDATE]).forEach((cid) => {
310
- const updateObj = update.cells[DIFF_UPDATE][cid];
311
- const cell = cellsMap.get(cid);
312
- if (cell) {
313
- Object.keys(updateObj).forEach((k) => {
314
- if (k === "previous") return;
315
- cell.setAttribute(k, updateObj[k]);
316
- });
317
- }
318
- });
319
- Object.keys(update.cells[DIFF_UPDATE]).forEach((cellId) => {
320
- const updateObj = update.cells[DIFF_UPDATE][cellId];
321
- const hasPrev = "previous" in updateObj;
322
- const hasParent = "parent" in updateObj;
323
- if (!hasPrev && !hasParent) return;
324
- const prevVal = hasPrev ? updateObj.previous : void 0;
325
- const parentVal = hasParent ? updateObj.parent : void 0;
326
- let anchorId = null;
327
- let fallbackToEnd = true;
328
- if (hasPrev) {
329
- if (prevVal === "") {
330
- if (parentVal) {
331
- anchorId = parentVal;
342
+ if (cellsMap) {
343
+ Object.keys(update.cells[DIFF_UPDATE]).forEach((cid) => {
344
+ const updateObj = update.cells[DIFF_UPDATE][cid];
345
+ const cell = cellsMap.get(cid);
346
+ if (cell) {
347
+ Object.keys(updateObj).forEach((k) => {
348
+ if (k === "previous") return;
349
+ cell.setAttribute(k, updateObj[k]);
350
+ });
351
+ } else {
352
+ console.warn(
353
+ `[y-mxgraph] applyFilePatch: cell ${cid} not found in cellsMap, skipping update`
354
+ );
355
+ }
356
+ });
357
+ }
358
+ if (cellsMap && orderArr) {
359
+ const reorderEntries = Object.keys(update.cells[DIFF_UPDATE]).map((cellId) => {
360
+ const updateObj = update.cells[DIFF_UPDATE][cellId];
361
+ const hasPrev = "previous" in updateObj;
362
+ const hasParent = "parent" in updateObj;
363
+ if (!hasPrev && !hasParent) return null;
364
+ const prevVal = hasPrev ? updateObj.previous : void 0;
365
+ const parentVal = hasParent ? updateObj.parent : void 0;
366
+ let anchorId = null;
367
+ let fallbackToEnd = true;
368
+ if (hasPrev) {
369
+ if (prevVal === "") {
370
+ anchorId = "";
371
+ fallbackToEnd = false;
372
+ } else if (prevVal === null || typeof prevVal === "undefined") {
373
+ anchorId = null;
332
374
  fallbackToEnd = true;
333
375
  } else {
334
- anchorId = null;
335
- fallbackToEnd = false;
376
+ anchorId = prevVal;
377
+ fallbackToEnd = true;
336
378
  }
337
- } else {
338
- anchorId = prevVal;
379
+ } else if (parentVal) {
380
+ anchorId = parentVal;
339
381
  fallbackToEnd = true;
340
382
  }
341
- } else if (parentVal) {
342
- anchorId = parentVal;
343
- fallbackToEnd = true;
344
- }
345
- const currentIds = orderArr.toArray();
346
- const currentIndex = currentIds.indexOf(cellId);
347
- if (currentIndex === -1) {
348
- let newCell = cellsMap.get(cellId);
349
- if (!newCell) {
350
- newCell = new Y__namespace.XmlElement("mxCell");
351
- newCell.setAttribute("id", cellId);
352
- Object.keys(updateObj).forEach((k) => {
353
- if (k === "previous") return;
354
- newCell.setAttribute(k, updateObj[k]);
355
- });
356
- cellsMap.set(cellId, newCell);
383
+ return { cellId, anchorId, fallbackToEnd, updateObj };
384
+ }).filter((e) => e !== null);
385
+ const applyReorder = (entry) => {
386
+ const { cellId, anchorId, fallbackToEnd, updateObj } = entry;
387
+ const currentIds = orderArr.toArray();
388
+ const currentIndex = currentIds.indexOf(cellId);
389
+ if (currentIndex === -1) {
390
+ let newCell = cellsMap.get(cellId);
391
+ if (!newCell) {
392
+ newCell = new Y__namespace.XmlElement("mxCell");
393
+ newCell.setAttribute("id", cellId);
394
+ Object.keys(updateObj).forEach((k) => {
395
+ if (k === "previous") return;
396
+ newCell.setAttribute(k, updateObj[k]);
397
+ });
398
+ cellsMap.set(cellId, newCell);
399
+ }
400
+ insertAfterUnique(orderArr, cellId, anchorId, fallbackToEnd);
401
+ return;
357
402
  }
358
- insertAfterUnique(
359
- orderArr,
360
- cellId,
361
- anchorId,
362
- fallbackToEnd
363
- );
364
- return;
365
- }
366
- insertAfterUnique(
367
- orderArr,
368
- cellId,
369
- anchorId,
370
- fallbackToEnd
371
- );
372
- });
403
+ insertAfterUnique(orderArr, cellId, anchorId, fallbackToEnd);
404
+ };
405
+ reorderEntries.filter((e) => e.anchorId === "" || e.anchorId === null).forEach(applyReorder);
406
+ reorderEntries.filter((e) => e.anchorId !== "" && e.anchorId !== null).forEach(applyReorder);
407
+ }
373
408
  }
374
409
  }
375
410
  if ("previous" in update) {
376
- const previous = update.previous || null;
377
- const orderArr = mxfile.get(
378
- transform.diagramOrderKey
379
- );
380
- ensureUniqueOrder(orderArr);
381
- insertAfterUnique(orderArr, id, previous, false);
411
+ const previous = Object.prototype.hasOwnProperty.call(update, "previous") ? update.previous : null;
412
+ const orderArr = mxfile.get(transform.diagramOrderKey);
413
+ if (orderArr) {
414
+ ensureUniqueOrder(orderArr);
415
+ insertAfterUnique(orderArr, id, previous, false);
416
+ }
382
417
  }
383
418
  }
384
419
  });
@@ -399,7 +434,7 @@ function initDocSnapshot(doc, resetSnapshot = false) {
399
434
  cellAttrs: /* @__PURE__ */ new Map(),
400
435
  diagramBackground: /* @__PURE__ */ new Map()
401
436
  };
402
- const diagrams = diagramOrder.map((id) => diagramsMap.get(id)).filter((d) => !!d);
437
+ const diagrams = diagramOrder.map((id) => diagramsMap == null ? void 0 : diagramsMap.get(id)).filter((d) => !!d);
403
438
  for (const d of diagrams) {
404
439
  const did = d.get("id") || "";
405
440
  if (!did) continue;
@@ -434,7 +469,7 @@ function initDocSnapshot(doc, resetSnapshot = false) {
434
469
  }
435
470
  }
436
471
  function generatePatch(events, explicitDoc) {
437
- var _a, _b, _c, _d, _e;
472
+ var _a, _b, _c, _d, _e, _f;
438
473
  const patch = {};
439
474
  const doc = (_b = (_a = events[0]) == null ? void 0 : _a.transaction) == null ? void 0 : _b.doc;
440
475
  if (!doc) return patch;
@@ -468,7 +503,7 @@ function generatePatch(events, explicitDoc) {
468
503
  };
469
504
  const orderIds = (_c = orderArr == null ? void 0 : orderArr.toArray()) != null ? _c : [];
470
505
  const currDiagramOrder = orderIds.length > 0 ? orderIds : diagramsMap ? Array.from(diagramsMap.keys()) : [];
471
- const diagramsList = currDiagramOrder.map((id) => diagramsMap.get(id)).filter((d) => !!d);
506
+ const diagramsList = currDiagramOrder.map((id) => diagramsMap == null ? void 0 : diagramsMap.get(id)).filter((d) => !!d);
472
507
  const currCellsOrder = /* @__PURE__ */ new Map();
473
508
  const cellAttrMap = /* @__PURE__ */ new Map();
474
509
  const currDiagramBackground = /* @__PURE__ */ new Map();
@@ -514,7 +549,7 @@ function generatePatch(events, explicitDoc) {
514
549
  for (const id of inserted) {
515
550
  const index = currDiagramOrder.indexOf(id);
516
551
  const previous = index <= 0 ? "" : currDiagramOrder[index - 1];
517
- const yDiagram = diagramsMap.get(id);
552
+ const yDiagram = diagramsMap == null ? void 0 : diagramsMap.get(id);
518
553
  if (!yDiagram) continue;
519
554
  const data = transform.serializer({ diagram: transform.serialize(yDiagram) });
520
555
  patch[DIFF_INSERT].push({ id, previous, data });
@@ -523,7 +558,8 @@ function generatePatch(events, explicitDoc) {
523
558
  }
524
559
  const prevNeighbor = (order, id) => {
525
560
  const i = order.indexOf(id);
526
- return i <= 0 ? "" : order[i - 1];
561
+ if (i === -1) return null;
562
+ return i === 0 ? "" : order[i - 1];
527
563
  };
528
564
  const common = currDiagramOrder.filter((id) => prevSet.has(id) && id);
529
565
  for (const id of common) {
@@ -572,7 +608,8 @@ function generatePatch(events, explicitDoc) {
572
608
  }
573
609
  const prevNeighbor = (order, id) => {
574
610
  const i = order.indexOf(id);
575
- return i <= 0 ? "" : order[i - 1];
611
+ if (i === -1) return null;
612
+ return i === 0 ? "" : order[i - 1];
576
613
  };
577
614
  const commonCells = currCells.filter((cid) => prevSet.has(cid) && cid);
578
615
  for (const cid of commonCells) {
@@ -649,7 +686,7 @@ function generatePatch(events, explicitDoc) {
649
686
  cellsPatch[DIFF_UPDATE] = cellsPatch[DIFF_UPDATE] || {};
650
687
  const cellUpdate = cellsPatch[DIFF_UPDATE][cellId] = cellsPatch[DIFF_UPDATE][cellId] || {};
651
688
  for (const key of Array.from(changed)) {
652
- cellUpdate[key] = el.getAttribute(key) || "";
689
+ cellUpdate[key] = (_d = el.getAttribute(key)) != null ? _d : "";
653
690
  }
654
691
  }
655
692
  if (prevDiagramOrder) {
@@ -678,8 +715,8 @@ function generatePatch(events, explicitDoc) {
678
715
  const cellUpdate = updateBucket[cid] = updateBucket[cid] || {};
679
716
  let changed = false;
680
717
  for (const k of keys) {
681
- const pv = (_d = prevAttrs[k]) != null ? _d : "";
682
- const cv = (_e = currAttrs[k]) != null ? _e : "";
718
+ const pv = (_e = prevAttrs[k]) != null ? _e : "";
719
+ const cv = (_f = currAttrs[k]) != null ? _f : "";
683
720
  if (pv !== cv) {
684
721
  cellUpdate[k] = cv;
685
722
  changed = true;
@@ -1365,6 +1402,91 @@ function bindCollaborator(file, options) {
1365
1402
  cleanupAwareness == null ? void 0 : cleanupAwareness();
1366
1403
  };
1367
1404
  }
1405
+ function checkConsistency(doc, fileData) {
1406
+ try {
1407
+ const xmlFromYdoc = transform.ydoc2xml(doc);
1408
+ if (xmlFromYdoc === fileData) return true;
1409
+ const diagramCountFromYdoc = (xmlFromYdoc.match(/<diagram /g) || []).length;
1410
+ const diagramCountFromFile = (fileData.match(/<diagram /g) || []).length;
1411
+ if (diagramCountFromYdoc !== diagramCountFromFile) return false;
1412
+ const cellCountFromYdoc = (xmlFromYdoc.match(/<mxCell /g) || []).length;
1413
+ const cellCountFromFile = (fileData.match(/<mxCell /g) || []).length;
1414
+ if (cellCountFromYdoc !== cellCountFromFile) return false;
1415
+ return true;
1416
+ } catch (e) {
1417
+ return false;
1418
+ }
1419
+ }
1420
+ class ConsistencyChecker {
1421
+ constructor(doc, getFileData, options = {}) {
1422
+ this.doc = doc;
1423
+ this.getFileData = getFileData;
1424
+ this.intervalId = null;
1425
+ this.handlers = /* @__PURE__ */ new Set();
1426
+ this.consecutiveDriftCount = 0;
1427
+ var _a, _b;
1428
+ this.source = (_a = options.source) != null ? _a : "binding";
1429
+ this.maxAutoFixAttempts = (_b = options.maxAutoFixAttempts) != null ? _b : 3;
1430
+ }
1431
+ /** 注册 drift 事件监听 */
1432
+ onDrift(handler) {
1433
+ this.handlers.add(handler);
1434
+ return () => this.handlers.delete(handler);
1435
+ }
1436
+ /** 启动定期检查(毫秒间隔),0 或负数表示禁用 */
1437
+ start(intervalMs) {
1438
+ this.stop();
1439
+ if (intervalMs <= 0) return;
1440
+ this.intervalId = setInterval(() => {
1441
+ this.check();
1442
+ }, intervalMs);
1443
+ }
1444
+ /** 停止定期检查 */
1445
+ stop() {
1446
+ if (this.intervalId != null) {
1447
+ clearInterval(this.intervalId);
1448
+ this.intervalId = null;
1449
+ }
1450
+ }
1451
+ /**
1452
+ * 执行一次一致性检查。
1453
+ * @returns true = 一致,false = 存在 drift
1454
+ */
1455
+ check() {
1456
+ const consistent = checkConsistency(this.doc, this.getFileData());
1457
+ if (consistent) {
1458
+ this.consecutiveDriftCount = 0;
1459
+ return true;
1460
+ }
1461
+ this.consecutiveDriftCount++;
1462
+ const event = {
1463
+ timestamp: Date.now(),
1464
+ source: this.source,
1465
+ direction: "unknown",
1466
+ details: `drift detected (consecutive #${this.consecutiveDriftCount})`
1467
+ };
1468
+ this.handlers.forEach((handler) => {
1469
+ try {
1470
+ handler(event);
1471
+ } catch (e) {
1472
+ console.warn("[y-mxgraph] drift handler error:", e);
1473
+ }
1474
+ });
1475
+ return false;
1476
+ }
1477
+ /** 重置连续 drift 计数(forceSync 成功后调用) */
1478
+ resetDriftCount() {
1479
+ this.consecutiveDriftCount = 0;
1480
+ }
1481
+ /** 是否已超过最大自动修复次数 */
1482
+ get shouldStopAutoFix() {
1483
+ return this.consecutiveDriftCount >= this.maxAutoFixAttempts;
1484
+ }
1485
+ destroy() {
1486
+ this.stop();
1487
+ this.handlers.clear();
1488
+ }
1489
+ }
1368
1490
  const defaultApplyFileData = (file, xml) => {
1369
1491
  file.ui.setFileData(xml);
1370
1492
  };
@@ -1476,6 +1598,7 @@ class Binding {
1476
1598
  this.suppressLocalApply = false;
1477
1599
  this.docInitialized = false;
1478
1600
  this.ui = null;
1601
+ this.debouncedForceSync = null;
1479
1602
  const {
1480
1603
  doc,
1481
1604
  awareness,
@@ -1485,12 +1608,15 @@ class Binding {
1485
1608
  initialContent = "replace",
1486
1609
  applyFileData = defaultApplyFileData,
1487
1610
  disableBeforeUnload = true,
1488
- transformPatch
1611
+ transformPatch,
1612
+ syncOnOrderMismatch = false
1489
1613
  } = options;
1490
1614
  this.doc = doc;
1491
1615
  this.file = file;
1492
1616
  this.initialContentStrategy = initialContent;
1493
1617
  this.transformPatch = transformPatch;
1618
+ this.applyFileData = applyFileData;
1619
+ this.syncOnOrderMismatch = syncOnOrderMismatch;
1494
1620
  const ui = file.getUi();
1495
1621
  const graph = ui.editor.graph;
1496
1622
  this.mxGraphModel = graph.model;
@@ -1541,7 +1667,6 @@ class Binding {
1541
1667
  this.mxGraphModel.addListener("change", this.mxListener);
1542
1668
  this.docObserver = (events, transaction) => {
1543
1669
  if (transaction.local && transaction.origin === LOCAL_ORIGIN) {
1544
- generatePatch(events);
1545
1670
  return;
1546
1671
  }
1547
1672
  if (this.shouldReplaceWhenDocHasData && !transaction.local) {
@@ -1568,7 +1693,8 @@ class Binding {
1568
1693
  this.docInitialized = true;
1569
1694
  }
1570
1695
  const patch = generatePatch(events);
1571
- if (Object.keys(patch).length === 0) return;
1696
+ const patchKeys = Object.keys(patch);
1697
+ if (patchKeys.length === 0) return;
1572
1698
  this.suppressLocalApply = true;
1573
1699
  try {
1574
1700
  file.patch([patch]);
@@ -1577,6 +1703,14 @@ class Binding {
1577
1703
  } finally {
1578
1704
  this.suppressLocalApply = false;
1579
1705
  }
1706
+ if (this.syncOnOrderMismatch && patch.u && this.checkOrderMismatch(patch)) {
1707
+ console.warn("[y-mxgraph] order mismatch detected, debounced forceSync (file → ydoc)");
1708
+ if (this.debouncedForceSync) clearTimeout(this.debouncedForceSync);
1709
+ this.debouncedForceSync = setTimeout(() => {
1710
+ this.debouncedForceSync = null;
1711
+ this.forceSync("file-to-ydoc");
1712
+ }, 300);
1713
+ }
1580
1714
  };
1581
1715
  doc.getMap(transform.key$1).observeDeep(this.docObserver);
1582
1716
  if (awareness) {
@@ -1590,6 +1724,29 @@ class Binding {
1590
1724
  if (undoManager) {
1591
1725
  this.cleanupUndoManager = bindUndoManager(doc, file, undoManager);
1592
1726
  }
1727
+ const {
1728
+ consistencyCheckInterval,
1729
+ onDrift
1730
+ } = options;
1731
+ if (consistencyCheckInterval && consistencyCheckInterval > 0) {
1732
+ this.consistencyChecker = new ConsistencyChecker(doc, () => file.data, {
1733
+ source: "binding"
1734
+ });
1735
+ if (onDrift) {
1736
+ this.consistencyChecker.onDrift(onDrift);
1737
+ }
1738
+ this.consistencyChecker.onDrift((event) => {
1739
+ var _a;
1740
+ if ((_a = this.consistencyChecker) == null ? void 0 : _a.shouldStopAutoFix) {
1741
+ console.warn(
1742
+ `[y-mxgraph] 连续 ${event.details} 次 drift,停止自动 forceSync`
1743
+ );
1744
+ return;
1745
+ }
1746
+ this.forceSync("ydoc-to-file");
1747
+ });
1748
+ this.consistencyChecker.start(consistencyCheckInterval);
1749
+ }
1593
1750
  }
1594
1751
  /** replace 策略下,构造时 doc 为空,现在 doc 有数据时需要强制替换 */
1595
1752
  get shouldReplaceWhenDocHasData() {
@@ -1606,17 +1763,83 @@ class Binding {
1606
1763
  this.ui.editor.setStatus("");
1607
1764
  (_a = this.ui.currentFile) == null ? void 0 : _a.setModified(false);
1608
1765
  }
1766
+ /**
1767
+ * 强制同步 ydoc 与 file,修复检测到的不一致。
1768
+ *
1769
+ * @param direction - 同步方向
1770
+ * - `ydoc-to-file`(默认):用 ydoc 数据覆盖 file
1771
+ * - `file-to-ydoc`:用 file 数据覆盖 ydoc
1772
+ */
1773
+ forceSync(direction = "ydoc-to-file") {
1774
+ var _a;
1775
+ if (direction === "ydoc-to-file") {
1776
+ const xml = transform.ydoc2xml(this.doc);
1777
+ if (!xml || !xml.includes("<diagram")) return;
1778
+ this.suppressLocalApply = true;
1779
+ try {
1780
+ this.applyFileData(this.file, xml);
1781
+ this.file.setShadowPages(this.file.ui.clonePages(this.file.ui.pages));
1782
+ initDocSnapshot(this.doc, false);
1783
+ this.resetEditorStatus();
1784
+ } finally {
1785
+ this.suppressLocalApply = false;
1786
+ }
1787
+ } else {
1788
+ this.doc.transact(() => {
1789
+ transform.xml2ydoc(this.file.data, this.doc);
1790
+ initDocSnapshot(this.doc, false);
1791
+ }, LOCAL_ORIGIN);
1792
+ }
1793
+ (_a = this.consistencyChecker) == null ? void 0 : _a.resetDriftCount();
1794
+ }
1795
+ checkOrderMismatch(patch) {
1796
+ var _a;
1797
+ if (!patch.u) return false;
1798
+ for (const [did, update] of Object.entries(patch.u)) {
1799
+ if (!((_a = update.cells) == null ? void 0 : _a.u)) continue;
1800
+ for (const [cid, cellUpdate] of Object.entries(update.cells.u)) {
1801
+ if (!("previous" in cellUpdate)) continue;
1802
+ const mxfile = this.doc.getMap(transform.key$1);
1803
+ const diagramsMap = mxfile.get(transform.key$2);
1804
+ const diagram = diagramsMap == null ? void 0 : diagramsMap.get(did);
1805
+ if (!diagram) continue;
1806
+ const gm = diagram.get(transform.key);
1807
+ if (!gm) continue;
1808
+ const orderArr = gm.get(transform.mxCellOrderKey);
1809
+ if (!orderArr) continue;
1810
+ const ids = orderArr.toArray();
1811
+ const idx = ids.indexOf(cid);
1812
+ if (idx === -1) continue;
1813
+ const expectedPrev = idx === 0 ? "" : ids[idx - 1];
1814
+ const actualPrev = cellUpdate.previous;
1815
+ if (expectedPrev !== actualPrev) {
1816
+ console.log(`[y-mxgraph] order mismatch: cell=${cid}, ydoc prev=${JSON.stringify(expectedPrev)}, patch prev=${JSON.stringify(actualPrev)}`);
1817
+ return true;
1818
+ }
1819
+ }
1820
+ }
1821
+ return false;
1822
+ }
1823
+ /**
1824
+ * 手动触发一次一致性检查。
1825
+ * @returns true = 一致,false = 存在 drift
1826
+ */
1827
+ checkConsistency() {
1828
+ if (!this.consistencyChecker) return true;
1829
+ return this.consistencyChecker.check();
1830
+ }
1609
1831
  /**
1610
1832
  * 销毁绑定,解除所有监听器
1611
1833
  * @param deep - 是否深度清理(包括 awareness/undoManager),默认 false
1612
1834
  */
1613
1835
  destroy(deep = false) {
1614
- var _a, _b;
1836
+ var _a, _b, _c;
1615
1837
  this.mxGraphModel.removeListener("change", this.mxListener);
1616
1838
  this.doc.getMap(transform.key$1).unobserveDeep(this.docObserver);
1839
+ (_a = this.consistencyChecker) == null ? void 0 : _a.destroy();
1617
1840
  if (deep) {
1618
- (_a = this.cleanupCollaborator) == null ? void 0 : _a.call(this);
1619
- (_b = this.cleanupUndoManager) == null ? void 0 : _b.call(this);
1841
+ (_b = this.cleanupCollaborator) == null ? void 0 : _b.call(this);
1842
+ (_c = this.cleanupUndoManager) == null ? void 0 : _c.call(this);
1620
1843
  }
1621
1844
  }
1622
1845
  /**