UNPKG

@wordpress/sync

Version:
702 lines (683 loc) 23.8 kB
/* wp:polyfill */ "use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.signalingConns = exports.rooms = exports.publishSignalingMessage = exports.log = exports.WebrtcProvider = exports.WebrtcConn = exports.SignalingConn = exports.Room = void 0; var ws = _interopRequireWildcard(require("lib0/websocket")); var map = _interopRequireWildcard(require("lib0/map")); var error = _interopRequireWildcard(require("lib0/error")); var random = _interopRequireWildcard(require("lib0/random")); var encoding = _interopRequireWildcard(require("lib0/encoding")); var decoding = _interopRequireWildcard(require("lib0/decoding")); var _observable = require("lib0/observable"); var logging = _interopRequireWildcard(require("lib0/logging")); var promise = _interopRequireWildcard(require("lib0/promise")); var bc = _interopRequireWildcard(require("lib0/broadcastchannel")); var buffer = _interopRequireWildcard(require("lib0/buffer")); var math = _interopRequireWildcard(require("lib0/math")); var _mutex = require("lib0/mutex"); var Y = _interopRequireWildcard(require("yjs")); var _simplepeerMin = _interopRequireDefault(require("simple-peer/simplepeer.min.js")); var syncProtocol = _interopRequireWildcard(require("y-protocols/sync")); var awarenessProtocol = _interopRequireWildcard(require("y-protocols/awareness")); var cryptoutils = _interopRequireWildcard(require("./crypto.js")); function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } // File copied as is from the y-webrtc package with only exports // added to the following vars/functions: signalingConns,rooms, publishSignalingMessage, log. /* eslint-disable eslint-comments/disable-enable-pair */ /* eslint-disable eslint-comments/no-unlimited-disable */ /* eslint-disable */ // @ts-nocheck // eslint-disable-line const log = exports.log = logging.createModuleLogger('y-webrtc'); const messageSync = 0; const messageQueryAwareness = 3; const messageAwareness = 1; const messageBcPeerId = 4; /** * @type {Map<string, SignalingConn>} */ const signalingConns = exports.signalingConns = new Map(); /** * @type {Map<string,Room>} */ const rooms = exports.rooms = new Map(); /** * @param {Room} room */ const checkIsSynced = room => { let synced = true; room.webrtcConns.forEach(peer => { if (!peer.synced) { synced = false; } }); if (!synced && room.synced || synced && !room.synced) { room.synced = synced; room.provider.emit('synced', [{ synced }]); log('synced ', logging.BOLD, room.name, logging.UNBOLD, ' with all peers'); } }; /** * @param {Room} room * @param {Uint8Array} buf * @param {function} syncedCallback * @return {encoding.Encoder?} */ const readMessage = (room, buf, syncedCallback) => { const decoder = decoding.createDecoder(buf); const encoder = encoding.createEncoder(); const messageType = decoding.readVarUint(decoder); if (room === undefined) { return null; } const awareness = room.awareness; const doc = room.doc; let sendReply = false; switch (messageType) { case messageSync: { encoding.writeVarUint(encoder, messageSync); const syncMessageType = syncProtocol.readSyncMessage(decoder, encoder, doc, room); if (syncMessageType === syncProtocol.messageYjsSyncStep2 && !room.synced) { syncedCallback(); } if (syncMessageType === syncProtocol.messageYjsSyncStep1) { sendReply = true; } break; } case messageQueryAwareness: encoding.writeVarUint(encoder, messageAwareness); encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(awareness, Array.from(awareness.getStates().keys()))); sendReply = true; break; case messageAwareness: awarenessProtocol.applyAwarenessUpdate(awareness, decoding.readVarUint8Array(decoder), room); break; case messageBcPeerId: { const add = decoding.readUint8(decoder) === 1; const peerName = decoding.readVarString(decoder); if (peerName !== room.peerId && (room.bcConns.has(peerName) && !add || !room.bcConns.has(peerName) && add)) { const removed = []; const added = []; if (add) { room.bcConns.add(peerName); added.push(peerName); } else { room.bcConns.delete(peerName); removed.push(peerName); } room.provider.emit('peers', [{ added, removed, webrtcPeers: Array.from(room.webrtcConns.keys()), bcPeers: Array.from(room.bcConns) }]); broadcastBcPeerId(room); } break; } default: console.error('Unable to compute message'); return encoder; } if (!sendReply) { // nothing has been written, no answer created return null; } return encoder; }; /** * @param {WebrtcConn} peerConn * @param {Uint8Array} buf * @return {encoding.Encoder?} */ const readPeerMessage = (peerConn, buf) => { const room = peerConn.room; log('received message from ', logging.BOLD, peerConn.remotePeerId, logging.GREY, ' (', room.name, ')', logging.UNBOLD, logging.UNCOLOR); return readMessage(room, buf, () => { peerConn.synced = true; log('synced ', logging.BOLD, room.name, logging.UNBOLD, ' with ', logging.BOLD, peerConn.remotePeerId); checkIsSynced(room); }); }; /** * @param {WebrtcConn} webrtcConn * @param {encoding.Encoder} encoder */ const sendWebrtcConn = (webrtcConn, encoder) => { log('send message to ', logging.BOLD, webrtcConn.remotePeerId, logging.UNBOLD, logging.GREY, ' (', webrtcConn.room.name, ')', logging.UNCOLOR); try { webrtcConn.peer.send(encoding.toUint8Array(encoder)); } catch (e) {} }; /** * @param {Room} room * @param {Uint8Array} m */ const broadcastWebrtcConn = (room, m) => { log('broadcast message in ', logging.BOLD, room.name, logging.UNBOLD); room.webrtcConns.forEach(conn => { try { conn.peer.send(m); } catch (e) {} }); }; class WebrtcConn { /** * @param {SignalingConn} signalingConn * @param {boolean} initiator * @param {string} remotePeerId * @param {Room} room */ constructor(signalingConn, initiator, remotePeerId, room) { log('establishing connection to ', logging.BOLD, remotePeerId); this.room = room; this.remotePeerId = remotePeerId; this.glareToken = undefined; this.closed = false; this.connected = false; this.synced = false; /** * @type {any} */ this.peer = new _simplepeerMin.default({ initiator, ...room.provider.peerOpts }); this.peer.on('signal', signal => { if (this.glareToken === undefined) { // add some randomness to the timestamp of the offer this.glareToken = Date.now() + Math.random(); } publishSignalingMessage(signalingConn, room, { to: remotePeerId, from: room.peerId, type: 'signal', token: this.glareToken, signal }); }); this.peer.on('connect', () => { log('connected to ', logging.BOLD, remotePeerId); this.connected = true; // send sync step 1 const provider = room.provider; const doc = provider.doc; const awareness = room.awareness; const encoder = encoding.createEncoder(); encoding.writeVarUint(encoder, messageSync); syncProtocol.writeSyncStep1(encoder, doc); sendWebrtcConn(this, encoder); const awarenessStates = awareness.getStates(); if (awarenessStates.size > 0) { const encoder = encoding.createEncoder(); encoding.writeVarUint(encoder, messageAwareness); encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(awareness, Array.from(awarenessStates.keys()))); sendWebrtcConn(this, encoder); } }); this.peer.on('close', () => { this.connected = false; this.closed = true; if (room.webrtcConns.has(this.remotePeerId)) { room.webrtcConns.delete(this.remotePeerId); room.provider.emit('peers', [{ removed: [this.remotePeerId], added: [], webrtcPeers: Array.from(room.webrtcConns.keys()), bcPeers: Array.from(room.bcConns) }]); } checkIsSynced(room); this.peer.destroy(); log('closed connection to ', logging.BOLD, remotePeerId); announceSignalingInfo(room); }); this.peer.on('error', err => { log('Error in connection to ', logging.BOLD, remotePeerId, ': ', err); announceSignalingInfo(room); }); this.peer.on('data', data => { const answer = readPeerMessage(this, data); if (answer !== null) { sendWebrtcConn(this, answer); } }); } destroy() { this.peer.destroy(); } } /** * @param {Room} room * @param {Uint8Array} m */ exports.WebrtcConn = WebrtcConn; const broadcastBcMessage = (room, m) => cryptoutils.encrypt(m, room.key).then(data => room.mux(() => bc.publish(room.name, data))); /** * @param {Room} room * @param {Uint8Array} m */ const broadcastRoomMessage = (room, m) => { if (room.bcconnected) { broadcastBcMessage(room, m); } broadcastWebrtcConn(room, m); }; /** * @param {Room} room */ const announceSignalingInfo = room => { signalingConns.forEach(conn => { // only subscribe if connection is established, otherwise the conn automatically subscribes to all rooms if (conn.connected) { conn.send({ type: 'subscribe', topics: [room.name] }); if (room.webrtcConns.size < room.provider.maxConns) { publishSignalingMessage(conn, room, { type: 'announce', from: room.peerId }); } } }); }; /** * @param {Room} room */ const broadcastBcPeerId = room => { if (room.provider.filterBcConns) { // broadcast peerId via broadcastchannel const encoderPeerIdBc = encoding.createEncoder(); encoding.writeVarUint(encoderPeerIdBc, messageBcPeerId); encoding.writeUint8(encoderPeerIdBc, 1); encoding.writeVarString(encoderPeerIdBc, room.peerId); broadcastBcMessage(room, encoding.toUint8Array(encoderPeerIdBc)); } }; class Room { /** * @param {Y.Doc} doc * @param {WebrtcProvider} provider * @param {string} name * @param {CryptoKey|null} key */ constructor(doc, provider, name, key) { /** * Do not assume that peerId is unique. This is only meant for sending signaling messages. * * @type {string} */ this.peerId = random.uuidv4(); this.doc = doc; /** * @type {awarenessProtocol.Awareness} */ this.awareness = provider.awareness; this.provider = provider; this.synced = false; this.name = name; // @todo make key secret by scoping this.key = key; /** * @type {Map<string, WebrtcConn>} */ this.webrtcConns = new Map(); /** * @type {Set<string>} */ this.bcConns = new Set(); this.mux = (0, _mutex.createMutex)(); this.bcconnected = false; /** * @param {ArrayBuffer} data */ this._bcSubscriber = data => cryptoutils.decrypt(new Uint8Array(data), key).then(m => this.mux(() => { const reply = readMessage(this, m, () => {}); if (reply) { broadcastBcMessage(this, encoding.toUint8Array(reply)); } })); /** * Listens to Yjs updates and sends them to remote peers * * @param {Uint8Array} update * @param {any} origin */ this._docUpdateHandler = (update, origin) => { const encoder = encoding.createEncoder(); encoding.writeVarUint(encoder, messageSync); syncProtocol.writeUpdate(encoder, update); broadcastRoomMessage(this, encoding.toUint8Array(encoder)); }; /** * Listens to Awareness updates and sends them to remote peers * * @param {any} changed * @param {any} origin */ this._awarenessUpdateHandler = ({ added, updated, removed }, origin) => { const changedClients = added.concat(updated).concat(removed); const encoderAwareness = encoding.createEncoder(); encoding.writeVarUint(encoderAwareness, messageAwareness); encoding.writeVarUint8Array(encoderAwareness, awarenessProtocol.encodeAwarenessUpdate(this.awareness, changedClients)); broadcastRoomMessage(this, encoding.toUint8Array(encoderAwareness)); }; this._beforeUnloadHandler = () => { awarenessProtocol.removeAwarenessStates(this.awareness, [doc.clientID], 'window unload'); rooms.forEach(room => { room.disconnect(); }); }; if (typeof window !== 'undefined') { window.addEventListener('beforeunload', this._beforeUnloadHandler); } else if (typeof process !== 'undefined') { process.on('exit', this._beforeUnloadHandler); } } connect() { this.doc.on('update', this._docUpdateHandler); this.awareness.on('update', this._awarenessUpdateHandler); // signal through all available signaling connections announceSignalingInfo(this); const roomName = this.name; bc.subscribe(roomName, this._bcSubscriber); this.bcconnected = true; // broadcast peerId via broadcastchannel broadcastBcPeerId(this); // write sync step 1 const encoderSync = encoding.createEncoder(); encoding.writeVarUint(encoderSync, messageSync); syncProtocol.writeSyncStep1(encoderSync, this.doc); broadcastBcMessage(this, encoding.toUint8Array(encoderSync)); // broadcast local state const encoderState = encoding.createEncoder(); encoding.writeVarUint(encoderState, messageSync); syncProtocol.writeSyncStep2(encoderState, this.doc); broadcastBcMessage(this, encoding.toUint8Array(encoderState)); // write queryAwareness const encoderAwarenessQuery = encoding.createEncoder(); encoding.writeVarUint(encoderAwarenessQuery, messageQueryAwareness); broadcastBcMessage(this, encoding.toUint8Array(encoderAwarenessQuery)); // broadcast local awareness state const encoderAwarenessState = encoding.createEncoder(); encoding.writeVarUint(encoderAwarenessState, messageAwareness); encoding.writeVarUint8Array(encoderAwarenessState, awarenessProtocol.encodeAwarenessUpdate(this.awareness, [this.doc.clientID])); broadcastBcMessage(this, encoding.toUint8Array(encoderAwarenessState)); } disconnect() { // signal through all available signaling connections signalingConns.forEach(conn => { if (conn.connected) { conn.send({ type: 'unsubscribe', topics: [this.name] }); } }); awarenessProtocol.removeAwarenessStates(this.awareness, [this.doc.clientID], 'disconnect'); // broadcast peerId removal via broadcastchannel const encoderPeerIdBc = encoding.createEncoder(); encoding.writeVarUint(encoderPeerIdBc, messageBcPeerId); encoding.writeUint8(encoderPeerIdBc, 0); // remove peerId from other bc peers encoding.writeVarString(encoderPeerIdBc, this.peerId); broadcastBcMessage(this, encoding.toUint8Array(encoderPeerIdBc)); bc.unsubscribe(this.name, this._bcSubscriber); this.bcconnected = false; this.doc.off('update', this._docUpdateHandler); this.awareness.off('update', this._awarenessUpdateHandler); this.webrtcConns.forEach(conn => conn.destroy()); } destroy() { this.disconnect(); if (typeof window !== 'undefined') { window.removeEventListener('beforeunload', this._beforeUnloadHandler); } else if (typeof process !== 'undefined') { process.off('exit', this._beforeUnloadHandler); } } } /** * @param {Y.Doc} doc * @param {WebrtcProvider} provider * @param {string} name * @param {CryptoKey|null} key * @return {Room} */ exports.Room = Room; const openRoom = (doc, provider, name, key) => { // there must only be one room if (rooms.has(name)) { throw error.create(`A Yjs Doc connected to room "${name}" already exists!`); } const room = new Room(doc, provider, name, key); rooms.set(name, /** @type {Room} */room); return room; }; /** * @param {SignalingConn} conn * @param {Room} room * @param {any} data */ const publishSignalingMessage = (conn, room, data) => { if (room.key) { cryptoutils.encryptJson(data, room.key).then(data => { conn.send({ type: 'publish', topic: room.name, data: buffer.toBase64(data) }); }); } else { conn.send({ type: 'publish', topic: room.name, data }); } }; exports.publishSignalingMessage = publishSignalingMessage; class SignalingConn extends ws.WebsocketClient { constructor(url) { super(url); /** * @type {Set<WebrtcProvider>} */ this.providers = new Set(); this.on('connect', () => { log(`connected (${url})`); const topics = Array.from(rooms.keys()); this.send({ type: 'subscribe', topics }); rooms.forEach(room => publishSignalingMessage(this, room, { type: 'announce', from: room.peerId })); }); this.on('message', m => { switch (m.type) { case 'publish': { const roomName = m.topic; const room = rooms.get(roomName); if (room == null || typeof roomName !== 'string') { return; } const execMessage = data => { const webrtcConns = room.webrtcConns; const peerId = room.peerId; if (data == null || data.from === peerId || data.to !== undefined && data.to !== peerId || room.bcConns.has(data.from)) { // ignore messages that are not addressed to this conn, or from clients that are connected via broadcastchannel return; } const emitPeerChange = webrtcConns.has(data.from) ? () => {} : () => room.provider.emit('peers', [{ removed: [], added: [data.from], webrtcPeers: Array.from(room.webrtcConns.keys()), bcPeers: Array.from(room.bcConns) }]); switch (data.type) { case 'announce': if (webrtcConns.size < room.provider.maxConns) { map.setIfUndefined(webrtcConns, data.from, () => new WebrtcConn(this, true, data.from, room)); emitPeerChange(); } break; case 'signal': if (data.signal.type === 'offer') { const existingConn = webrtcConns.get(data.from); if (existingConn) { const remoteToken = data.token; const localToken = existingConn.glareToken; if (localToken && localToken > remoteToken) { log('offer rejected: ', data.from); return; } // if we don't reject the offer, we will be accepting it and answering it existingConn.glareToken = undefined; } } if (data.signal.type === 'answer') { log('offer answered by: ', data.from); const existingConn = webrtcConns.get(data.from); existingConn.glareToken = undefined; } if (data.to === peerId) { map.setIfUndefined(webrtcConns, data.from, () => new WebrtcConn(this, false, data.from, room)).peer.signal(data.signal); emitPeerChange(); } break; } }; if (room.key) { if (typeof m.data === 'string') { cryptoutils.decryptJson(buffer.fromBase64(m.data), room.key).then(execMessage); } } else { execMessage(m.data); } } } }); this.on('disconnect', () => log(`disconnect (${url})`)); } } /** * @typedef {Object} ProviderOptions * @property {Array<string>} [signaling] * @property {string} [password] * @property {awarenessProtocol.Awareness} [awareness] * @property {number} [maxConns] * @property {boolean} [filterBcConns] * @property {any} [peerOpts] */ /** * @extends Observable<string> */ exports.SignalingConn = SignalingConn; class WebrtcProvider extends _observable.Observable { /** * @param {string} roomName * @param {Y.Doc} doc * @param {ProviderOptions?} opts */ constructor(roomName, doc, { signaling = ['wss://y-webrtc-eu.fly.dev'], password = null, awareness = new awarenessProtocol.Awareness(doc), maxConns = 20 + math.floor(random.rand() * 15), // the random factor reduces the chance that n clients form a cluster filterBcConns = true, peerOpts = {} // simple-peer options. See https://github.com/feross/simple-peer#peer--new-peeropts } = {}) { super(); this.roomName = roomName; this.doc = doc; this.filterBcConns = filterBcConns; /** * @type {awarenessProtocol.Awareness} */ this.awareness = awareness; this.shouldConnect = false; this.signalingUrls = signaling; this.signalingConns = []; this.maxConns = maxConns; this.peerOpts = peerOpts; /** * @type {PromiseLike<CryptoKey | null>} */ this.key = password ? cryptoutils.deriveKey(password, roomName) : (/** @type {PromiseLike<null>} */promise.resolve(null)); /** * @type {Room|null} */ this.room = null; this.key.then(key => { this.room = openRoom(doc, this, roomName, key); if (this.shouldConnect) { this.room.connect(); } else { this.room.disconnect(); } }); this.connect(); this.destroy = this.destroy.bind(this); doc.on('destroy', this.destroy); } /** * @type {boolean} */ get connected() { return this.room !== null && this.shouldConnect; } connect() { this.shouldConnect = true; this.signalingUrls.forEach(url => { const signalingConn = map.setIfUndefined(signalingConns, url, () => new SignalingConn(url)); this.signalingConns.push(signalingConn); signalingConn.providers.add(this); }); if (this.room) { this.room.connect(); } } disconnect() { this.shouldConnect = false; this.signalingConns.forEach(conn => { conn.providers.delete(this); if (conn.providers.size === 0) { conn.destroy(); signalingConns.delete(conn.url); } }); if (this.room) { this.room.disconnect(); } } destroy() { this.doc.off('destroy', this.destroy); // need to wait for key before deleting room this.key.then(() => { /** @type {Room} */this.room.destroy(); rooms.delete(this.roomName); }); super.destroy(); } } exports.WebrtcProvider = WebrtcProvider; //# sourceMappingURL=y-webrtc.js.map