open-collaboration-yjs
Version:
Open Collaboration Yjs integration, part of the Open Collaboration Tools project
133 lines (113 loc) • 5.45 kB
text/typescript
// ******************************************************************************
// Copyright 2024 TypeFox GmbH
// This program and the accompanying materials are made available under the
// terms of the MIT License, which is available in the project root.
// ******************************************************************************
import * as types from 'open-collaboration-protocol';
import * as Y from 'yjs';
import * as syncProtocol from 'y-protocols/sync';
import * as awarenessProtocol from 'y-protocols/awareness';
import * as encoding from 'lib0/encoding';
import * as decoding from 'lib0/decoding';
import { ObservableV2 } from 'lib0/observable';
export interface AwarenessChange {
added: number[];
updated: number[];
removed: number[];
}
export const LOCAL_ORIGIN = 'local';
export interface YjsProviderOptions {
resyncTimer?: number;
}
export class OpenCollaborationYjsProvider extends ObservableV2<string> {
private connection: types.ProtocolBroadcastConnection;
private doc: Y.Doc;
private awareness: awarenessProtocol.Awareness;
constructor(connection: types.ProtocolBroadcastConnection, doc: Y.Doc, awareness: awarenessProtocol.Awareness, options?: YjsProviderOptions) {
super();
this.connection = connection;
this.doc = doc;
this.awareness = awareness;
this.doc.on('update', this.yjsUpdateHandler.bind(this));
this.awareness.on('update', this.yjsAwarenessUpdateHandler.bind(this));
connection.sync.onDataUpdate(this.ocpDataUpdateHandler.bind(this));
connection.sync.onAwarenessUpdate(this.ocpAwarenessUpdateHandler.bind(this));
connection.sync.onAwarenessQuery(this.ocpAwarenessQueryHandler.bind(this));
if (options?.resyncTimer && options.resyncTimer > 0) {
this.setResyncInterval(options.resyncTimer);
}
}
private setResyncInterval(timeout: number): void {
const interval = setInterval(() => {
const encoder = encoding.createEncoder();
syncProtocol.writeSyncStep1(encoder, this.doc);
this.connection.sync.dataUpdate(this.encode(encoder));
}, timeout);
this.doc.on('destroy', () => {
clearInterval(interval);
});
}
private ocpDataUpdateHandler(origin: string, update: types.Binary): void {
const decoder = this.decode(update);
const encoder = encoding.createEncoder();
const syncMessageType = syncProtocol.readSyncMessage(decoder, encoder, this.doc, origin);
if (syncMessageType === syncProtocol.messageYjsSyncStep1) {
this.connection.sync.dataUpdate(origin, this.encode(encoder));
}
}
private ocpAwarenessUpdateHandler(origin: string, update: types.Binary): void {
const decoder = this.decode(update);
awarenessProtocol.applyAwarenessUpdate(this.awareness, decoding.readVarUint8Array(decoder), origin);
}
private ocpAwarenessQueryHandler(origin: string): void {
const encoder = encoding.createEncoder();
encoding.writeVarUint8Array(
encoder,
awarenessProtocol.encodeAwarenessUpdate(this.awareness, Array.from(this.awareness.getStates().keys()))
);
this.connection.sync.awarenessUpdate(origin, this.encode(encoder));
}
private yjsUpdateHandler(update: Uint8Array, origin: unknown): void {
if (origin !== this) {
const encoder = encoding.createEncoder();
syncProtocol.writeUpdate(encoder, update);
this.connection.sync.dataUpdate(this.encode(encoder));
}
}
private yjsAwarenessUpdateHandler(changed: AwarenessChange): void {
const changedClients = changed.added.concat(changed.updated).concat(changed.removed);
const encoder = encoding.createEncoder();
encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(this.awareness, changedClients));
this.connection.sync.awarenessUpdate(this.encode(encoder));
}
connect(): void {
// write sync step 1
const encoderSync = encoding.createEncoder();
syncProtocol.writeSyncStep1(encoderSync, this.doc);
this.connection.sync.dataUpdate(this.encode(encoderSync));
// broadcast local state
const encoderState = encoding.createEncoder();
syncProtocol.writeSyncStep2(encoderState, this.doc);
this.connection.sync.dataUpdate(this.encode(encoderState));
// query awareness info
this.connection.sync.awarenessQuery();
// broadcast local awareness info
const encoderAwareness = encoding.createEncoder();
encoding.writeVarUint8Array(
encoderAwareness,
awarenessProtocol.encodeAwarenessUpdate(this.awareness, [
this.doc.clientID
])
);
this.connection.sync.awarenessUpdate(this.encode(encoderAwareness));
}
dispose(): void {
awarenessProtocol.removeAwarenessStates(this.awareness, [this.doc.clientID], 'client disconnected');
}
private encode(encoder: encoding.Encoder): types.Binary {
return encoding.toUint8Array(encoder);
}
private decode(data: types.Binary): decoding.Decoder {
return decoding.createDecoder(data);
}
}