UNPKG

yjs-server

Version:

An extensible websocket server for the Yjs collaborative editing framework. Compatible with y-websocket.

98 lines (97 loc) 3.88 kB
import { Awareness, encodeAwarenessUpdate } from 'y-protocols/awareness.js'; import { removeAwarenessStates } from 'y-protocols/awareness'; import { send } from './socket-ops.js'; import * as syncProtocol from 'y-protocols/sync.js'; import { encoding } from 'lib0'; import { MessageType } from './internal.js'; export const makeRoom = (name, yDoc, docStorage, logger) => { const loadPromise = docStorage ?.loadDoc(name, yDoc) .then(() => true) .catch((err) => { logger.error({ name, err }, 'loadDoc failed, connections waiting for the doc to load will be closed'); return false; }) ?? Promise.resolve(true); return new Room(name, yDoc, loadPromise, docStorage, logger); }; export class Room { constructor(name, yDoc, loadPromise, docStorage, logger) { this.name = name; this.yDoc = yDoc; this.loadPromise = loadPromise; this.docStorage = docStorage; this.logger = logger; // maps from conn to set of controlled user ids. Delete all user ids from awareness when this conn is closed this.conns = new Map(); this.isDirty = false; this.awareness = new Awareness(yDoc); this.awareness.setLocalState(null); const handleAwarenessUpdate = ({ added, updated, removed }, conn) => { const changedClients = added.concat(updated, removed); const connControlledIds = this.conns.get(conn); if (connControlledIds) { added.forEach((clientId) => { connControlledIds.add(clientId); }); removed.forEach((clientId) => { connControlledIds.delete(clientId); }); } // broadcast awareness update const encoder = encoding.createEncoder(); encoding.writeVarUint(encoder, MessageType.Awareness); encoding.writeVarUint8Array(encoder, encodeAwarenessUpdate(this.awareness, changedClients)); const buff = encoding.toUint8Array(encoder); this.conns.forEach((_, c) => { send(c, buff); }); }; this.awareness.on('update', handleAwarenessUpdate); // broadcast updates this.handleDocUpdate = (update) => { const encoder = encoding.createEncoder(); encoding.writeVarUint(encoder, MessageType.Sync); syncProtocol.writeUpdate(encoder, update); const message = encoding.toUint8Array(encoder); this.conns.forEach((_, conn) => send(conn, message)); docStorage?.onUpdate?.(name, update, this.yDoc).catch((err) => { logger.warn({ name, err, yDoc: this.yDoc }, 'error calling onUpdate'); }); this.isDirty = true; }; this.yDoc.on('update', this.handleDocUpdate); } get numConnections() { return this.conns.size; } get connections() { return this.conns.keys(); } addConnection(conn) { this.conns.set(conn, new Set()); } removeConnection(conn) { const controlledIds = this.conns.get(conn); if (controlledIds) { removeAwarenessStates(this.awareness, Array.from(controlledIds), null); this.conns.delete(conn); } } destroy() { this.yDoc.off('update', this.handleDocUpdate); this.awareness.destroy(); if (this.isDirty && this.docStorage?.storeDoc) { this.docStorage .storeDoc(this.name, this.yDoc) .catch((err) => { this.logger.warn({ name: this.name, err, yDoc: this.yDoc }, 'error calling storeDoc'); }) .finally(() => { this.yDoc.destroy(); }); } else { this.yDoc.destroy(); } } }