@kerebron/extension-yjs 0.7.0 → 0.7.2

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.
Files changed (40) hide show
  1. package/esm/ExtensionYjs.d.ts.map +1 -1
  2. package/esm/ExtensionYjs.js +33 -0
  3. package/esm/ExtensionYjs.js.map +1 -1
  4. package/esm/WebsocketProvider.d.ts +1 -0
  5. package/esm/WebsocketProvider.d.ts.map +1 -1
  6. package/esm/WebsocketProvider.js +64 -41
  7. package/esm/WebsocketProvider.js.map +1 -1
  8. package/esm/binding/DiffViewer.d.ts +17 -0
  9. package/esm/binding/DiffViewer.d.ts.map +1 -0
  10. package/esm/binding/DiffViewer.js +96 -0
  11. package/esm/binding/DiffViewer.js.map +1 -0
  12. package/esm/binding/PmYjsBinding.d.ts +4 -0
  13. package/esm/binding/PmYjsBinding.d.ts.map +1 -1
  14. package/esm/binding/PmYjsBinding.js +49 -9
  15. package/esm/binding/PmYjsBinding.js.map +1 -1
  16. package/esm/binding/createNodeFromYElement.d.ts +1 -1
  17. package/esm/binding/createNodeFromYElement.d.ts.map +1 -1
  18. package/esm/binding/createNodeFromYElement.js.map +1 -1
  19. package/esm/debug.d.ts.map +1 -1
  20. package/esm/debug.js +11 -0
  21. package/esm/debug.js.map +1 -1
  22. package/esm/ui/selection.d.ts.map +1 -1
  23. package/esm/ui/selection.js +3 -3
  24. package/esm/ui/selection.js.map +1 -1
  25. package/esm/utils.d.ts +1 -1
  26. package/esm/utils.d.ts.map +1 -1
  27. package/esm/utils.js.map +1 -1
  28. package/esm/ySyncPlugin.d.ts.map +1 -1
  29. package/esm/ySyncPlugin.js +45 -14
  30. package/esm/ySyncPlugin.js.map +1 -1
  31. package/package.json +3 -3
  32. package/src/ExtensionYjs.ts +43 -0
  33. package/src/WebsocketProvider.ts +73 -61
  34. package/src/binding/DiffViewer.ts +138 -0
  35. package/src/binding/PmYjsBinding.ts +70 -10
  36. package/src/binding/createNodeFromYElement.ts +1 -1
  37. package/src/debug.ts +21 -0
  38. package/src/ui/selection.ts +5 -3
  39. package/src/utils.ts +1 -1
  40. package/src/ySyncPlugin.ts +58 -19
@@ -0,0 +1,138 @@
1
+ import * as Y from 'yjs';
2
+
3
+ import { Node } from 'prosemirror-model';
4
+ import { EditorState } from 'prosemirror-state';
5
+
6
+ import { isVisible } from '../utils.js';
7
+ import { YjsData } from './PmYjsBinding.js';
8
+ import { createNodeFromYElement } from './createNodeFromYElement.js';
9
+
10
+ export class DiffViewer {
11
+ prevSnapshot?: Y.Snapshot;
12
+ snapshot?: Y.Snapshot;
13
+
14
+ private historyYXmlFragment: Y.XmlFragment | undefined;
15
+ private historyDoc: Y.Doc | undefined;
16
+
17
+ constructor() {
18
+ }
19
+
20
+ isActive() {
21
+ return !!this.snapshot || !!this.prevSnapshot;
22
+ }
23
+
24
+ setSnapshot(yjs: YjsData, uSnapshot: Uint8Array, uPrevSnapshot?: Uint8Array) {
25
+ this.prevSnapshot = undefined;
26
+ if (uPrevSnapshot) {
27
+ this.prevSnapshot = Y.decodeSnapshotV2(uPrevSnapshot);
28
+ }
29
+
30
+ this.snapshot = Y.decodeSnapshotV2(uSnapshot);
31
+
32
+ this.historyDoc = Y.createDocFromSnapshot(
33
+ yjs.ydoc,
34
+ this.snapshot,
35
+ new Y.Doc({ gc: false }),
36
+ );
37
+
38
+ if (yjs.xmlFragment._item === null) {
39
+ /**
40
+ * If is a root type, we need to find the root key in the initial document
41
+ * and use it to get the history type.
42
+ */
43
+ const share: Map<string, Y.AbstractType<Y.YEvent<any>>> = yjs.ydoc.share;
44
+ const rootKey = Array.from(share.keys()).find(
45
+ (key) => share.get(key) === yjs.xmlFragment as Y.AbstractType<any>,
46
+ );
47
+ this.historyYXmlFragment = this.historyDoc.getXmlFragment(rootKey);
48
+ } else {
49
+ /**
50
+ * If it is a sub type, we use the item id to find the history type.
51
+ */
52
+ const historyStructs =
53
+ this.historyDoc.store.clients.get(yjs.xmlFragment._item.id.client) ??
54
+ [];
55
+ const itemIndex = Y.findIndexSS(
56
+ historyStructs,
57
+ yjs.xmlFragment._item.id.clock,
58
+ );
59
+ if (historyStructs[itemIndex] instanceof Y.GC) {
60
+ throw new Error('Incorrect type Y.GC');
61
+ }
62
+ const item: Y.Item = historyStructs[itemIndex];
63
+ const content: Y.ContentType = item.content;
64
+ this.historyYXmlFragment = content.type as Y.XmlFragment;
65
+ }
66
+ }
67
+
68
+ reset() {
69
+ this.snapshot = undefined;
70
+ this.prevSnapshot = undefined;
71
+ this.historyDoc = undefined;
72
+ this.historyYXmlFragment = undefined;
73
+ }
74
+
75
+ getFragmentContent(
76
+ state: EditorState,
77
+ ytr: Y.Transaction,
78
+ ): Node[] | undefined {
79
+ if (!this.historyYXmlFragment || !this.snapshot) {
80
+ return;
81
+ }
82
+
83
+ // before rendering, we are going to sanitize ops and split deleted ops
84
+ // if they were deleted by seperate users.
85
+ const pud: Y.PermanentUserData | undefined = undefined;
86
+ // pluginState.permanentUserData;
87
+ if (pud) {
88
+ pud.dss.forEach((ds) => {
89
+ Y.iterateDeletedStructs(ytr, ds, (_item) => {});
90
+ });
91
+ }
92
+ const computeYChange = (type: 'removed' | 'added', id: Y.ID) => {
93
+ const user = pud
94
+ ? (type === 'added'
95
+ ? pud.getUserByClientId(id.client)
96
+ : pud.getUserByDeletedId(id))
97
+ : undefined;
98
+ return {
99
+ user,
100
+ type,
101
+ };
102
+ };
103
+
104
+ const deleteSet = this.prevSnapshot
105
+ ? this.prevSnapshot.ds
106
+ : Y.emptySnapshot.ds;
107
+
108
+ // Create document fragment and render
109
+ const fragmentContent = Y.typeListToArraySnapshot(
110
+ this.historyYXmlFragment,
111
+ new Y.Snapshot(deleteSet, this.snapshot.sv),
112
+ ).map((t) => {
113
+ if (
114
+ !t._item.deleted || isVisible(t._item, this.snapshot) ||
115
+ isVisible(t._item, this.prevSnapshot)
116
+ ) {
117
+ return createNodeFromYElement(
118
+ t,
119
+ state.schema,
120
+ { mapping: new Map(), isOverlappingMark: new Map() },
121
+ this.snapshot,
122
+ this.prevSnapshot,
123
+ computeYChange,
124
+ );
125
+ } else {
126
+ // No need to render elements that are not visible by either snapshot.
127
+ // If a client adds and deletes content in the same snapshot the element is not visible by either snapshot.
128
+ return null;
129
+ }
130
+ }).filter((n) => n !== null);
131
+
132
+ return fragmentContent;
133
+ }
134
+
135
+ getHistoryDoc(): Y.Doc | undefined {
136
+ return this.historyDoc;
137
+ }
138
+ }
@@ -15,6 +15,7 @@ import { yXmlFragmentToProseMirrorRootNode } from './convertUtils.js';
15
15
  import { updateYFragment } from './updateYFragment.js';
16
16
  import { BindingMetadata } from './BindingMetadata.js';
17
17
  import { createNodeIfNotExists } from './createNodeFromYElement.js';
18
+ import { DiffViewer } from './DiffViewer.js';
18
19
 
19
20
  type Mutex = (f: () => void, g?: () => void) => void;
20
21
 
@@ -44,6 +45,7 @@ export class PmYjsBinding extends EventTarget {
44
45
 
45
46
  private yjs: YjsData | undefined;
46
47
  private selectionStash: SelectionStash | undefined;
48
+ public readonly diffViewer: DiffViewer;
47
49
 
48
50
  public addToYjsHistory = true;
49
51
  private hasImported = false;
@@ -63,6 +65,7 @@ export class PmYjsBinding extends EventTarget {
63
65
  constructor(private readonly editor: CoreEditor) {
64
66
  super();
65
67
 
68
+ this.diffViewer = new DiffViewer();
66
69
  this.bindingMetadata = { mapping: new Map(), isOverlappingMark: new Map() };
67
70
 
68
71
  this.mux = createMutex();
@@ -90,6 +93,8 @@ export class PmYjsBinding extends EventTarget {
90
93
  const tr = this.editor.state.tr;
91
94
  this.leaveRoom(tr);
92
95
  this.editor.dispatchTransaction(tr);
96
+ this.diffViewer.reset();
97
+ this.selectionStash?.destroy();
93
98
  }
94
99
 
95
100
  changeUser(user: User) {
@@ -104,23 +109,26 @@ export class PmYjsBinding extends EventTarget {
104
109
  createYjsProvider: CreateYjsProvider,
105
110
  tr: Transaction,
106
111
  ) {
112
+ this.connectionState = 'joining';
113
+ this.hasImported = false;
114
+
107
115
  if (this.provider) {
108
116
  this.provider.removeEventListener('synced', this.syncedHandler);
109
117
  this.provider.destroy();
110
- this.hasImported = false;
111
118
  this.provider = undefined;
112
119
  }
113
120
 
114
- this.addToYjsHistory = true;
115
-
116
- this.connectionState = 'joining';
117
121
  const [provider, ydoc] = createYjsProvider(
118
122
  roomId,
119
123
  );
124
+
125
+ this.addToYjsHistory = true;
126
+ this.provider = provider;
127
+
120
128
  this.bindingMetadata.mapping.clear();
121
129
  this.bindingMetadata.isOverlappingMark.clear();
130
+ this.diffViewer.reset();
122
131
 
123
- this.provider = provider;
124
132
  const fieldName = 'kerebron:' + this.editor.schema.topNodeType.name;
125
133
  this.yjs = { ydoc, xmlFragment: ydoc.getXmlFragment(fieldName) };
126
134
  this.selectionStash = new SelectionStash(
@@ -146,6 +154,10 @@ export class PmYjsBinding extends EventTarget {
146
154
  this.addToYjsHistory = true;
147
155
  this.provider = undefined;
148
156
 
157
+ this.bindingMetadata.mapping.clear();
158
+ this.bindingMetadata.isOverlappingMark.clear();
159
+ this.diffViewer.reset();
160
+
149
161
  this.selectionStash?.destroy();
150
162
  this.selectionStash = undefined;
151
163
  this.yjs?.xmlFragment.unobserveDeep(this._observeFunction);
@@ -181,12 +193,14 @@ export class PmYjsBinding extends EventTarget {
181
193
  return;
182
194
  }
183
195
 
196
+ if (this.diffViewer.isActive()) {
197
+ return;
198
+ }
199
+
184
200
  const state = this.editor.state;
185
201
 
186
202
  const { xmlFragment } = this.yjs;
187
203
 
188
- // TODO handleSnapShot
189
-
190
204
  const mapping = this.bindingMetadata.mapping;
191
205
  const delType = (_: any, type: Y.AbstractType<any>) =>
192
206
  mapping.delete(type);
@@ -212,9 +226,6 @@ export class PmYjsBinding extends EventTarget {
212
226
  ).filter((n) => n !== null);
213
227
 
214
228
  const tr = state.tr;
215
- if (ytr.origin instanceof Y.UndoManager) {
216
- } else {
217
- }
218
229
  tr.setMeta('addToYjsHistory', false);
219
230
  tr.replace(
220
231
  0,
@@ -236,6 +247,10 @@ export class PmYjsBinding extends EventTarget {
236
247
  return;
237
248
  }
238
249
 
250
+ if (this.diffViewer.isActive()) {
251
+ return;
252
+ }
253
+
239
254
  const doc = this.editor.state.doc;
240
255
  const { ydoc, xmlFragment } = this.yjs;
241
256
 
@@ -297,4 +312,49 @@ export class PmYjsBinding extends EventTarget {
297
312
  getSelectionStash(): SelectionStash | undefined {
298
313
  return this.selectionStash;
299
314
  }
315
+
316
+ isEditable() {
317
+ return !this.diffViewer.isActive();
318
+ }
319
+
320
+ setSnapshot(
321
+ snapshot: Uint8Array<ArrayBufferLike>,
322
+ prevSnapshot: Uint8Array<ArrayBufferLike> | undefined,
323
+ ) {
324
+ if (!this.yjs) {
325
+ return;
326
+ }
327
+ this.diffViewer.setSnapshot(this.yjs, snapshot, prevSnapshot);
328
+
329
+ // clear mapping because we are going to rerender
330
+ this.bindingMetadata.mapping.clear();
331
+ this.bindingMetadata.isOverlappingMark.clear();
332
+
333
+ this.mux(() => {
334
+ const historyDoc = this.diffViewer.getHistoryDoc();
335
+ if (!historyDoc) {
336
+ return;
337
+ }
338
+
339
+ historyDoc.transact((ytr) => {
340
+ const state = this.editor.state;
341
+ const fragmentContent = this.diffViewer.getFragmentContent(state, ytr);
342
+
343
+ if (!fragmentContent) {
344
+ return;
345
+ }
346
+
347
+ const tr = state.tr;
348
+ tr.replace(
349
+ 0,
350
+ state.doc.content.size,
351
+ new Slice(Fragment.from(fragmentContent), 0, 0),
352
+ );
353
+
354
+ tr.setMeta('addToHistory', false);
355
+ tr.setMeta(ySyncPluginKey, { isChangeOrigin: true }),
356
+ this.editor.dispatchTransaction(tr);
357
+ }, ySyncPluginKey);
358
+ });
359
+ }
300
360
  }
@@ -51,7 +51,7 @@ export const createNodeFromYElement = (
51
51
  meta: BindingMetadata,
52
52
  snapshot?: Y.Snapshot,
53
53
  prevSnapshot?: Y.Snapshot,
54
- computeYChange?: (arg0: 'removed' | 'added', arg1: Y.ID) => any,
54
+ computeYChange?: (changeType: 'removed' | 'added', yid: Y.ID) => any,
55
55
  ): PModel.Node | null => {
56
56
  const children: PModel.Node[] = [];
57
57
  const createChildren = (type: Y.XmlElement | Y.XmlText) => {
package/src/debug.ts CHANGED
@@ -177,6 +177,27 @@ export function debugYNode(
177
177
  return retVal;
178
178
  }
179
179
 
180
+ if (node instanceof Y.AbstractType) {
181
+ if (!config.isVisible(node._item)) {
182
+ return '';
183
+ }
184
+
185
+ const atype: Y.AbstractType<any> = node;
186
+
187
+ retVal += config.renderDeleted(
188
+ `Y.AbstractType(${atype._length})`,
189
+ node._item,
190
+ );
191
+ retVal += yDebugClient(node._item);
192
+ retVal += '\n';
193
+
194
+ retVal += indentText(
195
+ JSON.stringify(atype._item),
196
+ 1,
197
+ );
198
+ return retVal;
199
+ }
200
+
180
201
  if ('object' !== typeof node) {
181
202
  retVal += '' + node;
182
203
  return retVal;
@@ -186,11 +186,13 @@ export class SelectionStash {
186
186
  if (!('root' in view)) {
187
187
  return false;
188
188
  }
189
- const selection = view.root?.getSelection(); // https://stackoverflow.com/questions/62054839/shadowroot-getselection
189
+ const selection = document.getSelection();
190
190
 
191
- if (!selection || selection.anchorNode == null) return false;
191
+ if (
192
+ !selection || selection.anchorNode == null || selection.focusNode == null
193
+ ) return false;
192
194
 
193
- const range = document.createRange(); // https://github.com/yjs/y-prosemirror/pull/193
195
+ const range = document.createRange();
194
196
  range.setStart(selection.anchorNode, selection.anchorOffset);
195
197
  range.setEnd(selection.focusNode, selection.focusOffset);
196
198
 
package/src/utils.ts CHANGED
@@ -13,7 +13,7 @@ const _convolute = (digest: Uint8Array) => {
13
13
  export const hashOfJSON = (json: any) =>
14
14
  buf.toBase64(_convolute(sha256.digest(buf.encodeAny(json))));
15
15
 
16
- export const isVisible = (item: Y.Item, snapshot: Y.Snapshot) =>
16
+ export const isVisible = (item: Y.Item, snapshot?: Y.Snapshot) =>
17
17
  snapshot === undefined ? !item.deleted : (snapshot.sv.has(item.id.client) &&
18
18
  (snapshot.sv.get(item.id.client)!) > item.id.clock &&
19
19
  !Y.isDeleted(snapshot.ds, item.id));
@@ -30,7 +30,15 @@ interface YSyncMeta {
30
30
  leaveRoom?: boolean;
31
31
  isChangeOrigin?: boolean;
32
32
  isUndoRedoOperation?: boolean;
33
- restore?: any;
33
+ getYSnapshot?: {
34
+ resolve: (snapshot: Uint8Array) => void;
35
+ reject: (reason: any) => void;
36
+ };
37
+ setYSnapshot?: {
38
+ prevSnapshot?: Uint8Array;
39
+ snapshot: Uint8Array;
40
+ };
41
+ resetYSnapshot?: boolean;
34
42
  }
35
43
 
36
44
  /**
@@ -50,7 +58,8 @@ export const ySyncPlugin = (
50
58
  const plugin: Plugin<YSyncPluginState> = new Plugin<YSyncPluginState>({
51
59
  props: {
52
60
  editable: (state) => {
53
- return true;
61
+ const syncState = ySyncPluginKey.getState(state)!;
62
+ return syncState.binding.isEditable();
54
63
  },
55
64
  },
56
65
  key: ySyncPluginKey,
@@ -119,6 +128,34 @@ export const ySyncPlugin = (
119
128
  pluginState.isUndoRedoOperation = !!pluginMeta?.isChangeOrigin &&
120
129
  !!pluginMeta?.isUndoRedoOperation;
121
130
 
131
+ if (pluginMeta?.getYSnapshot) {
132
+ const yjs = pluginState.binding.getYjs();
133
+ if (yjs) {
134
+ const snapshot = Y.snapshot(yjs.ydoc);
135
+ pluginMeta.getYSnapshot.resolve(Y.encodeSnapshotV2(snapshot));
136
+ } else {
137
+ if (pluginMeta.getYSnapshot.reject) {
138
+ pluginMeta.getYSnapshot.reject(new Error('No yjs'));
139
+ } else {
140
+ throw new Error('No yjs');
141
+ }
142
+ }
143
+ return pluginState;
144
+ }
145
+
146
+ if (pluginMeta?.resetYSnapshot) {
147
+ pluginState.binding.diffViewer.reset();
148
+ return pluginState;
149
+ }
150
+
151
+ if (pluginMeta?.setYSnapshot) {
152
+ const { prevSnapshot, snapshot } = pluginMeta?.setYSnapshot;
153
+ setTimeout(() => { // Prevent from snapshot being overwritten by current tr
154
+ pluginState.binding.setSnapshot(snapshot, prevSnapshot);
155
+ }, 0);
156
+ return pluginState;
157
+ }
158
+
122
159
  return pluginState;
123
160
  },
124
161
  },
@@ -139,27 +176,29 @@ export const ySyncPlugin = (
139
176
  }
140
177
 
141
178
  const binding = pluginState.binding;
142
- if (
143
- // If the content doesn't change initially, we don't render anything to Yjs
144
- // If the content was cleared by a user action, we want to catch the change and
145
- // represent it in Yjs
146
- initialContentChanged ||
147
- view.state.doc.content.findDiffStart(
148
- view.state.doc.type.createAndFill()!.content,
149
- ) !== null
150
- ) {
151
- initialContentChanged = true;
179
+ if (binding.isEditable()) {
152
180
  if (
153
- pluginState.binding.addToYjsHistory === false &&
154
- !pluginState.isChangeOrigin
181
+ // If the content doesn't change initially, we don't render anything to Yjs
182
+ // If the content was cleared by a user action, we want to catch the change and
183
+ // represent it in Yjs
184
+ initialContentChanged ||
185
+ view.state.doc.content.findDiffStart(
186
+ view.state.doc.type.createAndFill()!.content,
187
+ ) !== null
155
188
  ) {
156
- const yUndoPluginState = yUndoPluginKey.getState(view.state);
157
- if (yUndoPluginState?.undoManager) {
158
- yUndoPluginState.undoManager.stopCapturing();
189
+ initialContentChanged = true;
190
+ if (
191
+ pluginState.binding.addToYjsHistory === false &&
192
+ !pluginState.isChangeOrigin
193
+ ) {
194
+ const yUndoPluginState = yUndoPluginKey.getState(view.state);
195
+ if (yUndoPluginState?.undoManager) {
196
+ yUndoPluginState.undoManager.stopCapturing();
197
+ }
159
198
  }
160
- }
161
199
 
162
- binding.pmChanged();
200
+ binding.pmChanged();
201
+ }
163
202
  }
164
203
  },
165
204
  destroy: () => {