feat: sync with keck

This commit is contained in:
DarkSky
2022-09-14 17:32:08 +08:00
parent 914f5ad436
commit 3ff0dbe781
11 changed files with 350 additions and 35 deletions
+115
View File
@@ -0,0 +1,115 @@
import * as Y from 'yjs';
import { Observable } from 'lib0/observable';
import * as url from 'lib0/url';
import { handler } from './handler';
import { registerKeckUpdateHandler } from './processor';
import { registerWebsocket } from './websocket';
/**
* Websocket Provider for Yjs. Creates a websocket connection to sync the shared document.
* The document name is attached to the provided url. I.e. the following example
* creates a websocket connection to http://localhost:3000/my-document-name
*
* @example
* import * as Y from 'yjs'
* import { KeckProvider } from 'jwt-rpc'
* const doc = new Y.Doc()
* const provider = new KeckProvider('http://localhost:3000', 'my-document-name', doc)
*/
export class KeckProvider extends Observable<string> {
maxBackOffTime: number;
url: string;
roomName: string;
doc: Y.Doc;
wsUnsuccessfulReconnects: number;
private _synced: boolean;
broadcastChannel: string;
private _broadcast?: {
broadcastMessage: (buf: ArrayBuffer) => void;
disconnect: () => void;
};
private _websocket?: {
broadcastMessage: (buf: ArrayBuffer) => void;
disconnect: () => void;
};
private _updateHandlerDestroy: () => void;
constructor(
token: string,
serverUrl: string,
roomName: string,
doc: Y.Doc,
{ params = {}, resyncInterval = -1, maxBackOffTime = 2500 } = {}
) {
super();
this.roomName = roomName;
// ensure that url is always ends with /
while (serverUrl[serverUrl.length - 1] === '/') {
serverUrl = serverUrl.slice(0, serverUrl.length - 1);
}
this.broadcastChannel = serverUrl + '/' + roomName + '/';
const encodedParams = url.encodeQueryParams(params);
this.url =
this.broadcastChannel +
(encodedParams.length === 0 ? '' : '?' + encodedParams);
this.doc = doc;
this.maxBackOffTime = maxBackOffTime;
this.wsUnsuccessfulReconnects = 0;
this._synced = false;
this._websocket = registerWebsocket(this, token, resyncInterval);
this._updateHandlerDestroy = registerKeckUpdateHandler(
this,
doc,
buf => {
this._websocket?.broadcastMessage(buf);
this._broadcast?.broadcastMessage(buf);
}
);
}
get messageHandlers() {
return handler;
}
get synced() {
return this._synced;
}
set synced(state) {
if (this._synced !== state) {
this._synced = state;
this.emit('synced', [state]);
this.emit('sync', [state]);
}
}
override destroy() {
if (this._broadcast) {
const disconnect = this._broadcast.disconnect;
this._broadcast = undefined;
disconnect();
}
if (this._websocket) {
const disconnect = this._websocket.disconnect;
this._websocket = undefined;
disconnect();
}
this._updateHandlerDestroy?.();
super.destroy();
}
}
+25 -3
View File
@@ -5,10 +5,11 @@ import * as syncProtocol from 'y-protocols/sync';
import * as Y from 'yjs';
import { Message } from './handler';
import { WebsocketProvider } from './provider';
import { KeckProvider } from './keckprovider';
import { WebsocketProvider } from './wsprovider';
export const readMessage = (
provider: WebsocketProvider,
provider: WebsocketProvider | KeckProvider,
buf: Uint8Array,
emitSynced: boolean
): encoding.Encoder => {
@@ -24,7 +25,7 @@ export const readMessage = (
return encoder;
};
export const registerUpdateHandler = (
export const registerWsUpdateHandler = (
provider: WebsocketProvider,
awareness: awarenessProtocol.Awareness,
doc: Y.Doc,
@@ -78,3 +79,24 @@ export const registerUpdateHandler = (
doc.off('update', documentUpdateHandler);
};
};
export const registerKeckUpdateHandler = (
provider: KeckProvider,
doc: Y.Doc,
broadcastMessage: (buf: ArrayBuffer) => void
) => {
// Listens to Yjs updates and sends them to remote peers (ws and broadcastchannel)
const documentUpdateHandler = (update: Uint8Array, origin: any) => {
if (origin !== provider) {
const encoder = encoding.createEncoder();
encoding.writeVarUint(encoder, Message.sync);
syncProtocol.writeUpdate(encoder, update);
broadcastMessage(encoding.toUint8Array(encoder));
}
};
doc.on('update', documentUpdateHandler);
return () => {
doc.off('update', documentUpdateHandler);
};
};
+23 -15
View File
@@ -5,8 +5,9 @@ import * as awarenessProtocol from 'y-protocols/awareness';
import * as syncProtocol from 'y-protocols/sync';
import { Message } from './handler';
import { KeckProvider } from './keckprovider';
import { readMessage } from './processor';
import { WebsocketProvider } from './provider';
import { WebsocketProvider } from './wsprovider';
enum WebSocketState {
disconnected = 0,
@@ -46,14 +47,14 @@ const _getToken = async (
return resp.json();
};
const _getTimeout = (provider: WebsocketProvider) =>
const _getTimeout = (provider: WebsocketProvider | KeckProvider) =>
math.min(
math.pow(2, provider.wsUnsuccessfulReconnects) * 100,
provider.maxBackOffTime
);
export const registerWebsocket = (
provider: WebsocketProvider,
provider: WebsocketProvider | KeckProvider,
token: string,
resync = -1,
reconnect = 3,
@@ -105,13 +106,19 @@ export const registerWebsocket = (
state = WebSocketState.disconnected;
provider.synced = false;
// update awareness (all users except local left)
awarenessProtocol.removeAwarenessStates(
provider.awareness,
Array.from(
provider.awareness.getStates().keys()
).filter(client => client !== provider.doc.clientID),
provider
);
const awareness = (provider as any)['awareness'];
if (awareness) {
awarenessProtocol.removeAwarenessStates(
awareness,
Array.from(awareness.getStates().keys()).filter(
(client): client is number =>
client !== provider.doc.clientID
),
provider
);
}
provider.emit('status', [{ status: 'disconnected' }]);
} else {
provider.wsUnsuccessfulReconnects++;
@@ -139,8 +146,10 @@ export const registerWebsocket = (
encoding.writeVarUint(encoder, Message.sync);
syncProtocol.writeSyncStep1(encoder, provider.doc);
websocket?.send(encoding.toUint8Array(encoder));
const awareness = (provider as any)['awareness'];
// broadcast local awareness state
if (provider.awareness.getLocalState() !== null) {
if (awareness && awareness.getLocalState() !== null) {
const encoderAwarenessState = encoding.createEncoder();
encoding.writeVarUint(
encoderAwarenessState,
@@ -148,10 +157,9 @@ export const registerWebsocket = (
);
encoding.writeVarUint8Array(
encoderAwarenessState,
awarenessProtocol.encodeAwarenessUpdate(
provider.awareness,
[provider.doc.clientID]
)
awarenessProtocol.encodeAwarenessUpdate(awareness, [
provider.doc.clientID,
])
);
websocket?.send(
encoding.toUint8Array(encoderAwarenessState)
@@ -6,7 +6,7 @@ import * as url from 'lib0/url';
import * as awarenessProtocol from 'y-protocols/awareness';
import { handler } from './handler';
import { registerUpdateHandler } from './processor';
import { registerWsUpdateHandler } from './processor';
import { registerWebsocket } from './websocket';
/**
@@ -85,7 +85,7 @@ export class WebsocketProvider extends Observable<string> {
// this.doc
// );
this._updateHandlerDestroy = registerUpdateHandler(
this._updateHandlerDestroy = registerWsUpdateHandler(
this,
awareness,
doc,