@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.
- package/esm/ExtensionYjs.d.ts +3 -11
- package/esm/ExtensionYjs.d.ts.map +1 -1
- package/esm/ExtensionYjs.js +38 -45
- package/esm/ExtensionYjs.js.map +1 -1
- package/esm/WebsocketProvider.d.ts +69 -0
- package/esm/WebsocketProvider.d.ts.map +1 -0
- package/esm/WebsocketProvider.js +354 -0
- package/esm/WebsocketProvider.js.map +1 -0
- package/esm/YjsProvider.d.ts +48 -0
- package/esm/YjsProvider.d.ts.map +1 -0
- package/esm/YjsProvider.js +12 -0
- package/esm/YjsProvider.js.map +1 -0
- package/esm/_dnt.shims.d.ts +2 -0
- package/esm/_dnt.shims.d.ts.map +1 -0
- package/esm/_dnt.shims.js +58 -0
- package/esm/_dnt.shims.js.map +1 -0
- package/esm/binding/BindingMetadata.d.ts +6 -0
- package/esm/binding/BindingMetadata.d.ts.map +1 -0
- package/esm/binding/BindingMetadata.js +2 -0
- package/esm/binding/BindingMetadata.js.map +1 -0
- package/esm/binding/PmYjsBinding.d.ts +41 -0
- package/esm/binding/PmYjsBinding.d.ts.map +1 -0
- package/esm/binding/PmYjsBinding.js +190 -0
- package/esm/binding/PmYjsBinding.js.map +1 -0
- package/esm/binding/convertUtils.d.ts +48 -0
- package/esm/binding/convertUtils.d.ts.map +1 -0
- package/esm/binding/convertUtils.js +80 -0
- package/esm/binding/convertUtils.js.map +1 -0
- package/esm/{createNodeFromYElement.d.ts → binding/createNodeFromYElement.d.ts} +1 -1
- package/esm/binding/createNodeFromYElement.d.ts.map +1 -0
- package/esm/{createNodeFromYElement.js → binding/createNodeFromYElement.js} +2 -2
- package/esm/binding/createNodeFromYElement.js.map +1 -0
- package/esm/{updateYFragment.d.ts → binding/updateYFragment.d.ts} +3 -3
- package/esm/binding/updateYFragment.d.ts.map +1 -0
- package/esm/{updateYFragment.js → binding/updateYFragment.js} +10 -7
- package/esm/binding/updateYFragment.js.map +1 -0
- package/esm/lib.d.ts +1 -7
- package/esm/lib.d.ts.map +1 -1
- package/esm/lib.js +1 -200
- package/esm/lib.js.map +1 -1
- package/esm/position.d.ts +8 -0
- package/esm/position.d.ts.map +1 -0
- package/esm/position.js +165 -0
- package/esm/position.js.map +1 -0
- package/esm/ui/selection.d.ts +29 -0
- package/esm/ui/selection.d.ts.map +1 -0
- package/esm/ui/selection.js +129 -0
- package/esm/ui/selection.js.map +1 -0
- package/esm/yPositionPlugin.d.ts +6 -1
- package/esm/yPositionPlugin.d.ts.map +1 -1
- package/esm/yPositionPlugin.js +91 -50
- package/esm/yPositionPlugin.js.map +1 -1
- package/esm/ySyncPlugin.d.ts +5 -22
- package/esm/ySyncPlugin.d.ts.map +1 -1
- package/esm/ySyncPlugin.js +54 -116
- package/esm/ySyncPlugin.js.map +1 -1
- package/esm/yUndoPlugin.d.ts +11 -10
- package/esm/yUndoPlugin.d.ts.map +1 -1
- package/esm/yUndoPlugin.js +90 -52
- package/esm/yUndoPlugin.js.map +1 -1
- package/package.json +9 -6
- package/src/ExtensionYjs.ts +55 -67
- package/src/WebsocketProvider.ts +516 -0
- package/src/YjsProvider.ts +75 -0
- package/src/_dnt.shims.ts +60 -0
- package/src/binding/BindingMetadata.ts +6 -0
- package/src/binding/PmYjsBinding.ts +300 -0
- package/src/binding/convertUtils.ts +124 -0
- package/src/{createNodeFromYElement.ts → binding/createNodeFromYElement.ts} +3 -3
- package/src/{updateYFragment.ts → binding/updateYFragment.ts} +15 -8
- package/src/lib.ts +4 -230
- package/src/position.ts +191 -0
- package/src/ui/selection.ts +216 -0
- package/src/yPositionPlugin.ts +122 -74
- package/src/ySyncPlugin.ts +87 -170
- package/src/yUndoPlugin.ts +113 -62
- package/esm/ProsemirrorBinding.d.ts +0 -60
- package/esm/ProsemirrorBinding.d.ts.map +0 -1
- package/esm/ProsemirrorBinding.js +0 -405
- package/esm/ProsemirrorBinding.js.map +0 -1
- package/esm/createNodeFromYElement.d.ts.map +0 -1
- package/esm/createNodeFromYElement.js.map +0 -1
- package/esm/updateYFragment.d.ts.map +0 -1
- package/esm/updateYFragment.js.map +0 -1
- package/esm/userColors.d.ts +0 -5
- package/esm/userColors.d.ts.map +0 -1
- package/esm/userColors.js +0 -11
- package/esm/userColors.js.map +0 -1
- package/src/ProsemirrorBinding.ts +0 -607
- package/src/userColors.ts +0 -10
package/src/ExtensionYjs.ts
CHANGED
|
@@ -1,40 +1,23 @@
|
|
|
1
|
-
import type {
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
17
|
+
import type { CreateYjsProvider } from './YjsProvider.js';
|
|
35
18
|
|
|
36
19
|
export interface YjsConfig {
|
|
37
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
|
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
|
+
}
|