@wordpress/sync
Version:
719 lines (717 loc) • 22.1 kB
JavaScript
"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