@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.js CHANGED
@@ -98,6 +98,9 @@ function createYjsBridge(config, options) {
98
98
  throw new Error("sharedProseMirror belongs to a different Y.Doc than the provided doc");
99
99
  }
100
100
  let lastBridgedText = null;
101
+ let pendingSkipCleanup = null;
102
+ let skipPending = false;
103
+ let parseFailed = false;
101
104
  const syncTextToProsemirror = (origin) => {
102
105
  const text = normalize(sharedText.toString());
103
106
  if (lastBridgedText === text) {
@@ -110,6 +113,7 @@ function createYjsBridge(config, options) {
110
113
  onError
111
114
  });
112
115
  if (result.ok) {
116
+ parseFailed = false;
113
117
  let canonical = text;
114
118
  if (origin === ORIGIN_INIT) {
115
119
  try {
@@ -122,6 +126,9 @@ function createYjsBridge(config, options) {
122
126
  }
123
127
  }
124
128
  lastBridgedText = canonical;
129
+ } else {
130
+ parseFailed = true;
131
+ lastBridgedText = text;
125
132
  }
126
133
  return result.ok;
127
134
  };
@@ -183,13 +190,17 @@ function createYjsBridge(config, options) {
183
190
  let parseError = false;
184
191
  if (fallbackText.length > 0) {
185
192
  replaceSharedText(sharedText, fallbackText, ORIGIN_INIT, normalize);
193
+ lastBridgedText = normalize(fallbackText);
186
194
  const fallbackResult = replaceSharedProseMirror(doc, sharedProseMirror, fallbackText, ORIGIN_INIT, {
187
195
  schema,
188
196
  parse,
189
197
  normalize,
190
198
  onError
191
199
  });
192
- if (!fallbackResult.ok) parseError = true;
200
+ if (!fallbackResult.ok) {
201
+ parseError = true;
202
+ parseFailed = true;
203
+ }
193
204
  }
194
205
  return { source: "text", ...parseError && { parseError: true } };
195
206
  }
@@ -214,10 +225,57 @@ function createYjsBridge(config, options) {
214
225
  }
215
226
  if (transactionTouchedXmlFragment(transaction.changed, sharedProseMirror)) {
216
227
  lastBridgedText = normalize(sharedText.toString());
228
+ parseFailed = false;
217
229
  return;
218
230
  }
219
231
  if (skipOrigins !== null && skipOrigins.has(transaction.origin)) {
220
- lastBridgedText = normalize(sharedText.toString());
232
+ const expectedText = normalize(sharedText.toString());
233
+ lastBridgedText = expectedText;
234
+ const pmTextNow = sharedProseMirrorToText(sharedProseMirror);
235
+ if (pmTextNow !== null && normalize(pmTextNow) === expectedText) {
236
+ parseFailed = false;
237
+ if (pendingSkipCleanup) pendingSkipCleanup();
238
+ return;
239
+ }
240
+ const xmlTextAtSkipStart = pmTextNow;
241
+ if (pendingSkipCleanup) pendingSkipCleanup();
242
+ skipPending = true;
243
+ let resolved = false;
244
+ const resolve = () => {
245
+ if (resolved) return;
246
+ resolved = true;
247
+ skipPending = false;
248
+ sharedProseMirror.unobserveDeep(xmlCatchUpObserver);
249
+ clearTimeout(timer);
250
+ pendingSkipCleanup = null;
251
+ };
252
+ const xmlCatchUpObserver = (_events, transaction2) => {
253
+ if (resolved) return;
254
+ if (skipOrigins.has(transaction2.origin)) {
255
+ const pmText = sharedProseMirrorToText(sharedProseMirror);
256
+ if (pmText !== null && normalize(pmText) === expectedText) {
257
+ parseFailed = false;
258
+ resolve();
259
+ }
260
+ }
261
+ };
262
+ const runFallback = () => {
263
+ resolve();
264
+ if (lastBridgedText !== expectedText) return;
265
+ const pmText = sharedProseMirrorToText(sharedProseMirror);
266
+ if (pmText === null || normalize(pmText) !== expectedText) {
267
+ const changedDuringSkip = xmlTextAtSkipStart !== null && pmText !== null && normalize(pmText) !== normalize(xmlTextAtSkipStart) || xmlTextAtSkipStart === null && pmText !== null || xmlTextAtSkipStart !== null && pmText === null;
268
+ if (changedDuringSkip) {
269
+ lastBridgedText = pmText !== null ? normalize(pmText) : lastBridgedText;
270
+ return;
271
+ }
272
+ lastBridgedText = null;
273
+ syncTextToProsemirror(ORIGIN_TEXT_TO_PM);
274
+ }
275
+ };
276
+ const timer = setTimeout(runFallback, 500);
277
+ sharedProseMirror.observeDeep(xmlCatchUpObserver);
278
+ pendingSkipCleanup = resolve;
221
279
  return;
222
280
  }
223
281
  syncTextToProsemirror(ORIGIN_TEXT_TO_PM);
@@ -227,6 +285,12 @@ function createYjsBridge(config, options) {
227
285
  return {
228
286
  bootstrapResult,
229
287
  syncToSharedText(doc2) {
288
+ if (skipPending) {
289
+ return { ok: false, reason: "skip-pending" };
290
+ }
291
+ if (parseFailed) {
292
+ return { ok: false, reason: "parse-failed" };
293
+ }
230
294
  let text;
231
295
  try {
232
296
  text = serialize(doc2);
@@ -246,6 +310,7 @@ function createYjsBridge(config, options) {
246
310
  },
247
311
  dispose() {
248
312
  sharedText.unobserve(textObserver);
313
+ if (pendingSkipCleanup) pendingSkipCleanup();
249
314
  }
250
315
  };
251
316
  }
@@ -297,7 +362,6 @@ var wiredBridges = /* @__PURE__ */ new WeakMap();
297
362
  var defaultOnWarning = (event) => console.warn(`[pm-cm] ${event.code}: ${event.message}`);
298
363
  function createBridgeSyncPlugin(bridge, options = {}) {
299
364
  const warn = options.onWarning ?? defaultOnWarning;
300
- let yjsBatchSeen = false;
301
365
  let needsSync = false;
302
366
  return new Plugin({
303
367
  key: bridgeSyncPluginKey,
@@ -308,10 +372,6 @@ function createBridgeSyncPlugin(bridge, options = {}) {
308
372
  apply(tr, _prev) {
309
373
  if (!tr.docChanged) return { needsSync };
310
374
  if (bridge.isYjsSyncChange(tr)) {
311
- yjsBatchSeen = true;
312
- return { needsSync };
313
- }
314
- if (yjsBatchSeen && tr.getMeta("appendedTransaction")) {
315
375
  return { needsSync };
316
376
  }
317
377
  needsSync = true;
@@ -329,6 +389,14 @@ function createBridgeSyncPlugin(bridge, options = {}) {
329
389
  if (needsSync) {
330
390
  const result = bridge.syncToSharedText(view.state.doc);
331
391
  if (!result.ok) {
392
+ if (result.reason === "skip-pending") {
393
+ return;
394
+ }
395
+ if (result.reason === "parse-failed") {
396
+ options.onSyncFailure?.(result, view);
397
+ warn({ code: "sync-failed", message: `bridge sync failed: ${result.reason}` });
398
+ return;
399
+ }
332
400
  if (result.reason !== "unchanged") {
333
401
  options.onSyncFailure?.(result, view);
334
402
  warn({ code: "sync-failed", message: `bridge sync failed: ${result.reason}` });
@@ -336,12 +404,15 @@ function createBridgeSyncPlugin(bridge, options = {}) {
336
404
  }
337
405
  }
338
406
  needsSync = false;
339
- yjsBatchSeen = false;
340
407
  },
341
408
  destroy() {
342
409
  const remaining = (wiredBridges.get(bridge) ?? 1) - 1;
343
- if (remaining <= 0) wiredBridges.delete(bridge);
344
- else wiredBridges.set(bridge, remaining);
410
+ if (remaining <= 0) {
411
+ wiredBridges.delete(bridge);
412
+ if (options.autoDispose) bridge.dispose();
413
+ } else {
414
+ wiredBridges.set(bridge, remaining);
415
+ }
345
416
  }
346
417
  };
347
418
  }
@@ -387,6 +458,8 @@ function broadcastTextCursor(awareness, cmCursorFieldName, sharedText, textAncho
387
458
  awareness.setLocalStateField(cmCursorFieldName, { anchor: relAnchor, head: relHead });
388
459
  }
389
460
  var defaultOnWarning2 = (event) => console.warn(`[pm-cm] ${event.code}: ${event.message}`);
461
+ var awarenessRefCounts = /* @__PURE__ */ new WeakMap();
462
+ var awarenessFieldNames = /* @__PURE__ */ new WeakMap();
390
463
  function createCursorSyncPlugin(options) {
391
464
  const { awareness, serialize, sharedText } = options;
392
465
  const warn = options.onWarning ?? defaultOnWarning2;
@@ -436,53 +509,80 @@ function createCursorSyncPlugin(options) {
436
509
  }
437
510
  },
438
511
  view(editorView) {
512
+ const viewCount = awarenessRefCounts.get(awareness) ?? 0;
513
+ awarenessRefCounts.set(awareness, viewCount + 1);
514
+ let fieldNames = awarenessFieldNames.get(awareness);
515
+ if (!fieldNames) {
516
+ fieldNames = /* @__PURE__ */ new Set();
517
+ awarenessFieldNames.set(awareness, fieldNames);
518
+ }
519
+ fieldNames.add(cursorFieldName);
520
+ if (sharedText) {
521
+ fieldNames.add(cmCursorFieldName);
522
+ }
439
523
  let suppressCmReaction = false;
440
524
  let lastCmAbsAnchor = -1;
441
525
  let lastCmAbsHead = -1;
442
526
  let cmCursorHandledByListener = false;
527
+ let inAwarenessHandler = false;
443
528
  const handleAwarenessUpdate = ({ added, updated }) => {
529
+ if (inAwarenessHandler) return;
444
530
  if (suppressCmReaction) return;
445
531
  if (!sharedText?.doc) return;
446
532
  const localId = awareness.clientID;
447
533
  if (!updated.includes(localId) && !added.includes(localId)) return;
448
534
  const localState = awareness.getLocalState();
449
535
  if (!localState) return;
450
- const cmCursor = localState[cmCursorFieldName];
451
- if (!cmCursor?.anchor || !cmCursor?.head) {
452
- if (lastCmAbsAnchor !== -1) {
536
+ inAwarenessHandler = true;
537
+ try {
538
+ const cmCursor = localState[cmCursorFieldName];
539
+ if (!cmCursor?.anchor || !cmCursor?.head) {
540
+ if (lastCmAbsAnchor !== -1) {
541
+ awareness.setLocalStateField(cursorFieldName, null);
542
+ lastCmAbsAnchor = -1;
543
+ lastCmAbsHead = -1;
544
+ }
545
+ return;
546
+ }
547
+ const absAnchor = createAbsolutePositionFromRelativePosition(cmCursor.anchor, sharedText.doc);
548
+ const absHead = createAbsolutePositionFromRelativePosition(cmCursor.head, sharedText.doc);
549
+ if (!absAnchor || !absHead) {
453
550
  awareness.setLocalStateField(cursorFieldName, null);
454
551
  lastCmAbsAnchor = -1;
455
552
  lastCmAbsHead = -1;
553
+ return;
456
554
  }
457
- return;
458
- }
459
- const absAnchor = createAbsolutePositionFromRelativePosition(cmCursor.anchor, sharedText.doc);
460
- const absHead = createAbsolutePositionFromRelativePosition(cmCursor.head, sharedText.doc);
461
- if (!absAnchor || !absHead) {
555
+ if (absAnchor.type !== sharedText || absHead.type !== sharedText) {
556
+ return;
557
+ }
558
+ if (absAnchor.index === lastCmAbsAnchor && absHead.index === lastCmAbsHead) return;
559
+ const map = getOrBuildMap(editorView.state.doc);
560
+ if (!map) {
561
+ awareness.setLocalStateField(cursorFieldName, null);
562
+ return;
563
+ }
564
+ const pmAnchor = reverseCursorMapLookup(map, absAnchor.index);
565
+ const pmHead = reverseCursorMapLookup(map, absHead.index);
566
+ if (pmAnchor === null || pmHead === null) {
567
+ awareness.setLocalStateField(cursorFieldName, null);
568
+ return;
569
+ }
570
+ lastCmAbsAnchor = absAnchor.index;
571
+ lastCmAbsHead = absHead.index;
572
+ if (pendingCmCursor != null) {
573
+ cmCursorHandledByListener = true;
574
+ }
575
+ const ok = broadcastPmCursor(awareness, cursorFieldName, editorView, pmAnchor, pmHead);
576
+ if (!ok && !warnedSyncPluginMissing) {
577
+ warnedSyncPluginMissing = true;
578
+ warn({ code: "ysync-plugin-missing", message: "ySyncPlugin state not available \u2014 cursor broadcast skipped" });
579
+ }
580
+ } catch {
462
581
  awareness.setLocalStateField(cursorFieldName, null);
463
582
  lastCmAbsAnchor = -1;
464
583
  lastCmAbsHead = -1;
465
- return;
466
- }
467
- if (absAnchor.index === lastCmAbsAnchor && absHead.index === lastCmAbsHead) return;
468
- const map = getOrBuildMap(editorView.state.doc);
469
- if (!map) {
470
- awareness.setLocalStateField(cursorFieldName, null);
471
- return;
472
- }
473
- const pmAnchor = reverseCursorMapLookup(map, absAnchor.index);
474
- const pmHead = reverseCursorMapLookup(map, absHead.index);
475
- if (pmAnchor === null || pmHead === null) {
476
- awareness.setLocalStateField(cursorFieldName, null);
477
- return;
478
- }
479
- lastCmAbsAnchor = absAnchor.index;
480
- lastCmAbsHead = absHead.index;
481
- cmCursorHandledByListener = true;
482
- const ok = broadcastPmCursor(awareness, cursorFieldName, editorView, pmAnchor, pmHead);
483
- if (!ok && !warnedSyncPluginMissing) {
484
- warnedSyncPluginMissing = true;
485
- warn({ code: "ysync-plugin-missing", message: "ySyncPlugin state not available \u2014 cursor broadcast skipped" });
584
+ } finally {
585
+ inAwarenessHandler = false;
486
586
  }
487
587
  };
488
588
  if (sharedText) {
@@ -521,6 +621,9 @@ function createCursorSyncPlugin(options) {
521
621
  }
522
622
  } else {
523
623
  awareness.setLocalStateField(cursorFieldName, null);
624
+ if (sharedText?.doc) {
625
+ awareness.setLocalStateField(cmCursorFieldName, null);
626
+ }
524
627
  }
525
628
  }
526
629
  cmCursorHandledByListener = false;
@@ -555,9 +658,18 @@ function createCursorSyncPlugin(options) {
555
658
  if (sharedText) {
556
659
  awareness.off("update", handleAwarenessUpdate);
557
660
  }
558
- awareness.setLocalStateField(cursorFieldName, null);
559
- if (sharedText) {
560
- awareness.setLocalStateField(cmCursorFieldName, null);
661
+ const remaining = (awarenessRefCounts.get(awareness) ?? 1) - 1;
662
+ if (remaining <= 0) {
663
+ awarenessRefCounts.delete(awareness);
664
+ const fields = awarenessFieldNames.get(awareness);
665
+ if (fields) {
666
+ for (const field of fields) {
667
+ awareness.setLocalStateField(field, null);
668
+ }
669
+ awarenessFieldNames.delete(awareness);
670
+ }
671
+ } else {
672
+ awarenessRefCounts.set(awareness, remaining);
561
673
  }
562
674
  }
563
675
  };
@@ -598,7 +710,10 @@ function createCollabPlugins(schema, options) {
598
710
  yUndoPlugin(options.yUndoPluginOpts)
599
711
  ];
600
712
  if (options.bridge) {
601
- plugins.push(createBridgeSyncPlugin(options.bridge, { onWarning: options.onWarning }));
713
+ plugins.push(createBridgeSyncPlugin(options.bridge, {
714
+ onWarning: options.onWarning,
715
+ autoDispose: options.autoDisposeBridge
716
+ }));
602
717
  }
603
718
  if (enableCursorSync && options.serialize) {
604
719
  plugins.push(