@pm-cm/yjs 0.0.11 → 0.0.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.
package/dist/index.cjs CHANGED
@@ -141,6 +141,9 @@ function createYjsBridge(config, options) {
141
141
  throw new Error("sharedProseMirror belongs to a different Y.Doc than the provided doc");
142
142
  }
143
143
  let lastBridgedText = null;
144
+ let pendingSkipCleanup = null;
145
+ let skipPending = false;
146
+ let parseFailed = false;
144
147
  const syncTextToProsemirror = (origin) => {
145
148
  const text = normalize(sharedText.toString());
146
149
  if (lastBridgedText === text) {
@@ -153,6 +156,7 @@ function createYjsBridge(config, options) {
153
156
  onError
154
157
  });
155
158
  if (result.ok) {
159
+ parseFailed = false;
156
160
  let canonical = text;
157
161
  if (origin === ORIGIN_INIT) {
158
162
  try {
@@ -165,6 +169,9 @@ function createYjsBridge(config, options) {
165
169
  }
166
170
  }
167
171
  lastBridgedText = canonical;
172
+ } else {
173
+ parseFailed = true;
174
+ lastBridgedText = text;
168
175
  }
169
176
  return result.ok;
170
177
  };
@@ -226,13 +233,17 @@ function createYjsBridge(config, options) {
226
233
  let parseError = false;
227
234
  if (fallbackText.length > 0) {
228
235
  replaceSharedText(sharedText, fallbackText, ORIGIN_INIT, normalize);
236
+ lastBridgedText = normalize(fallbackText);
229
237
  const fallbackResult = replaceSharedProseMirror(doc, sharedProseMirror, fallbackText, ORIGIN_INIT, {
230
238
  schema,
231
239
  parse,
232
240
  normalize,
233
241
  onError
234
242
  });
235
- if (!fallbackResult.ok) parseError = true;
243
+ if (!fallbackResult.ok) {
244
+ parseError = true;
245
+ parseFailed = true;
246
+ }
236
247
  }
237
248
  return { source: "text", ...parseError && { parseError: true } };
238
249
  }
@@ -257,10 +268,57 @@ function createYjsBridge(config, options) {
257
268
  }
258
269
  if (transactionTouchedXmlFragment(transaction.changed, sharedProseMirror)) {
259
270
  lastBridgedText = normalize(sharedText.toString());
271
+ parseFailed = false;
260
272
  return;
261
273
  }
262
274
  if (skipOrigins !== null && skipOrigins.has(transaction.origin)) {
263
- lastBridgedText = normalize(sharedText.toString());
275
+ const expectedText = normalize(sharedText.toString());
276
+ lastBridgedText = expectedText;
277
+ const pmTextNow = sharedProseMirrorToText(sharedProseMirror);
278
+ if (pmTextNow !== null && normalize(pmTextNow) === expectedText) {
279
+ parseFailed = false;
280
+ if (pendingSkipCleanup) pendingSkipCleanup();
281
+ return;
282
+ }
283
+ const xmlTextAtSkipStart = pmTextNow;
284
+ if (pendingSkipCleanup) pendingSkipCleanup();
285
+ skipPending = true;
286
+ let resolved = false;
287
+ const resolve = () => {
288
+ if (resolved) return;
289
+ resolved = true;
290
+ skipPending = false;
291
+ sharedProseMirror.unobserveDeep(xmlCatchUpObserver);
292
+ clearTimeout(timer);
293
+ pendingSkipCleanup = null;
294
+ };
295
+ const xmlCatchUpObserver = (_events, transaction2) => {
296
+ if (resolved) return;
297
+ if (skipOrigins.has(transaction2.origin)) {
298
+ const pmText = sharedProseMirrorToText(sharedProseMirror);
299
+ if (pmText !== null && normalize(pmText) === expectedText) {
300
+ parseFailed = false;
301
+ resolve();
302
+ }
303
+ }
304
+ };
305
+ const runFallback = () => {
306
+ resolve();
307
+ if (lastBridgedText !== expectedText) return;
308
+ const pmText = sharedProseMirrorToText(sharedProseMirror);
309
+ if (pmText === null || normalize(pmText) !== expectedText) {
310
+ const changedDuringSkip = xmlTextAtSkipStart !== null && pmText !== null && normalize(pmText) !== normalize(xmlTextAtSkipStart) || xmlTextAtSkipStart === null && pmText !== null || xmlTextAtSkipStart !== null && pmText === null;
311
+ if (changedDuringSkip) {
312
+ lastBridgedText = pmText !== null ? normalize(pmText) : lastBridgedText;
313
+ return;
314
+ }
315
+ lastBridgedText = null;
316
+ syncTextToProsemirror(ORIGIN_TEXT_TO_PM);
317
+ }
318
+ };
319
+ const timer = setTimeout(runFallback, 500);
320
+ sharedProseMirror.observeDeep(xmlCatchUpObserver);
321
+ pendingSkipCleanup = resolve;
264
322
  return;
265
323
  }
266
324
  syncTextToProsemirror(ORIGIN_TEXT_TO_PM);
@@ -270,6 +328,12 @@ function createYjsBridge(config, options) {
270
328
  return {
271
329
  bootstrapResult,
272
330
  syncToSharedText(doc2) {
331
+ if (skipPending) {
332
+ return { ok: false, reason: "skip-pending" };
333
+ }
334
+ if (parseFailed) {
335
+ return { ok: false, reason: "parse-failed" };
336
+ }
273
337
  let text;
274
338
  try {
275
339
  text = serialize(doc2);
@@ -289,6 +353,7 @@ function createYjsBridge(config, options) {
289
353
  },
290
354
  dispose() {
291
355
  sharedText.unobserve(textObserver);
356
+ if (pendingSkipCleanup) pendingSkipCleanup();
292
357
  }
293
358
  };
294
359
  }
@@ -340,7 +405,6 @@ var wiredBridges = /* @__PURE__ */ new WeakMap();
340
405
  var defaultOnWarning = (event) => console.warn(`[pm-cm] ${event.code}: ${event.message}`);
341
406
  function createBridgeSyncPlugin(bridge, options = {}) {
342
407
  const warn = options.onWarning ?? defaultOnWarning;
343
- let yjsBatchSeen = false;
344
408
  let needsSync = false;
345
409
  return new import_prosemirror_state.Plugin({
346
410
  key: bridgeSyncPluginKey,
@@ -351,10 +415,6 @@ function createBridgeSyncPlugin(bridge, options = {}) {
351
415
  apply(tr, _prev) {
352
416
  if (!tr.docChanged) return { needsSync };
353
417
  if (bridge.isYjsSyncChange(tr)) {
354
- yjsBatchSeen = true;
355
- return { needsSync };
356
- }
357
- if (yjsBatchSeen && tr.getMeta("appendedTransaction")) {
358
418
  return { needsSync };
359
419
  }
360
420
  needsSync = true;
@@ -372,6 +432,14 @@ function createBridgeSyncPlugin(bridge, options = {}) {
372
432
  if (needsSync) {
373
433
  const result = bridge.syncToSharedText(view.state.doc);
374
434
  if (!result.ok) {
435
+ if (result.reason === "skip-pending") {
436
+ return;
437
+ }
438
+ if (result.reason === "parse-failed") {
439
+ options.onSyncFailure?.(result, view);
440
+ warn({ code: "sync-failed", message: `bridge sync failed: ${result.reason}` });
441
+ return;
442
+ }
375
443
  if (result.reason !== "unchanged") {
376
444
  options.onSyncFailure?.(result, view);
377
445
  warn({ code: "sync-failed", message: `bridge sync failed: ${result.reason}` });
@@ -379,12 +447,15 @@ function createBridgeSyncPlugin(bridge, options = {}) {
379
447
  }
380
448
  }
381
449
  needsSync = false;
382
- yjsBatchSeen = false;
383
450
  },
384
451
  destroy() {
385
452
  const remaining = (wiredBridges.get(bridge) ?? 1) - 1;
386
- if (remaining <= 0) wiredBridges.delete(bridge);
387
- else wiredBridges.set(bridge, remaining);
453
+ if (remaining <= 0) {
454
+ wiredBridges.delete(bridge);
455
+ if (options.autoDispose) bridge.dispose();
456
+ } else {
457
+ wiredBridges.set(bridge, remaining);
458
+ }
388
459
  }
389
460
  };
390
461
  }
@@ -430,6 +501,8 @@ function broadcastTextCursor(awareness, cmCursorFieldName, sharedText, textAncho
430
501
  awareness.setLocalStateField(cmCursorFieldName, { anchor: relAnchor, head: relHead });
431
502
  }
432
503
  var defaultOnWarning2 = (event) => console.warn(`[pm-cm] ${event.code}: ${event.message}`);
504
+ var awarenessRefCounts = /* @__PURE__ */ new WeakMap();
505
+ var awarenessFieldNames = /* @__PURE__ */ new WeakMap();
433
506
  function createCursorSyncPlugin(options) {
434
507
  const { awareness, serialize, sharedText } = options;
435
508
  const warn = options.onWarning ?? defaultOnWarning2;
@@ -479,53 +552,80 @@ function createCursorSyncPlugin(options) {
479
552
  }
480
553
  },
481
554
  view(editorView) {
555
+ const viewCount = awarenessRefCounts.get(awareness) ?? 0;
556
+ awarenessRefCounts.set(awareness, viewCount + 1);
557
+ let fieldNames = awarenessFieldNames.get(awareness);
558
+ if (!fieldNames) {
559
+ fieldNames = /* @__PURE__ */ new Set();
560
+ awarenessFieldNames.set(awareness, fieldNames);
561
+ }
562
+ fieldNames.add(cursorFieldName);
563
+ if (sharedText) {
564
+ fieldNames.add(cmCursorFieldName);
565
+ }
482
566
  let suppressCmReaction = false;
483
567
  let lastCmAbsAnchor = -1;
484
568
  let lastCmAbsHead = -1;
485
569
  let cmCursorHandledByListener = false;
570
+ let inAwarenessHandler = false;
486
571
  const handleAwarenessUpdate = ({ added, updated }) => {
572
+ if (inAwarenessHandler) return;
487
573
  if (suppressCmReaction) return;
488
574
  if (!sharedText?.doc) return;
489
575
  const localId = awareness.clientID;
490
576
  if (!updated.includes(localId) && !added.includes(localId)) return;
491
577
  const localState = awareness.getLocalState();
492
578
  if (!localState) return;
493
- const cmCursor = localState[cmCursorFieldName];
494
- if (!cmCursor?.anchor || !cmCursor?.head) {
495
- if (lastCmAbsAnchor !== -1) {
579
+ inAwarenessHandler = true;
580
+ try {
581
+ const cmCursor = localState[cmCursorFieldName];
582
+ if (!cmCursor?.anchor || !cmCursor?.head) {
583
+ if (lastCmAbsAnchor !== -1) {
584
+ awareness.setLocalStateField(cursorFieldName, null);
585
+ lastCmAbsAnchor = -1;
586
+ lastCmAbsHead = -1;
587
+ }
588
+ return;
589
+ }
590
+ const absAnchor = (0, import_yjs.createAbsolutePositionFromRelativePosition)(cmCursor.anchor, sharedText.doc);
591
+ const absHead = (0, import_yjs.createAbsolutePositionFromRelativePosition)(cmCursor.head, sharedText.doc);
592
+ if (!absAnchor || !absHead) {
496
593
  awareness.setLocalStateField(cursorFieldName, null);
497
594
  lastCmAbsAnchor = -1;
498
595
  lastCmAbsHead = -1;
596
+ return;
499
597
  }
500
- return;
501
- }
502
- const absAnchor = (0, import_yjs.createAbsolutePositionFromRelativePosition)(cmCursor.anchor, sharedText.doc);
503
- const absHead = (0, import_yjs.createAbsolutePositionFromRelativePosition)(cmCursor.head, sharedText.doc);
504
- if (!absAnchor || !absHead) {
598
+ if (absAnchor.type !== sharedText || absHead.type !== sharedText) {
599
+ return;
600
+ }
601
+ if (absAnchor.index === lastCmAbsAnchor && absHead.index === lastCmAbsHead) return;
602
+ const map = getOrBuildMap(editorView.state.doc);
603
+ if (!map) {
604
+ awareness.setLocalStateField(cursorFieldName, null);
605
+ return;
606
+ }
607
+ const pmAnchor = (0, import_core.reverseCursorMapLookup)(map, absAnchor.index);
608
+ const pmHead = (0, import_core.reverseCursorMapLookup)(map, absHead.index);
609
+ if (pmAnchor === null || pmHead === null) {
610
+ awareness.setLocalStateField(cursorFieldName, null);
611
+ return;
612
+ }
613
+ lastCmAbsAnchor = absAnchor.index;
614
+ lastCmAbsHead = absHead.index;
615
+ if (pendingCmCursor != null) {
616
+ cmCursorHandledByListener = true;
617
+ }
618
+ const ok = broadcastPmCursor(awareness, cursorFieldName, editorView, pmAnchor, pmHead);
619
+ if (!ok && !warnedSyncPluginMissing) {
620
+ warnedSyncPluginMissing = true;
621
+ warn({ code: "ysync-plugin-missing", message: "ySyncPlugin state not available \u2014 cursor broadcast skipped" });
622
+ }
623
+ } catch {
505
624
  awareness.setLocalStateField(cursorFieldName, null);
506
625
  lastCmAbsAnchor = -1;
507
626
  lastCmAbsHead = -1;
508
- return;
509
- }
510
- if (absAnchor.index === lastCmAbsAnchor && absHead.index === lastCmAbsHead) return;
511
- const map = getOrBuildMap(editorView.state.doc);
512
- if (!map) {
513
- awareness.setLocalStateField(cursorFieldName, null);
514
- return;
515
- }
516
- const pmAnchor = (0, import_core.reverseCursorMapLookup)(map, absAnchor.index);
517
- const pmHead = (0, import_core.reverseCursorMapLookup)(map, absHead.index);
518
- if (pmAnchor === null || pmHead === null) {
519
- awareness.setLocalStateField(cursorFieldName, null);
520
- return;
521
- }
522
- lastCmAbsAnchor = absAnchor.index;
523
- lastCmAbsHead = absHead.index;
524
- cmCursorHandledByListener = true;
525
- const ok = broadcastPmCursor(awareness, cursorFieldName, editorView, pmAnchor, pmHead);
526
- if (!ok && !warnedSyncPluginMissing) {
527
- warnedSyncPluginMissing = true;
528
- warn({ code: "ysync-plugin-missing", message: "ySyncPlugin state not available \u2014 cursor broadcast skipped" });
627
+ } finally {
628
+ inAwarenessHandler = false;
529
629
  }
530
630
  };
531
631
  if (sharedText) {
@@ -564,6 +664,9 @@ function createCursorSyncPlugin(options) {
564
664
  }
565
665
  } else {
566
666
  awareness.setLocalStateField(cursorFieldName, null);
667
+ if (sharedText?.doc) {
668
+ awareness.setLocalStateField(cmCursorFieldName, null);
669
+ }
567
670
  }
568
671
  }
569
672
  cmCursorHandledByListener = false;
@@ -598,9 +701,18 @@ function createCursorSyncPlugin(options) {
598
701
  if (sharedText) {
599
702
  awareness.off("update", handleAwarenessUpdate);
600
703
  }
601
- awareness.setLocalStateField(cursorFieldName, null);
602
- if (sharedText) {
603
- awareness.setLocalStateField(cmCursorFieldName, null);
704
+ const remaining = (awarenessRefCounts.get(awareness) ?? 1) - 1;
705
+ if (remaining <= 0) {
706
+ awarenessRefCounts.delete(awareness);
707
+ const fields = awarenessFieldNames.get(awareness);
708
+ if (fields) {
709
+ for (const field of fields) {
710
+ awareness.setLocalStateField(field, null);
711
+ }
712
+ awarenessFieldNames.delete(awareness);
713
+ }
714
+ } else {
715
+ awarenessRefCounts.set(awareness, remaining);
604
716
  }
605
717
  }
606
718
  };
@@ -641,7 +753,10 @@ function createCollabPlugins(schema, options) {
641
753
  (0, import_y_prosemirror3.yUndoPlugin)(options.yUndoPluginOpts)
642
754
  ];
643
755
  if (options.bridge) {
644
- plugins.push(createBridgeSyncPlugin(options.bridge, { onWarning: options.onWarning }));
756
+ plugins.push(createBridgeSyncPlugin(options.bridge, {
757
+ onWarning: options.onWarning,
758
+ autoDispose: options.autoDisposeBridge
759
+ }));
645
760
  }
646
761
  if (enableCursorSync && options.serialize) {
647
762
  plugins.push(