@pm-cm/yjs 0.0.12 → 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 +123 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.js +123 -15
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -101,6 +101,12 @@ type ReplaceTextResult = {
|
|
|
101
101
|
} | {
|
|
102
102
|
ok: false;
|
|
103
103
|
reason: 'serialize-error';
|
|
104
|
+
} | {
|
|
105
|
+
ok: false;
|
|
106
|
+
reason: 'skip-pending';
|
|
107
|
+
} | {
|
|
108
|
+
ok: false;
|
|
109
|
+
reason: 'parse-failed';
|
|
104
110
|
};
|
|
105
111
|
/** Result of {@link replaceSharedProseMirror}. */
|
|
106
112
|
type ReplaceProseMirrorResult = {
|
|
@@ -207,6 +213,15 @@ type CollabPluginsOptions = {
|
|
|
207
213
|
yCursorPluginOpts?: YCursorPluginOpts;
|
|
208
214
|
/** Extra options forwarded to `yUndoPlugin`. */
|
|
209
215
|
yUndoPluginOpts?: YUndoPluginOpts;
|
|
216
|
+
/**
|
|
217
|
+
* When `true`, the bridge sync plugin calls `bridge.dispose()` when
|
|
218
|
+
* the last plugin instance for this bridge is destroyed. Default `false`.
|
|
219
|
+
*
|
|
220
|
+
* Enable this only when the factory owns the bridge lifecycle.
|
|
221
|
+
* When the caller creates and manages the bridge separately, leave
|
|
222
|
+
* this `false` and call `bridge.dispose()` manually.
|
|
223
|
+
*/
|
|
224
|
+
autoDisposeBridge?: boolean;
|
|
210
225
|
/** Called for non-fatal warnings. Propagated to child plugins. Default `console.warn`. */
|
|
211
226
|
onWarning?: OnWarning;
|
|
212
227
|
};
|
|
@@ -227,7 +242,7 @@ type BridgeSyncState = {
|
|
|
227
242
|
};
|
|
228
243
|
type BridgeSyncFailure = {
|
|
229
244
|
ok: false;
|
|
230
|
-
reason: 'detached' | 'serialize-error';
|
|
245
|
+
reason: 'detached' | 'serialize-error' | 'parse-failed';
|
|
231
246
|
};
|
|
232
247
|
/** Options for {@link createBridgeSyncPlugin}. */
|
|
233
248
|
type BridgeSyncPluginOptions = {
|
|
@@ -235,6 +250,11 @@ type BridgeSyncPluginOptions = {
|
|
|
235
250
|
onSyncFailure?: (result: BridgeSyncFailure, view: EditorView) => void;
|
|
236
251
|
/** Called for non-fatal warnings. Default `console.warn`. */
|
|
237
252
|
onWarning?: OnWarning;
|
|
253
|
+
/**
|
|
254
|
+
* When `true`, automatically call `bridge.dispose()` when the last
|
|
255
|
+
* plugin instance for this bridge is destroyed. Default `false`.
|
|
256
|
+
*/
|
|
257
|
+
autoDispose?: boolean;
|
|
238
258
|
};
|
|
239
259
|
/** ProseMirror plugin key for {@link createBridgeSyncPlugin}. Use to read the plugin state. */
|
|
240
260
|
declare const bridgeSyncPluginKey: PluginKey<BridgeSyncState>;
|
package/dist/index.d.ts
CHANGED
|
@@ -101,6 +101,12 @@ type ReplaceTextResult = {
|
|
|
101
101
|
} | {
|
|
102
102
|
ok: false;
|
|
103
103
|
reason: 'serialize-error';
|
|
104
|
+
} | {
|
|
105
|
+
ok: false;
|
|
106
|
+
reason: 'skip-pending';
|
|
107
|
+
} | {
|
|
108
|
+
ok: false;
|
|
109
|
+
reason: 'parse-failed';
|
|
104
110
|
};
|
|
105
111
|
/** Result of {@link replaceSharedProseMirror}. */
|
|
106
112
|
type ReplaceProseMirrorResult = {
|
|
@@ -207,6 +213,15 @@ type CollabPluginsOptions = {
|
|
|
207
213
|
yCursorPluginOpts?: YCursorPluginOpts;
|
|
208
214
|
/** Extra options forwarded to `yUndoPlugin`. */
|
|
209
215
|
yUndoPluginOpts?: YUndoPluginOpts;
|
|
216
|
+
/**
|
|
217
|
+
* When `true`, the bridge sync plugin calls `bridge.dispose()` when
|
|
218
|
+
* the last plugin instance for this bridge is destroyed. Default `false`.
|
|
219
|
+
*
|
|
220
|
+
* Enable this only when the factory owns the bridge lifecycle.
|
|
221
|
+
* When the caller creates and manages the bridge separately, leave
|
|
222
|
+
* this `false` and call `bridge.dispose()` manually.
|
|
223
|
+
*/
|
|
224
|
+
autoDisposeBridge?: boolean;
|
|
210
225
|
/** Called for non-fatal warnings. Propagated to child plugins. Default `console.warn`. */
|
|
211
226
|
onWarning?: OnWarning;
|
|
212
227
|
};
|
|
@@ -227,7 +242,7 @@ type BridgeSyncState = {
|
|
|
227
242
|
};
|
|
228
243
|
type BridgeSyncFailure = {
|
|
229
244
|
ok: false;
|
|
230
|
-
reason: 'detached' | 'serialize-error';
|
|
245
|
+
reason: 'detached' | 'serialize-error' | 'parse-failed';
|
|
231
246
|
};
|
|
232
247
|
/** Options for {@link createBridgeSyncPlugin}. */
|
|
233
248
|
type BridgeSyncPluginOptions = {
|
|
@@ -235,6 +250,11 @@ type BridgeSyncPluginOptions = {
|
|
|
235
250
|
onSyncFailure?: (result: BridgeSyncFailure, view: EditorView) => void;
|
|
236
251
|
/** Called for non-fatal warnings. Default `console.warn`. */
|
|
237
252
|
onWarning?: OnWarning;
|
|
253
|
+
/**
|
|
254
|
+
* When `true`, automatically call `bridge.dispose()` when the last
|
|
255
|
+
* plugin instance for this bridge is destroyed. Default `false`.
|
|
256
|
+
*/
|
|
257
|
+
autoDispose?: boolean;
|
|
238
258
|
};
|
|
239
259
|
/** ProseMirror plugin key for {@link createBridgeSyncPlugin}. Use to read the plugin state. */
|
|
240
260
|
declare const bridgeSyncPluginKey: PluginKey<BridgeSyncState>;
|
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)
|
|
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
|
-
|
|
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)
|
|
344
|
-
|
|
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,6 +509,17 @@ 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;
|
|
@@ -468,6 +552,9 @@ function createCursorSyncPlugin(options) {
|
|
|
468
552
|
lastCmAbsHead = -1;
|
|
469
553
|
return;
|
|
470
554
|
}
|
|
555
|
+
if (absAnchor.type !== sharedText || absHead.type !== sharedText) {
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
471
558
|
if (absAnchor.index === lastCmAbsAnchor && absHead.index === lastCmAbsHead) return;
|
|
472
559
|
const map = getOrBuildMap(editorView.state.doc);
|
|
473
560
|
if (!map) {
|
|
@@ -482,12 +569,18 @@ function createCursorSyncPlugin(options) {
|
|
|
482
569
|
}
|
|
483
570
|
lastCmAbsAnchor = absAnchor.index;
|
|
484
571
|
lastCmAbsHead = absHead.index;
|
|
485
|
-
|
|
572
|
+
if (pendingCmCursor != null) {
|
|
573
|
+
cmCursorHandledByListener = true;
|
|
574
|
+
}
|
|
486
575
|
const ok = broadcastPmCursor(awareness, cursorFieldName, editorView, pmAnchor, pmHead);
|
|
487
576
|
if (!ok && !warnedSyncPluginMissing) {
|
|
488
577
|
warnedSyncPluginMissing = true;
|
|
489
578
|
warn({ code: "ysync-plugin-missing", message: "ySyncPlugin state not available \u2014 cursor broadcast skipped" });
|
|
490
579
|
}
|
|
580
|
+
} catch {
|
|
581
|
+
awareness.setLocalStateField(cursorFieldName, null);
|
|
582
|
+
lastCmAbsAnchor = -1;
|
|
583
|
+
lastCmAbsHead = -1;
|
|
491
584
|
} finally {
|
|
492
585
|
inAwarenessHandler = false;
|
|
493
586
|
}
|
|
@@ -528,6 +621,9 @@ function createCursorSyncPlugin(options) {
|
|
|
528
621
|
}
|
|
529
622
|
} else {
|
|
530
623
|
awareness.setLocalStateField(cursorFieldName, null);
|
|
624
|
+
if (sharedText?.doc) {
|
|
625
|
+
awareness.setLocalStateField(cmCursorFieldName, null);
|
|
626
|
+
}
|
|
531
627
|
}
|
|
532
628
|
}
|
|
533
629
|
cmCursorHandledByListener = false;
|
|
@@ -562,9 +658,18 @@ function createCursorSyncPlugin(options) {
|
|
|
562
658
|
if (sharedText) {
|
|
563
659
|
awareness.off("update", handleAwarenessUpdate);
|
|
564
660
|
}
|
|
565
|
-
|
|
566
|
-
if (
|
|
567
|
-
|
|
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);
|
|
568
673
|
}
|
|
569
674
|
}
|
|
570
675
|
};
|
|
@@ -605,7 +710,10 @@ function createCollabPlugins(schema, options) {
|
|
|
605
710
|
yUndoPlugin(options.yUndoPluginOpts)
|
|
606
711
|
];
|
|
607
712
|
if (options.bridge) {
|
|
608
|
-
plugins.push(createBridgeSyncPlugin(options.bridge, {
|
|
713
|
+
plugins.push(createBridgeSyncPlugin(options.bridge, {
|
|
714
|
+
onWarning: options.onWarning,
|
|
715
|
+
autoDispose: options.autoDisposeBridge
|
|
716
|
+
}));
|
|
609
717
|
}
|
|
610
718
|
if (enableCursorSync && options.serialize) {
|
|
611
719
|
plugins.push(
|