UNPKG

@wordpress/sync

Version:
719 lines (717 loc) 22.1 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // packages/sync/src/providers/y-webrtc/y-webrtc.js var y_webrtc_exports = {}; __export(y_webrtc_exports, { Room: () => Room, SignalingConn: () => SignalingConn, WebrtcConn: () => WebrtcConn, WebrtcProvider: () => WebrtcProvider, log: () => log, publishSignalingMessage: () => publishSignalingMessage, rooms: () => rooms, signalingConns: () => signalingConns }); module.exports = __toCommonJS(y_webrtc_exports); var ws = __toESM(require("lib0/websocket")); var map = __toESM(require("lib0/map")); var error = __toESM(require("lib0/error")); var random = __toESM(require("lib0/random")); var encoding = __toESM(require("lib0/encoding")); var decoding = __toESM(require("lib0/decoding")); var import_observable = require("lib0/observable"); var logging = __toESM(require("lib0/logging")); var promise = __toESM(require("lib0/promise")); var bc = __toESM(require("lib0/broadcastchannel")); var buffer = __toESM(require("lib0/buffer")); var math = __toESM(require("lib0/math")); var import_mutex = require("lib0/mutex"); var Y = __toESM(require("yjs")); var import_simplepeer_min = __toESM(require("simple-peer/simplepeer.min.js")); var syncProtocol = __toESM(require("y-protocols/sync")); var awarenessProtocol = __toESM(require("y-protocols/awareness")); var cryptoutils = __toESM(require("./crypto.js")); var log = logging.createModuleLogger("y-webrtc"); var messageSync = 0; var messageQueryAwareness = 3; var messageAwareness = 1; var messageBcPeerId = 4; var signalingConns = /* @__PURE__ */ new Map(); var rooms = /* @__PURE__ */ new Map(); var 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" ); } }; var readMessage = (room, buf, syncedCallback) => { const decoder = decoding.createDecoder(buf); const encoder = encoding.createEncoder(); const messageType = decoding.readVarUint(decoder); if (room === void 0) { 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) { return null; } return encoder; }; var 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); }); }; var 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) { } }; var broadcastWebrtcConn = (room, m) => { log("broadcast message in ", logging.BOLD, room.name, logging.UNBOLD); room.webrtcConns.forEach((conn) => { try { conn.peer.send(m); } catch (e) { } }); }; var WebrtcConn = class { /** * @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 = void 0; this.closed = false; this.connected = false; this.synced = false; this.peer = new import_simplepeer_min.default({ initiator, ...room.provider.peerOpts }); this.peer.on("signal", (signal) => { if (this.glareToken === void 0) { 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; 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 encoder2 = encoding.createEncoder(); encoding.writeVarUint(encoder2, messageAwareness); encoding.writeVarUint8Array( encoder2, awarenessProtocol.encodeAwarenessUpdate( awareness, Array.from(awarenessStates.keys()) ) ); sendWebrtcConn(this, encoder2); } }); 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(); } }; var broadcastBcMessage = (room, m) => cryptoutils.encrypt(m, room.key).then((data) => room.mux(() => bc.publish(room.name, data))); var broadcastRoomMessage = (room, m) => { if (room.bcconnected) { broadcastBcMessage(room, m); } broadcastWebrtcConn(room, m); }; var announceSignalingInfo = (room) => { signalingConns.forEach((conn) => { 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 }); } } }); }; var broadcastBcPeerId = (room) => { if (room.provider.filterBcConns) { const encoderPeerIdBc = encoding.createEncoder(); encoding.writeVarUint(encoderPeerIdBc, messageBcPeerId); encoding.writeUint8(encoderPeerIdBc, 1); encoding.writeVarString(encoderPeerIdBc, room.peerId); broadcastBcMessage(room, encoding.toUint8Array(encoderPeerIdBc)); } }; var Room = class { /** * @param {Y.Doc} doc * @param {WebrtcProvider} provider * @param {string} name * @param {CryptoKey|null} key */ constructor(doc, provider, name, key) { this.peerId = random.uuidv4(); this.doc = doc; this.awareness = provider.awareness; this.provider = provider; this.synced = false; this.name = name; this.key = key; this.webrtcConns = /* @__PURE__ */ new Map(); this.bcConns = /* @__PURE__ */ new Set(); this.mux = (0, import_mutex.createMutex)(); this.bcconnected = false; 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) ); } }) ); this._docUpdateHandler = (update, origin) => { const encoder = encoding.createEncoder(); encoding.writeVarUint(encoder, messageSync); syncProtocol.writeUpdate(encoder, update); broadcastRoomMessage(this, encoding.toUint8Array(encoder)); }; 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); announceSignalingInfo(this); const roomName = this.name; bc.subscribe(roomName, this._bcSubscriber); this.bcconnected = true; broadcastBcPeerId(this); const encoderSync = encoding.createEncoder(); encoding.writeVarUint(encoderSync, messageSync); syncProtocol.writeSyncStep1(encoderSync, this.doc); broadcastBcMessage(this, encoding.toUint8Array(encoderSync)); const encoderState = encoding.createEncoder(); encoding.writeVarUint(encoderState, messageSync); syncProtocol.writeSyncStep2(encoderState, this.doc); broadcastBcMessage(this, encoding.toUint8Array(encoderState)); const encoderAwarenessQuery = encoding.createEncoder(); encoding.writeVarUint(encoderAwarenessQuery, messageQueryAwareness); broadcastBcMessage( this, encoding.toUint8Array(encoderAwarenessQuery) ); const encoderAwarenessState = encoding.createEncoder(); encoding.writeVarUint(encoderAwarenessState, messageAwareness); encoding.writeVarUint8Array( encoderAwarenessState, awarenessProtocol.encodeAwarenessUpdate(this.awareness, [ this.doc.clientID ]) ); broadcastBcMessage( this, encoding.toUint8Array(encoderAwarenessState) ); } disconnect() { signalingConns.forEach((conn) => { if (conn.connected) { conn.send({ type: "unsubscribe", topics: [this.name] }); } }); awarenessProtocol.removeAwarenessStates( this.awareness, [this.doc.clientID], "disconnect" ); const encoderPeerIdBc = encoding.createEncoder(); encoding.writeVarUint(encoderPeerIdBc, messageBcPeerId); encoding.writeUint8(encoderPeerIdBc, 0); 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); } } }; var openRoom = (doc, provider, name, key) => { 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; }; var publishSignalingMessage = (conn, room, data) => { if (room.key) { cryptoutils.encryptJson(data, room.key).then((data2) => { conn.send({ type: "publish", topic: room.name, data: buffer.toBase64(data2) }); }); } else { conn.send({ type: "publish", topic: room.name, data }); } }; var SignalingConn = class extends ws.WebsocketClient { constructor(url) { super(url); this.providers = /* @__PURE__ */ 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 !== void 0 && data.to !== peerId || room.bcConns.has(data.from)) { 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; } existingConn.glareToken = void 0; } } if (data.signal.type === "answer") { log("offer answered by: ", data.from); const existingConn = webrtcConns.get( data.from ); existingConn.glareToken = void 0; } 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})`)); } }; var WebrtcProvider = class extends import_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; this.awareness = awareness; this.shouldConnect = false; this.signalingUrls = signaling; this.signalingConns = []; this.maxConns = maxConns; this.peerOpts = peerOpts; this.key = password ? cryptoutils.deriveKey(password, roomName) : ( /** @type {PromiseLike<null>} */ promise.resolve(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); this.key.then(() => { this.room.destroy(); rooms.delete(this.roomName); }); super.destroy(); } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Room, SignalingConn, WebrtcConn, WebrtcProvider, log, publishSignalingMessage, rooms, signalingConns }); //# sourceMappingURL=y-webrtc.js.map