UNPKG

@substrate-system/mergeparty

Version:
294 lines (293 loc) 9.51 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 __name = (target, value) => __defProp(target, "name", { value, configurable: true }); 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); var relay_exports = {}; __export(relay_exports, { Relay: () => Relay }); module.exports = __toCommonJS(relay_exports); var import_cborg = require("cborg"); var import_eventemitter3 = require("eventemitter3"); var import_cloudflare = __toESM(require("@substrate-system/debug/cloudflare"), 1); var import_index = require("./index.js"); var import_messages = require("@substrate-system/automerge-repo-network-websocket/messages"); var import_protocolVersion = require("@substrate-system/automerge-repo-network-websocket/protocolVersion"); var import_util = require("../util.js"); const debug = (0, import_cloudflare.default)("mergeparty:relay"); class Relay extends import_eventemitter3.EventEmitter { static { __name(this, "Relay"); } // eslint-disable-line brace-style room; serverPeerId; isStorageServer = false; peerId; // our peer ID peerMetadata; // our peer metadata sockets = {}; _log; _baseLog; // Connection -> meta { peerId?:string, joined:boolean } byConn = /* @__PURE__ */ new Map(); constructor(room) { super(); this.room = room; this.serverPeerId = `server:${room.id}`; this._baseLog = (0, import_cloudflare.default)("mergeparty"); this._log = this._baseLog.extend("relay"); } listenerCount(_event) { return 0; } eventNames() { const eventKeys = [ "close", "peer-candidate", "peer-disconnected", "message" ]; return eventKeys; } // --- NetworkAdapterInterface required methods --- isReady() { return true; } /** * Called by the Repo to start things. * @param {PeerId} peerId The peerId of *this repo*. * @param {PeerMetadata} meta How this adapter should present itselft * to other peers. */ connect(peerId, meta) { this.peerId = peerId; this.peerMetadata = meta; } disconnect() { } send(message) { if ("data" in message && message.data?.byteLength === 0) { throw new Error("Tried to send a zero-length message"); } const to = this.sockets[message.targetId]; if (!to) { this._log(`Tried to send to disconnected peer: ${message.targetId}`); return; } const encoded = (0, import_cborg.encode)(message); to.send((0, import_util.toArrayBuffer)(encoded)); } open() { } close() { } subscribe() { } unsubscribe() { } get networkId() { return this.room.id; } // Abstract method from NetworkAdapter whenReady() { return Promise.resolve(); } // ---- WebSocket lifecycle ---- onConnect(conn) { this.byConn.set(conn, { joined: false }); } onClose(conn) { const meta = this.byConn.get(conn); if (meta?.peerId) { delete this.sockets[meta.peerId]; } this.byConn.delete(conn); this.emit("peer-disconnected", { peerId: this.byConn.get(conn)?.peerId }); } cborEncode(data) { return (0, import_cborg.encode)(data); } cborDecode(raw) { return (0, import_cborg.decode)((0, import_util.toU8)(raw)); } /** * Decode CBOR messages. * This handles 'join' + handshake process. * Emits `peer-candidate` and `message` events. * * @fires peer-candidate * @fires message */ async onMessage(raw, conn) { debug("[Relay] Received message from client"); if (typeof raw === "string") { this.sendErrorAndClose( conn, "Expected binary CBOR frame, got string" ); return; } let message; try { message = (0, import_cborg.decode)((0, import_util.toU8)(raw)); } catch (_err) { const err = _err; console.error(err.message); conn.close(); return; } const meta = this.byConn.get(conn) ?? { joined: false }; if (!meta.joined) { if (!(0, import_messages.isJoinMessage)(message)) { this.emit("message", message); return; } const join = message; const versions = join.supportedProtocolVersions ?? ["1"]; if (!versions.includes(import_index.SUPPORTED_PROTOCOL_VERSION)) { return this.sendErrorAndClose( conn, `Unsupported protocol version. Server supports ${import_index.SUPPORTED_PROTOCOL_VERSION}` ); } if (!join.senderId || typeof join.senderId !== "string") { this.sendErrorAndClose(conn, "`senderId` missing or invalid"); return; } const { senderId, peerMetadata, supportedProtocolVersions } = join; this.emit("peer-candidate", { peerId: senderId, peerMetadata }); this.sockets[senderId] = conn; this.sockets[join.senderId] = conn; this.byConn.set(conn, { joined: true, peerId: join.senderId }); const selectedProtocolVersion = selectProtocol(supportedProtocolVersions); if (selectedProtocolVersion === null) { this.send({ type: "error", senderId: this.peerId, message: "unsupported protocol version", targetId: senderId }); this.sockets[senderId].close(); delete this.sockets[senderId]; } else { this.send({ type: "peer", senderId: this.peerId, peerMetadata: this.peerMetadata, selectedProtocolVersion: import_protocolVersion.ProtocolV1, targetId: senderId }); } for (const existingId of Object.keys(this.sockets)) { if (existingId === join.senderId) continue; this.announce(existingId, join.senderId); } for (const existingId of Object.keys(this.sockets)) { if (existingId === join.senderId) continue; this.announce(join.senderId, existingId); } if (this.isStorageServer) { (0, import_util.assert)(this.peerId); this.announce(this.peerId, join.senderId); } return; } const msg = message; const t = msg.targetId; const isAliasToServer = typeof t === "string" && t.startsWith("server:"); const deliverToLocal = t === this.peerId || isAliasToServer; if (deliverToLocal) { const localMsg = isAliasToServer ? { ...msg, targetId: this.peerId } : msg; this.emit("message", localMsg); } if (t) { const target = this.sockets[t]; if (target) { target.send(raw); return; } } if (isAliasToServer) { for (const [peerId, conn2] of Object.entries(this.sockets)) { if (peerId === msg.senderId) continue; conn2.send((0, import_util.toArrayBuffer)((0, import_cborg.encode)({ ...msg, targetId: peerId }))); } } } announce(announcedPeerId, toClientId) { const msg = { type: "peer", senderId: announcedPeerId, // the peer being announced targetId: toClientId, // the client who should learn about it selectedProtocolVersion: import_index.SUPPORTED_PROTOCOL_VERSION, peerMetadata: {} }; const toConn = this.sockets[toClientId]; if (toConn) { toConn.send((0, import_util.toArrayBuffer)((0, import_cborg.encode)(msg))); } } // HTTP endpoint for health check async onRequest(req) { if (new URL(req.url).pathname.includes("/health")) { return Response.json({ status: "ok", room: this.room.id, connectedPeers: Array.from(this.room.getConnections()).length }, { status: 200, headers: import_index.CORS }); } return new Response("\u{1F44D} All good", { status: 200, headers: import_index.CORS }); } // ---- helpers ---- sendErrorAndClose(conn, message) { const errorMsg = { type: "error", message }; try { conn.send((0, import_util.toArrayBuffer)((0, import_cborg.encode)(errorMsg))); } finally { conn.close(); } } } const selectProtocol = /* @__PURE__ */ __name((versions) => { if (versions === void 0) return import_protocolVersion.ProtocolV1; if (versions.includes(import_protocolVersion.ProtocolV1)) return import_protocolVersion.ProtocolV1; return null; }, "selectProtocol"); //# sourceMappingURL=relay.cjs.map