UNPKG

open-collaboration-yjs

Version:

Open Collaboration Yjs integration, part of the Open Collaboration Tools project

133 lines (113 loc) 5.45 kB
// ****************************************************************************** // 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); } }