Add deno-only bootstrap peer implementation. Uses libdatachannel for the RTCDatachannel implementation.

Fix Typescript sourcemap serving
PeerManager: more robust when RTCPeerConnection fails or is not present
Separate source maps so the main files arent bloated
This commit is contained in:
2025-06-10 20:23:19 -07:00
parent a96d7e28c8
commit d0fd041f7e
27 changed files with 132 additions and 149 deletions

View File

@@ -33,6 +33,9 @@ export class PeerManager {
let peername = `${adjective}_${snake}`;
return peername;
}
isBootstrapPeer(peerID) {
return this.bootstrapPeerIDs?.has(peerID);
}
websocketSend(message) {
if (!this.websocket) {
throw new Error();
@@ -60,7 +63,7 @@ export class PeerManager {
}
this.messageSuperlog && console.log.apply(null, log("->signaler:", message));
if (message.type === "hello2") {
if (!this.isBootstrapPeer && Array.isArray(message?.bootstrapPeers)) {
if (!this._isBootstrapPeer && Array.isArray(message?.bootstrapPeers)) {
this.bootstrapPeerIDs = new Set(message.bootstrapPeers);
}
this.onHello2Received(this.bootstrapPeerIDs);
@@ -79,7 +82,7 @@ export class PeerManager {
if (!peerConnection) {
let remotePeerID = message.from;
let newPeer = new PeerConnection(this, remotePeerID, this.websocketSendPeerMessage.bind(this));
if (this.isBootstrapPeer) {
if (this._isBootstrapPeer) {
newPeer.setPolite(false);
}
peerConnection = newPeer;
@@ -105,7 +108,7 @@ export class PeerManager {
return newPeer;
}
async onHello2Received(bootstrapPeerIDs) {
if (this.isBootstrapPeer) {
if (this._isBootstrapPeer) {
this.connectPromiseCallbacks?.resolve();
return;
}
@@ -139,7 +142,7 @@ export class PeerManager {
peer_id: this.peerID,
session_id: this.sessionID,
// peer_name: app.peername,
is_bootstrap_peer: this.isBootstrapPeer,
is_bootstrap_peer: this._isBootstrapPeer,
// peer_description: this.rtcPeerDescription
});
}
@@ -164,7 +167,7 @@ export class PeerManager {
this.searchQueryFunctions = new Map();
this.RPC_remote = new Map();
this.rpc = {};
this.isBootstrapPeer = false;
this._isBootstrapPeer = false;
this.bootstrapPeerConnections = null;
this.sessionID = generateID();
this.websocket = null;
@@ -188,7 +191,7 @@ export class PeerManager {
this.animals = ['shrew', 'jerboa', 'lemur', 'weasel', 'possum', 'possum', 'marmoset', 'planigale', 'mole', 'narwhal'];
this.adjectives = ['snazzy', 'whimsical', 'jazzy', 'bonkers', 'wobbly', 'spiffy', 'chirpy', 'zesty', 'bubbly', 'perky', 'sassy'];
this.snakes = ['mamba', 'cobra', 'python', 'viper', 'krait', 'sidewinder', 'constrictor', 'boa', 'asp', 'anaconda', 'krait'];
this.isBootstrapPeer = isBootstrapPeer;
this._isBootstrapPeer = isBootstrapPeer;
this.peers = new Map();
this.routingTable = new Map();
this.userID = userID;
@@ -244,7 +247,7 @@ export class PeerManager {
}
numActive++;
}
if (!this.isBootstrapPeer && numActive === 0) {
if (!this._isBootstrapPeer && numActive === 0) {
console.log.apply(null, log(`No peers connected, will attempt to reconnect in ${this.reconnectPeriod} seconds...`));
// Websocket reconnect
if (this.websocket?.readyState === WebSocket.OPEN) {
@@ -257,6 +260,11 @@ export class PeerManager {
let output = `Current status:` + "\n" + `[${logID(this.peerID)}]${this.getPeername(this.peerID)}[local]` + "\n";
for (let [peerID, peer] of this.peers) {
output += `[${logID(peerID)}]${peer.rtcPeer?.connectionState}:${this.getPeername(peerID)}${this.bootstrapPeerIDs?.has(peerID) ? "[Bootstrap]" : ""}` + "\n";
if (peer.rpcSuperlog) {
for (let transactionID of peer.pendingRPCs.keys()) {
output += `[${logID(transactionID)}]`;
}
}
}
output += `numActivePeers: ${numActive}` + "\n";
console.log.apply(null, log(output));
@@ -421,7 +429,7 @@ class PeerConnection {
this.send({ type: "hello datachannel", from: this.peerManager.peerID, to: this.remotePeerID });
// this.dataChannel?.send(`{typeHello datachannel from: ${this.peerManager.peerID}`);
// console.log.apply(null, log([...this.peerManager.peers.keys()]));
if (this.peerManager.isBootstrapPeer) {
if (this.peerManager._isBootstrapPeer) {
this.send({ type: 'initial_peers', from: this.peerManager.peerID, peers: [...this.peerManager.peers.keys()].filter(entry => entry !== this.remotePeerID) });
// this.dataChannel.send(JSON.stringify());
}
@@ -442,6 +450,9 @@ class PeerConnection {
}
async connect() {
let connectionPromise = new Promise((resolve, reject) => { this.connectionPromise = { resolve, reject }; });
if (!(typeof RTCPeerConnection === "function")) {
throw new Error("RTCPeerConnection is not a function, exiting.");
}
this.rtcPeer = new RTCPeerConnection(PeerConnection.config);
this.rtcPeer.onconnectionstatechange = async (e) => {
this.webRTCSuperlog && console.log.apply(null, log(`rtcPeer: onconnectionstatechange: ${this.rtcPeer?.connectionState}: ${this.remotePeerID}`));
@@ -519,14 +530,19 @@ class PeerConnection {
}
async onWebsocketMessage(message) {
if (message.type == "rtc_connect") {
this.rtcPeer?.setRemoteDescription(message.description);
try {
this.rtcPeer?.setRemoteDescription(message.description);
}
catch (e) {
console.log(e);
}
}
// /*
// let ignoreOffer = false;
// let isSettingRemoteAnswerPending = false;
// signaler.onmessage = async ({ data: { description, candidate } }) => {
if (!this.rtcPeer) {
throw new Error();
throw new Error("Unable to instantiate RTCPeerConnection, exiting.");
}
let description = null;
if (message.type == "rtc_description") {
@@ -547,7 +563,12 @@ class PeerConnection {
return;
}
this.isSettingRemoteAnswerPending = description.type == "answer";
await this.rtcPeer.setRemoteDescription(description);
try {
await this.rtcPeer.setRemoteDescription(description);
}
catch (e) {
console.log("PeerConnection:setRemoteDescription:failed:", e, description);
}
this.isSettingRemoteAnswerPending = false;
if (description.type === "offer") {
await this.rtcPeer.setLocalDescription();
@@ -558,9 +579,10 @@ class PeerConnection {
try {
await this.rtcPeer.addIceCandidate(candidate);
}
catch (err) {
catch (e) {
if (!this.ignoreOffer) {
throw err;
console.log("PeerConnection:addIceCandidate:failed:", e, candidate);
throw e;
}
}
}
@@ -632,7 +654,7 @@ class PeerConnection {
// Think about a timeout here to auto reject it after a while.
let promise = new Promise((resolve, reject) => {
this.pendingRPCs.set(transactionID, { resolve, reject, functionName });
// setTimeout(() => reject("bad"), 1000);
setTimeout(() => reject(`function:${functionName}[${transactionID}] failed to resolve after 10 seconds.`), 10000);
});
let message = {
type: "rpc_call",
@@ -661,14 +683,12 @@ class PeerConnection {
throw new Error();
}
pendingRPC.resolve(message.response);
this.pendingRPCs.delete(message.transaction_id);
}
if (type === "rpc_call") {
this.rpcSuperlog && console.log.apply(null, log(`[${logID(this.remotePeerID)}]->[rpc][${logID(this.peerManager.peerID)}] call: `, message.function_name, message.transaction_id, JSON.stringify(message.args, null, 2)));
let response = await this.peerManager.callFromRemote(message.function_name, message.args);
this.rpcSuperlog && console.log.apply(null, log(`[rpc] call: response:`, response));
if (response === undefined) {
return;
}
let responseMessage = { type: 'rpc_response', function_name: message.function_name, transaction_id: message.transaction_id, response: response };
this.send(responseMessage);
}
@@ -715,3 +735,4 @@ PeerConnection.config = {
{ urls: "stun:stun4.l.google.com" },
],
};
//# sourceMappingURL=PeerManager.js.map