mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-15 17:16:16 +08:00
feat: sync with keck
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
Reference in New Issue
Block a user