@kerebron/extension-yjs 0.6.6 → 0.7.0

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 (90) hide show
  1. package/esm/ExtensionYjs.d.ts +3 -11
  2. package/esm/ExtensionYjs.d.ts.map +1 -1
  3. package/esm/ExtensionYjs.js +38 -45
  4. package/esm/ExtensionYjs.js.map +1 -1
  5. package/esm/WebsocketProvider.d.ts +69 -0
  6. package/esm/WebsocketProvider.d.ts.map +1 -0
  7. package/esm/WebsocketProvider.js +354 -0
  8. package/esm/WebsocketProvider.js.map +1 -0
  9. package/esm/YjsProvider.d.ts +48 -0
  10. package/esm/YjsProvider.d.ts.map +1 -0
  11. package/esm/YjsProvider.js +12 -0
  12. package/esm/YjsProvider.js.map +1 -0
  13. package/esm/_dnt.shims.d.ts +2 -0
  14. package/esm/_dnt.shims.d.ts.map +1 -0
  15. package/esm/_dnt.shims.js +58 -0
  16. package/esm/_dnt.shims.js.map +1 -0
  17. package/esm/binding/BindingMetadata.d.ts +6 -0
  18. package/esm/binding/BindingMetadata.d.ts.map +1 -0
  19. package/esm/binding/BindingMetadata.js +2 -0
  20. package/esm/binding/BindingMetadata.js.map +1 -0
  21. package/esm/binding/PmYjsBinding.d.ts +41 -0
  22. package/esm/binding/PmYjsBinding.d.ts.map +1 -0
  23. package/esm/binding/PmYjsBinding.js +190 -0
  24. package/esm/binding/PmYjsBinding.js.map +1 -0
  25. package/esm/binding/convertUtils.d.ts +48 -0
  26. package/esm/binding/convertUtils.d.ts.map +1 -0
  27. package/esm/binding/convertUtils.js +80 -0
  28. package/esm/binding/convertUtils.js.map +1 -0
  29. package/esm/{createNodeFromYElement.d.ts → binding/createNodeFromYElement.d.ts} +1 -1
  30. package/esm/binding/createNodeFromYElement.d.ts.map +1 -0
  31. package/esm/{createNodeFromYElement.js → binding/createNodeFromYElement.js} +2 -2
  32. package/esm/binding/createNodeFromYElement.js.map +1 -0
  33. package/esm/{updateYFragment.d.ts → binding/updateYFragment.d.ts} +3 -3
  34. package/esm/binding/updateYFragment.d.ts.map +1 -0
  35. package/esm/{updateYFragment.js → binding/updateYFragment.js} +10 -7
  36. package/esm/binding/updateYFragment.js.map +1 -0
  37. package/esm/lib.d.ts +1 -7
  38. package/esm/lib.d.ts.map +1 -1
  39. package/esm/lib.js +1 -200
  40. package/esm/lib.js.map +1 -1
  41. package/esm/position.d.ts +8 -0
  42. package/esm/position.d.ts.map +1 -0
  43. package/esm/position.js +165 -0
  44. package/esm/position.js.map +1 -0
  45. package/esm/ui/selection.d.ts +29 -0
  46. package/esm/ui/selection.d.ts.map +1 -0
  47. package/esm/ui/selection.js +129 -0
  48. package/esm/ui/selection.js.map +1 -0
  49. package/esm/yPositionPlugin.d.ts +6 -1
  50. package/esm/yPositionPlugin.d.ts.map +1 -1
  51. package/esm/yPositionPlugin.js +91 -50
  52. package/esm/yPositionPlugin.js.map +1 -1
  53. package/esm/ySyncPlugin.d.ts +5 -22
  54. package/esm/ySyncPlugin.d.ts.map +1 -1
  55. package/esm/ySyncPlugin.js +54 -116
  56. package/esm/ySyncPlugin.js.map +1 -1
  57. package/esm/yUndoPlugin.d.ts +11 -10
  58. package/esm/yUndoPlugin.d.ts.map +1 -1
  59. package/esm/yUndoPlugin.js +90 -52
  60. package/esm/yUndoPlugin.js.map +1 -1
  61. package/package.json +9 -6
  62. package/src/ExtensionYjs.ts +55 -67
  63. package/src/WebsocketProvider.ts +516 -0
  64. package/src/YjsProvider.ts +75 -0
  65. package/src/_dnt.shims.ts +60 -0
  66. package/src/binding/BindingMetadata.ts +6 -0
  67. package/src/binding/PmYjsBinding.ts +300 -0
  68. package/src/binding/convertUtils.ts +124 -0
  69. package/src/{createNodeFromYElement.ts → binding/createNodeFromYElement.ts} +3 -3
  70. package/src/{updateYFragment.ts → binding/updateYFragment.ts} +15 -8
  71. package/src/lib.ts +4 -230
  72. package/src/position.ts +191 -0
  73. package/src/ui/selection.ts +216 -0
  74. package/src/yPositionPlugin.ts +122 -74
  75. package/src/ySyncPlugin.ts +87 -170
  76. package/src/yUndoPlugin.ts +113 -62
  77. package/esm/ProsemirrorBinding.d.ts +0 -60
  78. package/esm/ProsemirrorBinding.d.ts.map +0 -1
  79. package/esm/ProsemirrorBinding.js +0 -405
  80. package/esm/ProsemirrorBinding.js.map +0 -1
  81. package/esm/createNodeFromYElement.d.ts.map +0 -1
  82. package/esm/createNodeFromYElement.js.map +0 -1
  83. package/esm/updateYFragment.d.ts.map +0 -1
  84. package/esm/updateYFragment.js.map +0 -1
  85. package/esm/userColors.d.ts +0 -5
  86. package/esm/userColors.d.ts.map +0 -1
  87. package/esm/userColors.js +0 -11
  88. package/esm/userColors.js.map +0 -1
  89. package/src/ProsemirrorBinding.ts +0 -607
  90. package/src/userColors.ts +0 -10
@@ -1,40 +1,23 @@
1
- import type { Node, Schema } from 'prosemirror-model';
2
- import type { Plugin } from 'prosemirror-state';
1
+ import type { EditorState, Plugin, Transaction } from 'prosemirror-state';
3
2
 
4
3
  import * as Y from 'yjs';
5
- import * as awarenessProtocol from 'y-protocols/awareness';
6
4
 
7
- import { Converter, CoreEditor, Extension } from '@kerebron/editor';
5
+ import { Extension } from '@kerebron/editor';
8
6
  import type {
9
7
  CommandFactories,
8
+ CommandFactory,
10
9
  CommandShortcuts,
11
10
  } from '@kerebron/editor/commands';
12
11
 
13
- import { ySyncPluginKey } from './keys.js';
14
12
  import { ySyncPlugin } from './ySyncPlugin.js';
15
13
  import { yPositionPlugin } from './yPositionPlugin.js';
16
- import { redo, undo, yUndoPlugin } from './yUndoPlugin.js';
17
-
18
- export interface YjsProvider {
19
- on(eventName: string, callback: (event: any) => void): void;
20
- awareness: awarenessProtocol.Awareness;
21
- }
22
-
23
- function stringToIndex(str: string, arrayLength: number) {
24
- let hash = 0;
25
-
26
- for (let i = 0; i < str.length; i++) {
27
- hash = (hash << 5) - hash + str.charCodeAt(i);
28
- hash |= 0; // force 32-bit integer
29
- }
30
-
31
- return Math.abs(hash) % arrayLength;
32
- }
14
+ import { redoCommand, undoCommand, yUndoPlugin } from './yUndoPlugin.js';
15
+ import { ySyncPluginKey } from './keys.js';
33
16
 
34
- export type CreateWsProvider = (roomId: string) => [YjsProvider, Y.Doc];
17
+ import type { CreateYjsProvider } from './YjsProvider.js';
35
18
 
36
19
  export interface YjsConfig {
37
- createWsProvider: CreateWsProvider;
20
+ createYjsProvider: CreateYjsProvider;
38
21
  }
39
22
 
40
23
  export class ExtensionYjs extends Extension {
@@ -43,11 +26,55 @@ export class ExtensionYjs extends Extension {
43
26
  override conflicts = ['history'];
44
27
  requires = ['remote-selection'];
45
28
 
46
- // declare type Command = (state: EditorState, dispatch?: (tr: Transaction) => void, view?: EditorView) => boolean;
47
29
  override getCommandFactories(): Partial<CommandFactories> {
30
+ const changeRoom: CommandFactory = (roomId: string) => {
31
+ return (state: EditorState, dispatch?: (tr: Transaction) => void) => {
32
+ const tr = state.tr;
33
+ tr.setMeta(ySyncPluginKey, { changeRoom: { roomId } });
34
+
35
+ if (dispatch) {
36
+ dispatch(tr);
37
+ }
38
+
39
+ return true;
40
+ };
41
+ };
42
+
43
+ const leaveRoom: CommandFactory = () => {
44
+ return (state: EditorState, dispatch?: (tr: Transaction) => void) => {
45
+ const tr = state.tr;
46
+ tr.setMeta(ySyncPluginKey, { leaveRoom: true });
47
+
48
+ if (dispatch) {
49
+ dispatch(tr);
50
+ }
51
+
52
+ return true;
53
+ };
54
+ };
55
+
56
+ const getYDoc: CommandFactory = (
57
+ { resolve, reject }: {
58
+ resolve: (doc: Y.Doc) => void;
59
+ reject: (reason: any) => void;
60
+ },
61
+ ) => {
62
+ return (state: EditorState, dispatch?: (tr: Transaction) => void) => {
63
+ const tr = state.tr;
64
+ tr.setMeta(ySyncPluginKey, { getYDoc: { resolve, reject } });
65
+ if (dispatch) {
66
+ dispatch(tr);
67
+ }
68
+ return true;
69
+ };
70
+ };
71
+
48
72
  return {
49
- 'undo': () => undo,
50
- 'redo': () => redo,
73
+ getYDoc,
74
+ changeRoom,
75
+ leaveRoom,
76
+ 'undo': () => undoCommand,
77
+ 'redo': () => redoCommand,
51
78
  };
52
79
  }
53
80
 
@@ -62,48 +89,9 @@ export class ExtensionYjs extends Extension {
62
89
  super();
63
90
  }
64
91
 
65
- // changeUser(userName: string) {
66
- // const idx = stringToIndex(userName, userColors.length);
67
- // const userColor = userColors[idx];
68
- // this.wsProvider.awareness.setLocalStateField('user', {
69
- // name: userName,
70
- // color: userColor.color,
71
- // colorLight: userColor.light,
72
- // });
73
- // }
74
- // //
75
-
76
- override getConverters(
77
- editor: CoreEditor,
78
- schema: Schema,
79
- ): Record<string, Converter> {
80
- return {
81
- 'yjs': {
82
- fromDoc: async (document: Node): Promise<Uint8Array> => {
83
- throw new Error('Not implemented');
84
- },
85
- toDoc: async (buffer: Uint8Array): Promise<Node> => {
86
- const roomId = new TextDecoder().decode(buffer);
87
-
88
- const tr = editor.state.tr.setMeta(ySyncPluginKey, {
89
- roomId: '',
90
- });
91
- editor.view.dispatch(tr);
92
-
93
- setTimeout(() => {
94
- const tr = editor.state.tr.setMeta(ySyncPluginKey, { roomId });
95
- editor.view.dispatch(tr);
96
- }, 100);
97
-
98
- return schema.topNodeType.createAndFill()!;
99
- },
100
- },
101
- };
102
- }
103
-
104
92
  override getProseMirrorPlugins(): Plugin[] {
105
93
  return [
106
- ySyncPlugin(this.editor.schema, this.config.createWsProvider),
94
+ ySyncPlugin(this.editor, this.config.createYjsProvider),
107
95
  yPositionPlugin(this.editor),
108
96
  yUndoPlugin(),
109
97
  ];
@@ -0,0 +1,516 @@
1
+ import * as Y from 'yjs';
2
+ import * as bc from 'lib0/broadcastchannel';
3
+ import * as time from 'lib0/time';
4
+ import * as encoding from 'lib0/encoding';
5
+ import * as decoding from 'lib0/decoding';
6
+ import * as syncProtocol from 'y-protocols/sync';
7
+ import * as authProtocol from 'y-protocols/auth';
8
+ import * as awarenessProtocol from 'y-protocols/awareness';
9
+ import * as math from 'lib0/math';
10
+ import * as url from 'lib0/url';
11
+
12
+ import {
13
+ messageAuth,
14
+ messageAwareness,
15
+ MessageHandler,
16
+ messageQueryAwareness,
17
+ messageSync,
18
+ MessageType,
19
+ YjsProvider,
20
+ } from './YjsProvider.js';
21
+
22
+ // @todo - this should depend on awareness.outdatedTime
23
+ const messageReconnectTimeout = 30000;
24
+
25
+ const permissionDeniedHandler = (provider: WebsocketProvider, reason: string) =>
26
+ console.warn(`Permission denied to access ${provider.url}.\n${reason}`);
27
+
28
+ const readMessage = (
29
+ provider: WebsocketProvider,
30
+ buf: Uint8Array,
31
+ emitSynced: boolean,
32
+ ): encoding.Encoder => {
33
+ const decoder = decoding.createDecoder(buf);
34
+ const encoder = encoding.createEncoder();
35
+ const messageType = decoding.readVarUint(decoder);
36
+ const messageHandler = provider.messageHandlers[messageType];
37
+ if (messageHandler) {
38
+ messageHandler(encoder, decoder, provider, emitSynced, messageType);
39
+ } else {
40
+ console.error('Unable to compute message');
41
+ }
42
+ return encoder;
43
+ };
44
+
45
+ /**
46
+ * Outsource this function so that a new websocket connection is created immediately.
47
+ * I suspect that the `ws.onclose` event is not always fired if there are network issues.
48
+ */
49
+ const closeWebsocketConnection = (
50
+ provider: WebsocketProvider,
51
+ ws: WebSocket,
52
+ event: CloseEvent | null,
53
+ ) => {
54
+ if (ws === provider.ws) {
55
+ provider.dispatchEvent(
56
+ new CustomEvent('connection-close', { detail: { event, provider } }),
57
+ );
58
+ provider.ws = undefined;
59
+ ws.close();
60
+ provider.wsconnecting = false;
61
+ if (provider.wsconnected) {
62
+ provider.wsconnected = false;
63
+ provider.synced = false;
64
+ // update awareness (all users except local left)
65
+ awarenessProtocol.removeAwarenessStates(
66
+ provider.awareness,
67
+ Array.from(provider.awareness.getStates().keys()).filter((client) =>
68
+ client !== provider.doc.clientID
69
+ ),
70
+ provider,
71
+ );
72
+ provider.dispatchEvent(
73
+ new CustomEvent('status', { detail: { status: 'disconnected' } }),
74
+ );
75
+ } else {
76
+ provider.wsUnsuccessfulReconnects++;
77
+ }
78
+ // Start with no reconnect timeout and increase timeout by
79
+ // using exponential backoff starting with 100ms
80
+ if (!provider.destroyed) {
81
+ provider.setupWSTimeout = setTimeout(
82
+ setupWS,
83
+ math.min(
84
+ math.pow(2, provider.wsUnsuccessfulReconnects) * 100,
85
+ provider.maxBackoffTime,
86
+ ),
87
+ provider,
88
+ );
89
+ }
90
+ }
91
+ };
92
+
93
+ const setupWS = (provider: WebsocketProvider) => {
94
+ provider.setupWSTimeout = undefined;
95
+ if (provider.shouldConnect && !provider.ws) {
96
+ const websocket = new provider._WS(provider.url, provider.protocols);
97
+ websocket.binaryType = 'arraybuffer';
98
+ provider.ws = websocket;
99
+ provider.wsconnecting = true;
100
+ provider.wsconnected = false;
101
+ provider.synced = false;
102
+
103
+ websocket.onmessage = (event) => {
104
+ provider.wsLastMessageReceived = time.getUnixTime();
105
+ const encoder = readMessage(provider, new Uint8Array(event.data), true);
106
+ if (encoding.length(encoder) > 1) {
107
+ websocket.send(encoding.toUint8Array(encoder));
108
+ }
109
+ };
110
+ websocket.onerror = (event) => {
111
+ provider.dispatchEvent(
112
+ new CustomEvent('connection-error', { detail: { event, provider } }),
113
+ );
114
+ };
115
+ websocket.onclose = (event) => {
116
+ closeWebsocketConnection(provider, websocket, event);
117
+ };
118
+ websocket.onopen = () => {
119
+ provider.wsLastMessageReceived = time.getUnixTime();
120
+ provider.wsconnecting = false;
121
+ provider.wsconnected = true;
122
+ provider.wsUnsuccessfulReconnects = 0;
123
+ provider.dispatchEvent(
124
+ new CustomEvent('status', { detail: { status: 'connected' } }),
125
+ );
126
+ // always send sync step 1 when connected
127
+ const encoder = encoding.createEncoder();
128
+ encoding.writeVarUint(encoder, messageSync);
129
+ syncProtocol.writeSyncStep1(encoder, provider.doc);
130
+ websocket.send(encoding.toUint8Array(encoder));
131
+ // broadcast local awareness state
132
+ if (provider.awareness.getLocalState() !== null) {
133
+ const encoderAwarenessState = encoding.createEncoder();
134
+ encoding.writeVarUint(encoderAwarenessState, messageAwareness);
135
+ encoding.writeVarUint8Array(
136
+ encoderAwarenessState,
137
+ awarenessProtocol.encodeAwarenessUpdate(provider.awareness, [
138
+ provider.doc.clientID,
139
+ ]),
140
+ );
141
+ websocket.send(encoding.toUint8Array(encoderAwarenessState));
142
+ }
143
+ };
144
+ provider.dispatchEvent(
145
+ new CustomEvent('status', { detail: { status: 'connecting' } }),
146
+ );
147
+ }
148
+ };
149
+
150
+ const broadcastMessage = (provider: WebsocketProvider, buf: Uint8Array) => {
151
+ const ws = provider.ws;
152
+ if (provider.wsconnected && ws && ws.readyState === ws.OPEN) {
153
+ ws.send(buf);
154
+ }
155
+ if (provider.bcconnected) {
156
+ bc.publish(provider.bcChannel, buf, provider);
157
+ }
158
+ };
159
+
160
+ interface WebsocketProviderOpts {
161
+ connect: boolean;
162
+ awareness: awarenessProtocol.Awareness;
163
+ params: Record<string, string>; // specify url parameters
164
+ protocols: Array<string>; // specify websocket protocols
165
+ WebSocketPolyfill: typeof WebSocket; // Optionall provide a WebSocket polyfill
166
+ resyncInterval: number; // Request server state every `resyncInterval` milliseconds
167
+ maxBackoffTime: number; // Maximum amount of time to wait before trying to reconnect (we try to reconnect using exponential backoff)
168
+ disableBc: boolean; // Disable cross-tab BroadcastChannel communication
169
+ }
170
+
171
+ /**
172
+ * Websocket Provider for Yjs. Creates a websocket connection to sync the shared document.
173
+ * The document name is attached to the provided url. I.e. the following example
174
+ * creates a websocket connection to http://localhost:1234/my-document-name
175
+ *
176
+ * @example
177
+ * import * as Y from 'yjs'
178
+ * import { WebsocketProvider } from '@kerebron/extension-yjs/WebsocketProvider'
179
+ * const doc = new Y.Doc()
180
+ * const provider = new WebsocketProvider('http://localhost:1234', 'my-document-name', doc)
181
+ */
182
+ export class WebsocketProvider extends EventTarget implements YjsProvider {
183
+ roomname: string;
184
+ doc: Y.Doc;
185
+
186
+ _synced = false;
187
+ shouldConnect: boolean;
188
+ ws: WebSocket | undefined;
189
+ _resyncInterval = 0;
190
+ serverUrl: string;
191
+ bcChannel: string;
192
+ maxBackoffTime: number;
193
+ /**
194
+ * The specified url parameters. This can be safely updated. The changed parameters will be used
195
+ * when a new connection is established.
196
+ */
197
+ params: Record<string, string>;
198
+ protocols: string[];
199
+ _WS: typeof WebSocket;
200
+ awareness: awarenessProtocol.Awareness;
201
+ destroyed = false;
202
+ wsconnected = false;
203
+ wsconnecting = false;
204
+ bcconnected = false;
205
+ disableBc: boolean;
206
+ wsUnsuccessfulReconnects: number;
207
+ messageHandlers: MessageHandler<WebsocketProvider>[];
208
+ wsLastMessageReceived: number;
209
+ private _bcSubscriber: (data: any, origin: any) => void;
210
+ private _updateHandler: (update: any, origin: any) => void;
211
+ private _awarenessUpdateHandler: (
212
+ { added, updated, removed }: { added: any; updated: any; removed: any },
213
+ _origin: any,
214
+ ) => void;
215
+ private _exitHandler: () => void;
216
+ private _checkInterval: number;
217
+ setupWSTimeout: number | undefined;
218
+
219
+ constructor(
220
+ serverUrl: string,
221
+ roomname: string,
222
+ doc: Y.Doc,
223
+ opts: WebsocketProviderOpts = {
224
+ connect: true,
225
+ awareness: new awarenessProtocol.Awareness(doc),
226
+ params: {},
227
+ protocols: [],
228
+ WebSocketPolyfill: WebSocket,
229
+ resyncInterval: -1,
230
+ maxBackoffTime: 2500,
231
+ disableBc: false,
232
+ },
233
+ ) {
234
+ super();
235
+ // ensure that serverUrl does not end with /
236
+ this.serverUrl = serverUrl;
237
+ this.roomname = roomname;
238
+ this.doc = doc;
239
+
240
+ while (this.serverUrl[this.serverUrl.length - 1] === '/') {
241
+ this.serverUrl = this.serverUrl.slice(0, this.serverUrl.length - 1);
242
+ }
243
+ this.bcChannel = this.serverUrl + '/' + roomname;
244
+ this.maxBackoffTime = opts.maxBackoffTime;
245
+ this.params = opts.params;
246
+ this.protocols = opts.protocols;
247
+ this._WS = opts.WebSocketPolyfill;
248
+ this.awareness = opts.awareness;
249
+ this.disableBc = opts.disableBc;
250
+ this.wsUnsuccessfulReconnects = 0;
251
+ this.messageHandlers = this.setupMessageHandlers();
252
+ this.wsLastMessageReceived = 0;
253
+ this.shouldConnect = opts.connect;
254
+
255
+ if (opts.resyncInterval > 0) {
256
+ this._resyncInterval = setInterval(() => {
257
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
258
+ // resend sync step 1
259
+ const encoder = encoding.createEncoder();
260
+ encoding.writeVarUint(encoder, messageSync);
261
+ syncProtocol.writeSyncStep1(encoder, doc);
262
+ this.ws.send(encoding.toUint8Array(encoder));
263
+ }
264
+ }, opts.resyncInterval);
265
+ }
266
+
267
+ this._bcSubscriber = (data, origin) => {
268
+ if (origin !== this) {
269
+ const encoder = readMessage(this, new Uint8Array(data), false);
270
+ if (encoding.length(encoder) > 1) {
271
+ bc.publish(this.bcChannel, encoding.toUint8Array(encoder), this);
272
+ }
273
+ }
274
+ };
275
+ /**
276
+ * Listens to Yjs updates and sends them to remote peers (ws and broadcastchannel)
277
+ */
278
+ this._updateHandler = (update, origin) => {
279
+ if (origin !== this) {
280
+ const encoder = encoding.createEncoder();
281
+ encoding.writeVarUint(encoder, messageSync);
282
+ syncProtocol.writeUpdate(encoder, update);
283
+ broadcastMessage(this, encoding.toUint8Array(encoder));
284
+ }
285
+ };
286
+ this.doc.on('update', this._updateHandler);
287
+ this._awarenessUpdateHandler = ({ added, updated, removed }, _origin) => {
288
+ const changedClients = added.concat(updated).concat(removed);
289
+ const encoder = encoding.createEncoder();
290
+ encoding.writeVarUint(encoder, messageAwareness);
291
+ encoding.writeVarUint8Array(
292
+ encoder,
293
+ awarenessProtocol.encodeAwarenessUpdate(opts.awareness, changedClients),
294
+ );
295
+ broadcastMessage(this, encoding.toUint8Array(encoder));
296
+ };
297
+ this._exitHandler = () => {
298
+ awarenessProtocol.removeAwarenessStates(
299
+ this.awareness,
300
+ [doc.clientID],
301
+ 'app closed',
302
+ );
303
+ };
304
+ // if (env.isNode && typeof process !== 'undefined') {
305
+ // process.on('exit', this._exitHandler);
306
+ // }
307
+ opts.awareness.on('update', this._awarenessUpdateHandler);
308
+ this._checkInterval = /** @type {any} */ (setInterval(() => {
309
+ if (
310
+ this.wsconnected &&
311
+ messageReconnectTimeout <
312
+ time.getUnixTime() - this.wsLastMessageReceived
313
+ ) {
314
+ // no message received in a long time - not even your own awareness
315
+ // updates (which are updated every 15 seconds)
316
+
317
+ if (this.ws) {
318
+ closeWebsocketConnection(
319
+ this,
320
+ this.ws,
321
+ null,
322
+ );
323
+ }
324
+ }
325
+ }, messageReconnectTimeout / 10));
326
+
327
+ if (opts.connect) {
328
+ this.connect();
329
+ }
330
+ }
331
+
332
+ get url() {
333
+ const encodedParams = url.encodeQueryParams(this.params);
334
+ return this.serverUrl + '/' + this.roomname +
335
+ (encodedParams.length === 0 ? '' : '?' + encodedParams);
336
+ }
337
+
338
+ get synced(): boolean {
339
+ return this._synced;
340
+ }
341
+
342
+ set synced(state: boolean) {
343
+ if (this._synced !== state) {
344
+ this._synced = state;
345
+ this.dispatchEvent(new CustomEvent('synced', { detail: { state } }));
346
+ this.dispatchEvent(new CustomEvent('sync', { detail: { state } }));
347
+ }
348
+ }
349
+
350
+ destroy() {
351
+ this.destroyed = true;
352
+
353
+ if (this._resyncInterval !== 0) {
354
+ clearInterval(this._resyncInterval);
355
+ }
356
+ if (this.setupWSTimeout) {
357
+ clearTimeout(this.setupWSTimeout);
358
+ }
359
+ clearInterval(this._checkInterval);
360
+ this.disconnect();
361
+ // if (env.isNode && typeof process !== 'undefined') {
362
+ // process.off('exit', this._exitHandler);
363
+ // }
364
+ this.awareness.off('update', this._awarenessUpdateHandler);
365
+ this.doc.off('update', this._updateHandler);
366
+ this.awareness.destroy();
367
+ }
368
+
369
+ connectBc() {
370
+ if (this.disableBc) {
371
+ return;
372
+ }
373
+ if (!this.bcconnected) {
374
+ bc.subscribe(this.bcChannel, this._bcSubscriber);
375
+ this.bcconnected = true;
376
+ }
377
+ // send sync step1 to bc
378
+ // write sync step 1
379
+ const encoderSync = encoding.createEncoder();
380
+ encoding.writeVarUint(encoderSync, messageSync);
381
+ syncProtocol.writeSyncStep1(encoderSync, this.doc);
382
+ bc.publish(this.bcChannel, encoding.toUint8Array(encoderSync), this);
383
+ // broadcast local state
384
+ const encoderState = encoding.createEncoder();
385
+ encoding.writeVarUint(encoderState, messageSync);
386
+ syncProtocol.writeSyncStep2(encoderState, this.doc);
387
+ bc.publish(this.bcChannel, encoding.toUint8Array(encoderState), this);
388
+ // write queryAwareness
389
+ const encoderAwarenessQuery = encoding.createEncoder();
390
+ encoding.writeVarUint(encoderAwarenessQuery, messageQueryAwareness);
391
+ bc.publish(
392
+ this.bcChannel,
393
+ encoding.toUint8Array(encoderAwarenessQuery),
394
+ this,
395
+ );
396
+ // broadcast local awareness state
397
+ const encoderAwarenessState = encoding.createEncoder();
398
+ encoding.writeVarUint(encoderAwarenessState, messageAwareness);
399
+ encoding.writeVarUint8Array(
400
+ encoderAwarenessState,
401
+ awarenessProtocol.encodeAwarenessUpdate(this.awareness, [
402
+ this.doc.clientID,
403
+ ]),
404
+ );
405
+ bc.publish(
406
+ this.bcChannel,
407
+ encoding.toUint8Array(encoderAwarenessState),
408
+ this,
409
+ );
410
+ }
411
+
412
+ disconnectBc() {
413
+ // broadcast message with local awareness state set to null (indicating disconnect)
414
+ const encoder = encoding.createEncoder();
415
+ encoding.writeVarUint(encoder, messageAwareness);
416
+ encoding.writeVarUint8Array(
417
+ encoder,
418
+ awarenessProtocol.encodeAwarenessUpdate(this.awareness, [
419
+ this.doc.clientID,
420
+ ], new Map()),
421
+ );
422
+ broadcastMessage(this, encoding.toUint8Array(encoder));
423
+ if (this.bcconnected) {
424
+ bc.unsubscribe(this.bcChannel, this._bcSubscriber);
425
+ this.bcconnected = false;
426
+ }
427
+ }
428
+
429
+ disconnect() {
430
+ this.shouldConnect = false;
431
+ this.disconnectBc();
432
+ if (this.ws) {
433
+ closeWebsocketConnection(this, this.ws, null);
434
+ }
435
+ }
436
+
437
+ connect() {
438
+ this.shouldConnect = true;
439
+ if (!this.wsconnected && !this.ws) {
440
+ setupWS(this);
441
+ this.connectBc();
442
+ }
443
+ }
444
+
445
+ private setupMessageHandlers() {
446
+ const messageHandlers: MessageHandler<WebsocketProvider>[] = [];
447
+ messageHandlers[messageSync] = (
448
+ encoder: encoding.Encoder,
449
+ decoder: decoding.Decoder,
450
+ provider: WebsocketProvider,
451
+ emitSynced: boolean,
452
+ _messageType: MessageType,
453
+ ) => {
454
+ encoding.writeVarUint(encoder, messageSync);
455
+ const syncMessageType: MessageType = syncProtocol.readSyncMessage(
456
+ decoder,
457
+ encoder,
458
+ provider.doc,
459
+ provider,
460
+ );
461
+ if (
462
+ emitSynced && syncMessageType === syncProtocol.messageYjsSyncStep2 &&
463
+ !provider.synced
464
+ ) {
465
+ provider.synced = true;
466
+ }
467
+ };
468
+
469
+ messageHandlers[messageQueryAwareness] = (
470
+ encoder: encoding.Encoder,
471
+ _decoder: decoding.Decoder,
472
+ provider: WebsocketProvider,
473
+ _emitSynced: boolean,
474
+ _messageType: MessageType,
475
+ ) => {
476
+ encoding.writeVarUint(encoder, messageAwareness);
477
+ encoding.writeVarUint8Array(
478
+ encoder,
479
+ awarenessProtocol.encodeAwarenessUpdate(
480
+ provider.awareness,
481
+ Array.from(provider.awareness.getStates().keys()),
482
+ ),
483
+ );
484
+ };
485
+
486
+ messageHandlers[messageAwareness] = (
487
+ _encoder: encoding.Encoder,
488
+ decoder: decoding.Decoder,
489
+ provider: WebsocketProvider,
490
+ _emitSynced: boolean,
491
+ _messageType: MessageType,
492
+ ) => {
493
+ awarenessProtocol.applyAwarenessUpdate(
494
+ provider.awareness,
495
+ decoding.readVarUint8Array(decoder),
496
+ provider,
497
+ );
498
+ };
499
+
500
+ messageHandlers[messageAuth] = (
501
+ _encoder: encoding.Encoder,
502
+ decoder: decoding.Decoder,
503
+ provider: WebsocketProvider,
504
+ _emitSynced: boolean,
505
+ _messageType: MessageType,
506
+ ) => {
507
+ authProtocol.readAuthMessage(
508
+ decoder,
509
+ provider.doc,
510
+ (_ydoc, reason) => permissionDeniedHandler(provider, reason),
511
+ );
512
+ };
513
+
514
+ return messageHandlers;
515
+ }
516
+ }