y-mxgraph 0.8.6 → 0.9.1

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.js CHANGED
@@ -1,4 +1,4 @@
1
- import { g as getMap, b as backgroundKey, k as key, a as key$1, c as key$2, d as diagramOrderKey, m as mxCellOrderKey, e as key$3, p as parse, f as parse$1, h as serializer, s as serialize, x as xml2ydoc, y as ydoc2xml } from "./index-BhW4J2Zt.js";
1
+ import { g as getMap, b as backgroundKey, k as key, a as key$1, c as key$2, d as diagramOrderKey, m as mxCellOrderKey, e as key$3, p as parse, f as parse$1, s as serializer, h as serialize, y as ydoc2xml, x as xml2ydoc } from "./index-CN7tFgEG.js";
2
2
  import * as Y from "yjs";
3
3
  import { colord } from "colord";
4
4
  function patchValueToXmlAttr(value) {
@@ -74,9 +74,15 @@ const DIFF_UPDATE = "u";
74
74
  const docSnapshots = /* @__PURE__ */ new WeakMap();
75
75
  function insertAfterUnique(orderArr, id, previous, fallbackToEnd = false) {
76
76
  const currentIds = orderArr.toArray();
77
- let anchorPos = previous ? currentIds.indexOf(previous) : -1;
77
+ let anchorPos = previous != null ? currentIds.indexOf(previous) : -1;
78
78
  if (anchorPos === -1 && fallbackToEnd) anchorPos = currentIds.length - 1;
79
79
  let targetIndex = anchorPos + 1;
80
+ if (previous === "" && id !== "0" && id !== "1") {
81
+ const layerIndex = currentIds.indexOf("1");
82
+ if (layerIndex >= 0) {
83
+ targetIndex = layerIndex + 1;
84
+ }
85
+ }
80
86
  const existingIndex = currentIds.indexOf(id);
81
87
  if (existingIndex === -1) {
82
88
  orderArr.insert(targetIndex, [id]);
@@ -131,34 +137,41 @@ function pruneEmptyPatch(patch) {
131
137
  }
132
138
  function applyFilePatch(doc, patch, options) {
133
139
  doc.transact(() => {
140
+ var _a, _b;
134
141
  const mxfile = doc.getMap(key$1);
135
142
  if (patch[DIFF_REMOVE]) {
136
143
  const diagramsMap = mxfile.get(key$2);
137
- const orderArr = mxfile.get(
138
- diagramOrderKey
139
- );
140
- ensureUniqueOrder(orderArr);
141
- const orderList = orderArr.toArray();
144
+ const orderArr = mxfile.get(diagramOrderKey);
145
+ if (orderArr) ensureUniqueOrder(orderArr);
142
146
  const removeIds = patch[DIFF_REMOVE];
143
147
  if (removeIds && removeIds.length) {
144
- const indexList = removeIds.map((id) => orderList.indexOf(id)).filter((i) => i !== -1).sort((a, b) => b - a);
145
- indexList.forEach((idx) => orderArr.delete(idx, 1));
146
- removeIds.forEach((id) => diagramsMap.delete(id));
148
+ if (orderArr) {
149
+ const orderList = orderArr.toArray();
150
+ const indexList = removeIds.map((id) => orderList.indexOf(id)).filter((i) => i !== -1).sort((a, b) => b - a);
151
+ indexList.forEach((idx) => orderArr.delete(idx, 1));
152
+ }
153
+ if (diagramsMap) {
154
+ removeIds.forEach((id) => {
155
+ if (diagramsMap.has(id)) {
156
+ diagramsMap.delete(id);
157
+ }
158
+ });
159
+ }
147
160
  }
148
161
  }
149
162
  if (patch[DIFF_INSERT]) {
150
163
  const diagramsMap = mxfile.get(key$2);
151
- const orderArr = mxfile.get(
152
- diagramOrderKey
153
- );
154
- ensureUniqueOrder(orderArr);
155
- const currentOrder = orderArr.toArray();
156
- if (currentOrder.length === 0 && diagramsMap && diagramsMap.size > 0) {
157
- const allIds = Array.from(diagramsMap.keys());
158
- orderArr.push(allIds);
164
+ const orderArr = mxfile.get(diagramOrderKey);
165
+ if (orderArr) {
166
+ ensureUniqueOrder(orderArr);
167
+ const currentOrder = orderArr.toArray();
168
+ if (currentOrder.length === 0 && diagramsMap && diagramsMap.size > 0) {
169
+ const allIds = Array.from(diagramsMap.keys());
170
+ orderArr.push(allIds);
171
+ }
172
+ ensureUniqueOrder(orderArr);
159
173
  }
160
- ensureUniqueOrder(orderArr);
161
- const existingIds = orderArr.toArray();
174
+ const existingIds = (_a = orderArr == null ? void 0 : orderArr.toArray()) != null ? _a : [];
162
175
  const existingIndex = /* @__PURE__ */ new Map();
163
176
  existingIds.forEach((id, idx) => existingIndex.set(id, idx));
164
177
  const inserts = patch[DIFF_INSERT].map((item, order) => {
@@ -169,7 +182,7 @@ function applyFilePatch(doc, patch, options) {
169
182
  );
170
183
  return {
171
184
  id: item.id,
172
- previous: item.previous || "",
185
+ previous: item.previous === void 0 ? null : item.previous,
173
186
  diagramElement,
174
187
  order
175
188
  };
@@ -211,16 +224,19 @@ function applyFilePatch(doc, patch, options) {
211
224
  return b.order - a.order;
212
225
  });
213
226
  for (const item of enriched) {
214
- diagramsMap.set(item.id, item.diagramElement);
215
- insertAfterUnique(orderArr, item.id, item.anchorId || null);
227
+ if (diagramsMap) {
228
+ diagramsMap.set(item.id, item.diagramElement);
229
+ }
230
+ if (orderArr) {
231
+ const anchorArg = item.anchorId === "" ? "" : (_b = item.anchorId) != null ? _b : null;
232
+ insertAfterUnique(orderArr, item.id, anchorArg);
233
+ }
216
234
  }
217
235
  }
218
236
  if (patch[DIFF_UPDATE]) {
219
237
  Object.keys(patch[DIFF_UPDATE]).forEach((id) => {
220
- const diagramsMap = mxfile.get(
221
- key$2
222
- );
223
- const diagram = diagramsMap.get(id);
238
+ const diagramsMap = mxfile.get(key$2);
239
+ const diagram = diagramsMap == null ? void 0 : diagramsMap.get(id);
224
240
  if (diagram) {
225
241
  const update = patch[DIFF_UPDATE][id];
226
242
  if ("name" in update) {
@@ -234,132 +250,151 @@ function applyFilePatch(doc, patch, options) {
234
250
  }
235
251
  if (update.cells) {
236
252
  const yMxGraphModel = diagram.get(key);
237
- if (!yMxGraphModel) return;
253
+ if (!yMxGraphModel) {
254
+ console.warn(
255
+ "[y-mxgraph] applyFilePatch: yMxGraphModel not found for diagram, skipping cells update"
256
+ );
257
+ return;
258
+ }
238
259
  const cellsMap = yMxGraphModel.get(key$3);
239
260
  const orderArr = yMxGraphModel.get(mxCellOrderKey);
240
- if (!cellsMap || !orderArr) return;
241
- ensureUniqueOrder(orderArr);
261
+ if (!cellsMap && !orderArr) {
262
+ console.warn(
263
+ "[y-mxgraph] applyFilePatch: both cellsMap and orderArr missing, skipping cells update"
264
+ );
265
+ return;
266
+ }
267
+ if (orderArr) ensureUniqueOrder(orderArr);
242
268
  if (update.cells[DIFF_REMOVE] && update.cells[DIFF_REMOVE].length) {
243
- const orderIds = orderArr.toArray();
244
- const removeIndexList = update.cells[DIFF_REMOVE].map(
245
- (cid) => orderIds.indexOf(cid)
246
- ).filter((i) => i !== -1).sort((a, b) => b - a);
247
- removeIndexList.forEach((idx) => orderArr.delete(idx, 1));
248
- update.cells[DIFF_REMOVE].forEach((cid) => cellsMap.delete(cid));
269
+ if (orderArr) {
270
+ const orderIds = orderArr.toArray();
271
+ const removeIndexList = update.cells[DIFF_REMOVE].map(
272
+ (cid) => orderIds.indexOf(cid)
273
+ ).filter((i) => i !== -1).sort((a, b) => b - a);
274
+ removeIndexList.forEach((idx) => orderArr.delete(idx, 1));
275
+ }
276
+ if (cellsMap) {
277
+ update.cells[DIFF_REMOVE].forEach((cid) => {
278
+ if (cellsMap.has(cid)) {
279
+ cellsMap.delete(cid);
280
+ }
281
+ });
282
+ }
249
283
  }
250
284
  if (update.cells[DIFF_INSERT] && update.cells[DIFF_INSERT].length) {
251
285
  for (const item of update.cells[DIFF_INSERT]) {
252
286
  const id2 = item["id"];
253
287
  if (!id2) continue;
254
- const xmlElement = new Y.XmlElement("mxCell");
255
- Object.keys(item).forEach((key2) => {
256
- if (key2 === "previous") return;
257
- xmlElement.setAttribute(key2, item[key2]);
258
- });
259
- cellsMap.set(id2, xmlElement);
260
- const previous = item["previous"];
261
- const parent = item["parent"];
262
- let anchorId = null;
263
- let fallbackToEnd = true;
264
- if (typeof previous !== "undefined") {
265
- if (previous === "") {
266
- if (parent) {
267
- anchorId = parent;
268
- fallbackToEnd = true;
269
- } else {
270
- anchorId = null;
288
+ if (cellsMap) {
289
+ const xmlElement = new Y.XmlElement("mxCell");
290
+ Object.keys(item).forEach((key2) => {
291
+ if (key2 === "previous") return;
292
+ xmlElement.setAttribute(key2, item[key2]);
293
+ });
294
+ cellsMap.set(id2, xmlElement);
295
+ }
296
+ if (orderArr) {
297
+ const previous = item["previous"];
298
+ const parent = item["parent"];
299
+ let anchorId = null;
300
+ let fallbackToEnd = true;
301
+ if (typeof previous !== "undefined") {
302
+ if (previous === "") {
303
+ anchorId = "";
271
304
  fallbackToEnd = false;
305
+ } else {
306
+ anchorId = previous;
307
+ fallbackToEnd = true;
272
308
  }
273
- } else {
274
- anchorId = previous;
309
+ } else if (parent) {
310
+ anchorId = parent;
275
311
  fallbackToEnd = true;
276
312
  }
277
- } else if (parent) {
278
- anchorId = parent;
279
- fallbackToEnd = true;
313
+ insertAfterUnique(
314
+ orderArr,
315
+ id2,
316
+ anchorId,
317
+ fallbackToEnd
318
+ );
280
319
  }
281
- insertAfterUnique(
282
- orderArr,
283
- id2,
284
- anchorId,
285
- fallbackToEnd
286
- );
287
320
  }
288
321
  }
289
322
  if (update.cells[DIFF_UPDATE]) {
290
- Object.keys(update.cells[DIFF_UPDATE]).forEach((cid) => {
291
- const updateObj = update.cells[DIFF_UPDATE][cid];
292
- const cell = cellsMap.get(cid);
293
- if (cell) {
294
- Object.keys(updateObj).forEach((k) => {
295
- if (k === "previous") return;
296
- cell.setAttribute(k, updateObj[k]);
297
- });
298
- }
299
- });
300
- Object.keys(update.cells[DIFF_UPDATE]).forEach((cellId) => {
301
- const updateObj = update.cells[DIFF_UPDATE][cellId];
302
- const hasPrev = "previous" in updateObj;
303
- const hasParent = "parent" in updateObj;
304
- if (!hasPrev && !hasParent) return;
305
- const prevVal = hasPrev ? updateObj.previous : void 0;
306
- const parentVal = hasParent ? updateObj.parent : void 0;
307
- let anchorId = null;
308
- let fallbackToEnd = true;
309
- if (hasPrev) {
310
- if (prevVal === "") {
311
- if (parentVal) {
312
- anchorId = parentVal;
323
+ if (cellsMap) {
324
+ Object.keys(update.cells[DIFF_UPDATE]).forEach((cid) => {
325
+ const updateObj = update.cells[DIFF_UPDATE][cid];
326
+ const cell = cellsMap.get(cid);
327
+ if (cell) {
328
+ Object.keys(updateObj).forEach((k) => {
329
+ if (k === "previous") return;
330
+ cell.setAttribute(k, updateObj[k]);
331
+ });
332
+ } else {
333
+ console.warn(
334
+ `[y-mxgraph] applyFilePatch: cell ${cid} not found in cellsMap, skipping update`
335
+ );
336
+ }
337
+ });
338
+ }
339
+ if (cellsMap && orderArr) {
340
+ const reorderEntries = Object.keys(update.cells[DIFF_UPDATE]).map((cellId) => {
341
+ const updateObj = update.cells[DIFF_UPDATE][cellId];
342
+ const hasPrev = "previous" in updateObj;
343
+ const hasParent = "parent" in updateObj;
344
+ if (!hasPrev && !hasParent) return null;
345
+ const prevVal = hasPrev ? updateObj.previous : void 0;
346
+ const parentVal = hasParent ? updateObj.parent : void 0;
347
+ let anchorId = null;
348
+ let fallbackToEnd = true;
349
+ if (hasPrev) {
350
+ if (prevVal === "") {
351
+ anchorId = "";
352
+ fallbackToEnd = false;
353
+ } else if (prevVal === null || typeof prevVal === "undefined") {
354
+ anchorId = null;
313
355
  fallbackToEnd = true;
314
356
  } else {
315
- anchorId = null;
316
- fallbackToEnd = false;
357
+ anchorId = prevVal;
358
+ fallbackToEnd = true;
317
359
  }
318
- } else {
319
- anchorId = prevVal;
360
+ } else if (parentVal) {
361
+ anchorId = parentVal;
320
362
  fallbackToEnd = true;
321
363
  }
322
- } else if (parentVal) {
323
- anchorId = parentVal;
324
- fallbackToEnd = true;
325
- }
326
- const currentIds = orderArr.toArray();
327
- const currentIndex = currentIds.indexOf(cellId);
328
- if (currentIndex === -1) {
329
- let newCell = cellsMap.get(cellId);
330
- if (!newCell) {
331
- newCell = new Y.XmlElement("mxCell");
332
- newCell.setAttribute("id", cellId);
333
- Object.keys(updateObj).forEach((k) => {
334
- if (k === "previous") return;
335
- newCell.setAttribute(k, updateObj[k]);
336
- });
337
- cellsMap.set(cellId, newCell);
364
+ return { cellId, anchorId, fallbackToEnd, updateObj };
365
+ }).filter((e) => e !== null);
366
+ const applyReorder = (entry) => {
367
+ const { cellId, anchorId, fallbackToEnd, updateObj } = entry;
368
+ const currentIds = orderArr.toArray();
369
+ const currentIndex = currentIds.indexOf(cellId);
370
+ if (currentIndex === -1) {
371
+ let newCell = cellsMap.get(cellId);
372
+ if (!newCell) {
373
+ newCell = new Y.XmlElement("mxCell");
374
+ newCell.setAttribute("id", cellId);
375
+ Object.keys(updateObj).forEach((k) => {
376
+ if (k === "previous") return;
377
+ newCell.setAttribute(k, updateObj[k]);
378
+ });
379
+ cellsMap.set(cellId, newCell);
380
+ }
381
+ insertAfterUnique(orderArr, cellId, anchorId, fallbackToEnd);
382
+ return;
338
383
  }
339
- insertAfterUnique(
340
- orderArr,
341
- cellId,
342
- anchorId,
343
- fallbackToEnd
344
- );
345
- return;
346
- }
347
- insertAfterUnique(
348
- orderArr,
349
- cellId,
350
- anchorId,
351
- fallbackToEnd
352
- );
353
- });
384
+ insertAfterUnique(orderArr, cellId, anchorId, fallbackToEnd);
385
+ };
386
+ reorderEntries.filter((e) => e.anchorId === "" || e.anchorId === null).forEach(applyReorder);
387
+ reorderEntries.filter((e) => e.anchorId !== "" && e.anchorId !== null).forEach(applyReorder);
388
+ }
354
389
  }
355
390
  }
356
391
  if ("previous" in update) {
357
- const previous = update.previous || null;
358
- const orderArr = mxfile.get(
359
- diagramOrderKey
360
- );
361
- ensureUniqueOrder(orderArr);
362
- insertAfterUnique(orderArr, id, previous, false);
392
+ const previous = Object.prototype.hasOwnProperty.call(update, "previous") ? update.previous : null;
393
+ const orderArr = mxfile.get(diagramOrderKey);
394
+ if (orderArr) {
395
+ ensureUniqueOrder(orderArr);
396
+ insertAfterUnique(orderArr, id, previous, false);
397
+ }
363
398
  }
364
399
  }
365
400
  });
@@ -380,7 +415,7 @@ function initDocSnapshot(doc, resetSnapshot = false) {
380
415
  cellAttrs: /* @__PURE__ */ new Map(),
381
416
  diagramBackground: /* @__PURE__ */ new Map()
382
417
  };
383
- const diagrams = diagramOrder.map((id) => diagramsMap.get(id)).filter((d) => !!d);
418
+ const diagrams = diagramOrder.map((id) => diagramsMap == null ? void 0 : diagramsMap.get(id)).filter((d) => !!d);
384
419
  for (const d of diagrams) {
385
420
  const did = d.get("id") || "";
386
421
  if (!did) continue;
@@ -415,7 +450,7 @@ function initDocSnapshot(doc, resetSnapshot = false) {
415
450
  }
416
451
  }
417
452
  function generatePatch(events, explicitDoc) {
418
- var _a, _b, _c, _d, _e;
453
+ var _a, _b, _c, _d, _e, _f;
419
454
  const patch = {};
420
455
  const doc = (_b = (_a = events[0]) == null ? void 0 : _a.transaction) == null ? void 0 : _b.doc;
421
456
  if (!doc) return patch;
@@ -449,7 +484,7 @@ function generatePatch(events, explicitDoc) {
449
484
  };
450
485
  const orderIds = (_c = orderArr == null ? void 0 : orderArr.toArray()) != null ? _c : [];
451
486
  const currDiagramOrder = orderIds.length > 0 ? orderIds : diagramsMap ? Array.from(diagramsMap.keys()) : [];
452
- const diagramsList = currDiagramOrder.map((id) => diagramsMap.get(id)).filter((d) => !!d);
487
+ const diagramsList = currDiagramOrder.map((id) => diagramsMap == null ? void 0 : diagramsMap.get(id)).filter((d) => !!d);
453
488
  const currCellsOrder = /* @__PURE__ */ new Map();
454
489
  const cellAttrMap = /* @__PURE__ */ new Map();
455
490
  const currDiagramBackground = /* @__PURE__ */ new Map();
@@ -495,7 +530,7 @@ function generatePatch(events, explicitDoc) {
495
530
  for (const id of inserted) {
496
531
  const index = currDiagramOrder.indexOf(id);
497
532
  const previous = index <= 0 ? "" : currDiagramOrder[index - 1];
498
- const yDiagram = diagramsMap.get(id);
533
+ const yDiagram = diagramsMap == null ? void 0 : diagramsMap.get(id);
499
534
  if (!yDiagram) continue;
500
535
  const data = serializer({ diagram: serialize(yDiagram) });
501
536
  patch[DIFF_INSERT].push({ id, previous, data });
@@ -504,7 +539,8 @@ function generatePatch(events, explicitDoc) {
504
539
  }
505
540
  const prevNeighbor = (order, id) => {
506
541
  const i = order.indexOf(id);
507
- return i <= 0 ? "" : order[i - 1];
542
+ if (i === -1) return null;
543
+ return i === 0 ? "" : order[i - 1];
508
544
  };
509
545
  const common = currDiagramOrder.filter((id) => prevSet.has(id) && id);
510
546
  for (const id of common) {
@@ -553,7 +589,8 @@ function generatePatch(events, explicitDoc) {
553
589
  }
554
590
  const prevNeighbor = (order, id) => {
555
591
  const i = order.indexOf(id);
556
- return i <= 0 ? "" : order[i - 1];
592
+ if (i === -1) return null;
593
+ return i === 0 ? "" : order[i - 1];
557
594
  };
558
595
  const commonCells = currCells.filter((cid) => prevSet.has(cid) && cid);
559
596
  for (const cid of commonCells) {
@@ -630,7 +667,7 @@ function generatePatch(events, explicitDoc) {
630
667
  cellsPatch[DIFF_UPDATE] = cellsPatch[DIFF_UPDATE] || {};
631
668
  const cellUpdate = cellsPatch[DIFF_UPDATE][cellId] = cellsPatch[DIFF_UPDATE][cellId] || {};
632
669
  for (const key2 of Array.from(changed)) {
633
- cellUpdate[key2] = el.getAttribute(key2) || "";
670
+ cellUpdate[key2] = (_d = el.getAttribute(key2)) != null ? _d : "";
634
671
  }
635
672
  }
636
673
  if (prevDiagramOrder) {
@@ -659,8 +696,8 @@ function generatePatch(events, explicitDoc) {
659
696
  const cellUpdate = updateBucket[cid] = updateBucket[cid] || {};
660
697
  let changed = false;
661
698
  for (const k of keys) {
662
- const pv = (_d = prevAttrs[k]) != null ? _d : "";
663
- const cv = (_e = currAttrs[k]) != null ? _e : "";
699
+ const pv = (_e = prevAttrs[k]) != null ? _e : "";
700
+ const cv = (_f = currAttrs[k]) != null ? _f : "";
664
701
  if (pv !== cv) {
665
702
  cellUpdate[k] = cv;
666
703
  changed = true;
@@ -1346,6 +1383,91 @@ function bindCollaborator(file, options) {
1346
1383
  cleanupAwareness == null ? void 0 : cleanupAwareness();
1347
1384
  };
1348
1385
  }
1386
+ function checkConsistency(doc, fileData) {
1387
+ try {
1388
+ const xmlFromYdoc = ydoc2xml(doc);
1389
+ if (xmlFromYdoc === fileData) return true;
1390
+ const diagramCountFromYdoc = (xmlFromYdoc.match(/<diagram /g) || []).length;
1391
+ const diagramCountFromFile = (fileData.match(/<diagram /g) || []).length;
1392
+ if (diagramCountFromYdoc !== diagramCountFromFile) return false;
1393
+ const cellCountFromYdoc = (xmlFromYdoc.match(/<mxCell /g) || []).length;
1394
+ const cellCountFromFile = (fileData.match(/<mxCell /g) || []).length;
1395
+ if (cellCountFromYdoc !== cellCountFromFile) return false;
1396
+ return true;
1397
+ } catch (e) {
1398
+ return false;
1399
+ }
1400
+ }
1401
+ class ConsistencyChecker {
1402
+ constructor(doc, getFileData, options = {}) {
1403
+ this.doc = doc;
1404
+ this.getFileData = getFileData;
1405
+ this.intervalId = null;
1406
+ this.handlers = /* @__PURE__ */ new Set();
1407
+ this.consecutiveDriftCount = 0;
1408
+ var _a, _b;
1409
+ this.source = (_a = options.source) != null ? _a : "binding";
1410
+ this.maxAutoFixAttempts = (_b = options.maxAutoFixAttempts) != null ? _b : 3;
1411
+ }
1412
+ /** 注册 drift 事件监听 */
1413
+ onDrift(handler) {
1414
+ this.handlers.add(handler);
1415
+ return () => this.handlers.delete(handler);
1416
+ }
1417
+ /** 启动定期检查(毫秒间隔),0 或负数表示禁用 */
1418
+ start(intervalMs) {
1419
+ this.stop();
1420
+ if (intervalMs <= 0) return;
1421
+ this.intervalId = setInterval(() => {
1422
+ this.check();
1423
+ }, intervalMs);
1424
+ }
1425
+ /** 停止定期检查 */
1426
+ stop() {
1427
+ if (this.intervalId != null) {
1428
+ clearInterval(this.intervalId);
1429
+ this.intervalId = null;
1430
+ }
1431
+ }
1432
+ /**
1433
+ * 执行一次一致性检查。
1434
+ * @returns true = 一致,false = 存在 drift
1435
+ */
1436
+ check() {
1437
+ const consistent = checkConsistency(this.doc, this.getFileData());
1438
+ if (consistent) {
1439
+ this.consecutiveDriftCount = 0;
1440
+ return true;
1441
+ }
1442
+ this.consecutiveDriftCount++;
1443
+ const event = {
1444
+ timestamp: Date.now(),
1445
+ source: this.source,
1446
+ direction: "unknown",
1447
+ details: `drift detected (consecutive #${this.consecutiveDriftCount})`
1448
+ };
1449
+ this.handlers.forEach((handler) => {
1450
+ try {
1451
+ handler(event);
1452
+ } catch (e) {
1453
+ console.warn("[y-mxgraph] drift handler error:", e);
1454
+ }
1455
+ });
1456
+ return false;
1457
+ }
1458
+ /** 重置连续 drift 计数(forceSync 成功后调用) */
1459
+ resetDriftCount() {
1460
+ this.consecutiveDriftCount = 0;
1461
+ }
1462
+ /** 是否已超过最大自动修复次数 */
1463
+ get shouldStopAutoFix() {
1464
+ return this.consecutiveDriftCount >= this.maxAutoFixAttempts;
1465
+ }
1466
+ destroy() {
1467
+ this.stop();
1468
+ this.handlers.clear();
1469
+ }
1470
+ }
1349
1471
  const defaultApplyFileData = (file, xml) => {
1350
1472
  file.ui.setFileData(xml);
1351
1473
  };
@@ -1457,6 +1579,7 @@ class Binding {
1457
1579
  this.suppressLocalApply = false;
1458
1580
  this.docInitialized = false;
1459
1581
  this.ui = null;
1582
+ this.debouncedForceSync = null;
1460
1583
  const {
1461
1584
  doc,
1462
1585
  awareness,
@@ -1466,12 +1589,15 @@ class Binding {
1466
1589
  initialContent = "replace",
1467
1590
  applyFileData = defaultApplyFileData,
1468
1591
  disableBeforeUnload = true,
1469
- transformPatch
1592
+ transformPatch,
1593
+ syncOnOrderMismatch = false
1470
1594
  } = options;
1471
1595
  this.doc = doc;
1472
1596
  this.file = file;
1473
1597
  this.initialContentStrategy = initialContent;
1474
1598
  this.transformPatch = transformPatch;
1599
+ this.applyFileData = applyFileData;
1600
+ this.syncOnOrderMismatch = syncOnOrderMismatch;
1475
1601
  const ui = file.getUi();
1476
1602
  const graph = ui.editor.graph;
1477
1603
  this.mxGraphModel = graph.model;
@@ -1522,7 +1648,6 @@ class Binding {
1522
1648
  this.mxGraphModel.addListener("change", this.mxListener);
1523
1649
  this.docObserver = (events, transaction) => {
1524
1650
  if (transaction.local && transaction.origin === LOCAL_ORIGIN) {
1525
- generatePatch(events);
1526
1651
  return;
1527
1652
  }
1528
1653
  if (this.shouldReplaceWhenDocHasData && !transaction.local) {
@@ -1549,7 +1674,8 @@ class Binding {
1549
1674
  this.docInitialized = true;
1550
1675
  }
1551
1676
  const patch = generatePatch(events);
1552
- if (Object.keys(patch).length === 0) return;
1677
+ const patchKeys = Object.keys(patch);
1678
+ if (patchKeys.length === 0) return;
1553
1679
  this.suppressLocalApply = true;
1554
1680
  try {
1555
1681
  file.patch([patch]);
@@ -1558,6 +1684,14 @@ class Binding {
1558
1684
  } finally {
1559
1685
  this.suppressLocalApply = false;
1560
1686
  }
1687
+ if (this.syncOnOrderMismatch && patch.u && this.checkOrderMismatch(patch)) {
1688
+ console.warn("[y-mxgraph] order mismatch detected, debounced forceSync (file → ydoc)");
1689
+ if (this.debouncedForceSync) clearTimeout(this.debouncedForceSync);
1690
+ this.debouncedForceSync = setTimeout(() => {
1691
+ this.debouncedForceSync = null;
1692
+ this.forceSync("file-to-ydoc");
1693
+ }, 300);
1694
+ }
1561
1695
  };
1562
1696
  doc.getMap(key$1).observeDeep(this.docObserver);
1563
1697
  if (awareness) {
@@ -1571,6 +1705,29 @@ class Binding {
1571
1705
  if (undoManager) {
1572
1706
  this.cleanupUndoManager = bindUndoManager(doc, file, undoManager);
1573
1707
  }
1708
+ const {
1709
+ consistencyCheckInterval,
1710
+ onDrift
1711
+ } = options;
1712
+ if (consistencyCheckInterval && consistencyCheckInterval > 0) {
1713
+ this.consistencyChecker = new ConsistencyChecker(doc, () => file.data, {
1714
+ source: "binding"
1715
+ });
1716
+ if (onDrift) {
1717
+ this.consistencyChecker.onDrift(onDrift);
1718
+ }
1719
+ this.consistencyChecker.onDrift((event) => {
1720
+ var _a;
1721
+ if ((_a = this.consistencyChecker) == null ? void 0 : _a.shouldStopAutoFix) {
1722
+ console.warn(
1723
+ `[y-mxgraph] 连续 ${event.details} 次 drift,停止自动 forceSync`
1724
+ );
1725
+ return;
1726
+ }
1727
+ this.forceSync("ydoc-to-file");
1728
+ });
1729
+ this.consistencyChecker.start(consistencyCheckInterval);
1730
+ }
1574
1731
  }
1575
1732
  /** replace 策略下,构造时 doc 为空,现在 doc 有数据时需要强制替换 */
1576
1733
  get shouldReplaceWhenDocHasData() {
@@ -1587,17 +1744,83 @@ class Binding {
1587
1744
  this.ui.editor.setStatus("");
1588
1745
  (_a = this.ui.currentFile) == null ? void 0 : _a.setModified(false);
1589
1746
  }
1747
+ /**
1748
+ * 强制同步 ydoc 与 file,修复检测到的不一致。
1749
+ *
1750
+ * @param direction - 同步方向
1751
+ * - `ydoc-to-file`(默认):用 ydoc 数据覆盖 file
1752
+ * - `file-to-ydoc`:用 file 数据覆盖 ydoc
1753
+ */
1754
+ forceSync(direction = "ydoc-to-file") {
1755
+ var _a;
1756
+ if (direction === "ydoc-to-file") {
1757
+ const xml = ydoc2xml(this.doc);
1758
+ if (!xml || !xml.includes("<diagram")) return;
1759
+ this.suppressLocalApply = true;
1760
+ try {
1761
+ this.applyFileData(this.file, xml);
1762
+ this.file.setShadowPages(this.file.ui.clonePages(this.file.ui.pages));
1763
+ initDocSnapshot(this.doc, false);
1764
+ this.resetEditorStatus();
1765
+ } finally {
1766
+ this.suppressLocalApply = false;
1767
+ }
1768
+ } else {
1769
+ this.doc.transact(() => {
1770
+ xml2ydoc(this.file.data, this.doc);
1771
+ initDocSnapshot(this.doc, false);
1772
+ }, LOCAL_ORIGIN);
1773
+ }
1774
+ (_a = this.consistencyChecker) == null ? void 0 : _a.resetDriftCount();
1775
+ }
1776
+ checkOrderMismatch(patch) {
1777
+ var _a;
1778
+ if (!patch.u) return false;
1779
+ for (const [did, update] of Object.entries(patch.u)) {
1780
+ if (!((_a = update.cells) == null ? void 0 : _a.u)) continue;
1781
+ for (const [cid, cellUpdate] of Object.entries(update.cells.u)) {
1782
+ if (!("previous" in cellUpdate)) continue;
1783
+ const mxfile = this.doc.getMap(key$1);
1784
+ const diagramsMap = mxfile.get(key$2);
1785
+ const diagram = diagramsMap == null ? void 0 : diagramsMap.get(did);
1786
+ if (!diagram) continue;
1787
+ const gm = diagram.get(key);
1788
+ if (!gm) continue;
1789
+ const orderArr = gm.get(mxCellOrderKey);
1790
+ if (!orderArr) continue;
1791
+ const ids = orderArr.toArray();
1792
+ const idx = ids.indexOf(cid);
1793
+ if (idx === -1) continue;
1794
+ const expectedPrev = idx === 0 ? "" : ids[idx - 1];
1795
+ const actualPrev = cellUpdate.previous;
1796
+ if (expectedPrev !== actualPrev) {
1797
+ console.log(`[y-mxgraph] order mismatch: cell=${cid}, ydoc prev=${JSON.stringify(expectedPrev)}, patch prev=${JSON.stringify(actualPrev)}`);
1798
+ return true;
1799
+ }
1800
+ }
1801
+ }
1802
+ return false;
1803
+ }
1804
+ /**
1805
+ * 手动触发一次一致性检查。
1806
+ * @returns true = 一致,false = 存在 drift
1807
+ */
1808
+ checkConsistency() {
1809
+ if (!this.consistencyChecker) return true;
1810
+ return this.consistencyChecker.check();
1811
+ }
1590
1812
  /**
1591
1813
  * 销毁绑定,解除所有监听器
1592
1814
  * @param deep - 是否深度清理(包括 awareness/undoManager),默认 false
1593
1815
  */
1594
1816
  destroy(deep = false) {
1595
- var _a, _b;
1817
+ var _a, _b, _c;
1596
1818
  this.mxGraphModel.removeListener("change", this.mxListener);
1597
1819
  this.doc.getMap(key$1).unobserveDeep(this.docObserver);
1820
+ (_a = this.consistencyChecker) == null ? void 0 : _a.destroy();
1598
1821
  if (deep) {
1599
- (_a = this.cleanupCollaborator) == null ? void 0 : _a.call(this);
1600
- (_b = this.cleanupUndoManager) == null ? void 0 : _b.call(this);
1822
+ (_b = this.cleanupCollaborator) == null ? void 0 : _b.call(this);
1823
+ (_c = this.cleanupUndoManager) == null ? void 0 : _c.call(this);
1601
1824
  }
1602
1825
  }
1603
1826
  /**