Merge remote-tracking branch 'origin/main' into bobbyd-status-bar
This commit is contained in:
@@ -78,7 +78,7 @@ export class PeerManager {
|
|||||||
return { first, second }
|
return { first, second }
|
||||||
}
|
}
|
||||||
|
|
||||||
getPeername(peerID:string) {
|
getPeername(peerID: string) {
|
||||||
let { first: adjective, second: snake } = this.funkyName(peerID, this.adjectives, this.snakes);
|
let { first: adjective, second: snake } = this.funkyName(peerID, this.adjectives, this.snakes);
|
||||||
let peername = `${adjective}_${snake}`
|
let peername = `${adjective}_${snake}`
|
||||||
return peername;
|
return peername;
|
||||||
@@ -293,7 +293,7 @@ export class PeerManager {
|
|||||||
let numActive = 0;
|
let numActive = 0;
|
||||||
|
|
||||||
for (let [id, peer] of this.peers) {
|
for (let [id, peer] of this.peers) {
|
||||||
if (id === this.bootstrapPeerID ||
|
if (/*id === this.bootstrapPeerID ||*/
|
||||||
peer.rtcPeer?.connectionState === "new" ||
|
peer.rtcPeer?.connectionState === "new" ||
|
||||||
peer.rtcPeer?.connectionState === "connecting"
|
peer.rtcPeer?.connectionState === "connecting"
|
||||||
) {
|
) {
|
||||||
@@ -321,7 +321,7 @@ export class PeerManager {
|
|||||||
|
|
||||||
let output = `Current status:` + "\n" + `[${logID(this.peerID)}]${this.getPeername(this.peerID)}[local]` + "\n";
|
let output = `Current status:` + "\n" + `[${logID(this.peerID)}]${this.getPeername(this.peerID)}[local]` + "\n";
|
||||||
for (let [peerID, peer] of this.peers) {
|
for (let [peerID, peer] of this.peers) {
|
||||||
output += `[${logID(peerID)}]${peer.rtcPeer?.connectionState}:${this.getPeername(peerID)}${(peerID === this.bootstrapPeerID) ? "[Bootstrap]":""}` + "\n";
|
output += `[${logID(peerID)}]${peer.rtcPeer?.connectionState}:${this.getPeername(peerID)}${(peerID === this.bootstrapPeerID) ? "[Bootstrap]" : ""}` + "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
output += `numActivePeers: ${numActive}` + "\n";
|
output += `numActivePeers: ${numActive}` + "\n";
|
||||||
@@ -396,11 +396,11 @@ export class PeerManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onPeerDisconnected(remotePeerID: PeerID) {
|
onPeerDisconnected(peerID: PeerID) {
|
||||||
let deleted = this.peers.delete(remotePeerID);
|
let deleted = this.peers.delete(peerID);
|
||||||
|
|
||||||
if (!deleted) {
|
if (!deleted) {
|
||||||
throw new Error(`Can't find peer that disconnected ${remotePeerID}`);
|
throw new Error(`Can't find peer that disconnected ${peerID}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: What do we do if we lose connection to the bootstrap peer?
|
// TODO: What do we do if we lose connection to the bootstrap peer?
|
||||||
@@ -409,12 +409,14 @@ export class PeerManager {
|
|||||||
// We should disconnect from the websocket once we connect to our intial peers.
|
// We should disconnect from the websocket once we connect to our intial peers.
|
||||||
|
|
||||||
// If we have no peer connections, try to connect. If connection fails, start a timer to reconnect.
|
// If we have no peer connections, try to connect. If connection fails, start a timer to reconnect.
|
||||||
if (remotePeerID === this.bootstrapPeerID) {
|
if (peerID === this.bootstrapPeerID) {
|
||||||
this.bootstrapPeerID = null;
|
this.bootstrapPeerID = null;
|
||||||
this.bootstrapPeerConnection = null;
|
this.bootstrapPeerConnection = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dispatchEvent(PeerEventTypes.PEER_DISCONNECTED, { peerID: remotePeerID });
|
this.peerStateSuperlog && console.log.apply(null, log(`PeerManager: disconnected from peer ${peerID}`));
|
||||||
|
|
||||||
|
this.dispatchEvent(PeerEventTypes.PEER_DISCONNECTED, { peerID: peerID });
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -499,7 +501,7 @@ class PeerConnection {
|
|||||||
private ignoreOffer: boolean = false;
|
private ignoreOffer: boolean = false;
|
||||||
private isSettingRemoteAnswerPending: boolean = false;
|
private isSettingRemoteAnswerPending: boolean = false;
|
||||||
private polite = true;
|
private polite = true;
|
||||||
private webRTCSuperlog = false;
|
private webRTCSuperlog = true;
|
||||||
private dataChannelSuperlog = false;
|
private dataChannelSuperlog = false;
|
||||||
private chunkSize = (16 * 1024) - 100;
|
private chunkSize = (16 * 1024) - 100;
|
||||||
messageSuperlog: boolean = false;
|
messageSuperlog: boolean = false;
|
||||||
@@ -518,7 +520,7 @@ class PeerConnection {
|
|||||||
static config = {
|
static config = {
|
||||||
iceServers: [
|
iceServers: [
|
||||||
{ urls: "stun:ddln.app" },
|
{ urls: "stun:ddln.app" },
|
||||||
{ urls: "turn:ddln.app", username: "a", credential: "b" },
|
// { urls: "turn:ddln.app", username: "a", credential: "b" },
|
||||||
{ urls: "stun:stun.l.google.com" }, // keeping this for now as my STUN server is not returning ipv6
|
{ urls: "stun:stun.l.google.com" }, // keeping this for now as my STUN server is not returning ipv6
|
||||||
{ urls: "stun:stun1.l.google.com" },
|
{ urls: "stun:stun1.l.google.com" },
|
||||||
{ urls: "stun:stun2.l.google.com" },
|
{ urls: "stun:stun2.l.google.com" },
|
||||||
@@ -649,9 +651,23 @@ class PeerConnection {
|
|||||||
// this.rtcPeer.onicecandidate = ({ candidate }) => this.signaler.send(JSON.stringify({ candidate }));
|
// this.rtcPeer.onicecandidate = ({ candidate }) => this.signaler.send(JSON.stringify({ candidate }));
|
||||||
// this.rtcPeer.onicecandidate = ({ candidate }) => console.log.apply(null, log(candidate);
|
// this.rtcPeer.onicecandidate = ({ candidate }) => console.log.apply(null, log(candidate);
|
||||||
|
|
||||||
|
this.rtcPeer.onicegatheringstatechange = (event) => {
|
||||||
|
this.webRTCSuperlog && console.log.apply(null, log("onicegatheringstatechange:", this.rtcPeer?.iceGatheringState));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rtcPeer.oniceconnectionstatechange = (event:Event) => {
|
||||||
|
this.webRTCSuperlog && console.log.apply(null, log("oniceconnectionstatechange:", this.rtcPeer?.iceConnectionState));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
this.rtcPeer.onicecandidateerror = (event: RTCPeerConnectionIceErrorEvent) => {
|
||||||
|
this.webRTCSuperlog && console.log.apply(null, log(`onicecandidateerror: ${event.errorCode} ${event.errorText} ${event.address} ${event.url}`));
|
||||||
|
}
|
||||||
|
|
||||||
this.rtcPeer.onicecandidate = ({ candidate }) => {
|
this.rtcPeer.onicecandidate = ({ candidate }) => {
|
||||||
this.webRTCSuperlog && console.log.apply(null, log(candidate));
|
this.webRTCSuperlog && console.log.apply(null, log(`onicecandidate`, candidate));
|
||||||
this.sendPeerMessage(this.remotePeerID, { type: "rtc_candidate", candidate: candidate });
|
this.sendPeerMessage(this.remotePeerID, { type: "rtc_candidate", candidate: candidate });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1094
static/App.js
Normal file
1094
static/App.js
Normal file
File diff suppressed because it is too large
Load Diff
9
static/IDUtils.js
Normal file
9
static/IDUtils.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
function uuidv4() {
|
||||||
|
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16));
|
||||||
|
}
|
||||||
|
export function generateID() {
|
||||||
|
if (self.crypto.hasOwnProperty("randomUUID")) {
|
||||||
|
return self.crypto.randomUUID();
|
||||||
|
}
|
||||||
|
return uuidv4();
|
||||||
|
}
|
||||||
701
static/PeerManager.js
Normal file
701
static/PeerManager.js
Normal file
@@ -0,0 +1,701 @@
|
|||||||
|
// connect to WS server, send info, connecto to bootstrap peer
|
||||||
|
// once connected to bootstrap peer,
|
||||||
|
// Goal, connect to bootstrap peer, ask bootstrap peer for peers that have posts from users that we care about. get peers, connect to those peers, sync.
|
||||||
|
// how? do "perfect negotiation" with bootstrap peer. All logic here moves to BP.
|
||||||
|
import { generateID } from "IDUtils";
|
||||||
|
import { log, logID } from "log";
|
||||||
|
export var PeerEventTypes;
|
||||||
|
(function (PeerEventTypes) {
|
||||||
|
PeerEventTypes[PeerEventTypes["PEER_CONNECTED"] = 0] = "PEER_CONNECTED";
|
||||||
|
PeerEventTypes[PeerEventTypes["PEER_DISCONNECTED"] = 1] = "PEER_DISCONNECTED";
|
||||||
|
})(PeerEventTypes || (PeerEventTypes = {}));
|
||||||
|
export class PeerManager {
|
||||||
|
hashIdToIndices(id) {
|
||||||
|
let indices = [];
|
||||||
|
for (let char of id) {
|
||||||
|
if (char !== '0' && char !== '-') {
|
||||||
|
indices.push(parseInt(char, 16));
|
||||||
|
if (indices.length == 2) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [indices[0], indices[1]];
|
||||||
|
}
|
||||||
|
funkyName(id, listOne, listTwo) {
|
||||||
|
let [one, two] = this.hashIdToIndices(id);
|
||||||
|
let first = listOne[one % listOne.length];
|
||||||
|
let second = listTwo[two % listTwo.length];
|
||||||
|
return { first, second };
|
||||||
|
}
|
||||||
|
getPeername(peerID) {
|
||||||
|
let { first: adjective, second: snake } = this.funkyName(peerID, this.adjectives, this.snakes);
|
||||||
|
let peername = `${adjective}_${snake}`;
|
||||||
|
return peername;
|
||||||
|
}
|
||||||
|
websocketSend(message) {
|
||||||
|
if (!this.websocket) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
let messageJSON = "";
|
||||||
|
try {
|
||||||
|
messageJSON = JSON.stringify(message);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
log(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.messageSuperlog && console.log.apply(null, log("<-signaler:", message));
|
||||||
|
this.websocket.send(messageJSON);
|
||||||
|
}
|
||||||
|
onWebsocketMessage(event) {
|
||||||
|
let messageJSON = event.data;
|
||||||
|
let message = null;
|
||||||
|
try {
|
||||||
|
message = JSON.parse(messageJSON);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
log(e);
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
this.messageSuperlog && console.log.apply(null, log("->signaler:", message));
|
||||||
|
if (message.type === "hello2") {
|
||||||
|
if (!this.isBootstrapPeer) {
|
||||||
|
this.bootstrapPeerID = message.bootstrapPeers[0];
|
||||||
|
}
|
||||||
|
this.onHello2Received(this.bootstrapPeerID);
|
||||||
|
}
|
||||||
|
if (message.type === "peer_message") {
|
||||||
|
let peerConnection = this.peers.get(message.from);
|
||||||
|
if (message.message.type === "rtc_description") {
|
||||||
|
// let existingConnection = this.peers.get(message.from);
|
||||||
|
// // We're already connected, so delete the existing connection and make a new one.
|
||||||
|
if (peerConnection?.rtcPeer?.connectionState === "connected") {
|
||||||
|
console.log.apply(null, log("Connecting peer is already connected. Deleting existing peer connection and reconnecting."));
|
||||||
|
peerConnection.disconnect();
|
||||||
|
this.peers.delete(message.from);
|
||||||
|
peerConnection = undefined;
|
||||||
|
}
|
||||||
|
if (!peerConnection) {
|
||||||
|
let remotePeerID = message.from;
|
||||||
|
let newPeer = new PeerConnection(this, remotePeerID, this.websocketSendPeerMessage.bind(this));
|
||||||
|
if (this.isBootstrapPeer) {
|
||||||
|
newPeer.setPolite(false);
|
||||||
|
}
|
||||||
|
peerConnection = newPeer;
|
||||||
|
this.peers.set(newPeer.remotePeerID, newPeer);
|
||||||
|
this.onConnectRequest(newPeer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!peerConnection) {
|
||||||
|
console.log.apply(null, log("Can't find peer for peer message:", message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
peerConnection.onWebsocketMessage(message.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async onConnectRequest(newPeer) {
|
||||||
|
// let remotePeerID = message.from;
|
||||||
|
// let newPeer = new PeerConnection(this, remotePeerID, this.websocketSendPeerMessage.bind(this));
|
||||||
|
// if (this.isBootstrapPeer) {
|
||||||
|
// newPeer.setPolite(false);
|
||||||
|
// }
|
||||||
|
await newPeer.connect();
|
||||||
|
this.onPeerConnected(newPeer.remotePeerID);
|
||||||
|
return newPeer;
|
||||||
|
}
|
||||||
|
async onHello2Received(bootstrapPeerID) {
|
||||||
|
if (this.isBootstrapPeer) {
|
||||||
|
this.connectPromiseCallbacks?.resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!bootstrapPeerID) {
|
||||||
|
console.log.apply(null, log("Didn't get bootstrap peer, waiting 10 seconds..."));
|
||||||
|
// let callSendHello2OnTimeout = () => { console.log(this, "jajajajaj");this.sendHello2() };
|
||||||
|
// setTimeout(callSendHello2OnTimeout, 5_000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.bootstrapPeerConnection = await this.connectToPeer(bootstrapPeerID);
|
||||||
|
this.connectPromiseCallbacks?.resolve();
|
||||||
|
}
|
||||||
|
sendHello2() {
|
||||||
|
this.websocketSend({
|
||||||
|
type: "hello2",
|
||||||
|
user_id: this.userID,
|
||||||
|
// user_name: app.username,
|
||||||
|
peer_id: this.peerID,
|
||||||
|
session_id: this.sessionID,
|
||||||
|
// peer_name: app.peername,
|
||||||
|
is_bootstrap_peer: this.isBootstrapPeer,
|
||||||
|
// peer_description: this.rtcPeerDescription
|
||||||
|
});
|
||||||
|
}
|
||||||
|
websocketSendPeerMessage(remotePeerID, peerMessage) {
|
||||||
|
this.websocketSend({
|
||||||
|
type: "peer_message",
|
||||||
|
from: this.peerID,
|
||||||
|
to: remotePeerID,
|
||||||
|
from_username: "blah user",
|
||||||
|
from_peername: "blah peer",
|
||||||
|
message: peerMessage
|
||||||
|
});
|
||||||
|
// let responseMessage = { type: "peer_message",
|
||||||
|
// from: app.peerID,
|
||||||
|
// to: data.from,
|
||||||
|
// from_username: app.username,
|
||||||
|
// from_peername: app.peername,
|
||||||
|
// message: { type: "get_posts_for_user", post_ids: postIds, user_id: message.user_id } }
|
||||||
|
}
|
||||||
|
constructor(userID, peerID, isBootstrapPeer) {
|
||||||
|
// private signaler: Signaler;
|
||||||
|
this.searchQueryFunctions = new Map();
|
||||||
|
this.RPC_remote = new Map();
|
||||||
|
this.rpc = {};
|
||||||
|
this.isBootstrapPeer = false;
|
||||||
|
this.bootstrapPeerConnection = null;
|
||||||
|
this.sessionID = generateID();
|
||||||
|
this.websocket = null;
|
||||||
|
this.bootstrapPeerID = null;
|
||||||
|
this.connectPromiseCallbacks = null;
|
||||||
|
this.connectPromise = null;
|
||||||
|
this.pingPeers = [];
|
||||||
|
this.watchdogPeriodSeconds = 10;
|
||||||
|
this.eventListeners = new Map();
|
||||||
|
this.reconnectPeriod = 10;
|
||||||
|
this.messageSuperlog = false;
|
||||||
|
this.watchdogInterval = 0;
|
||||||
|
this.reconnectTimer = null;
|
||||||
|
this.peerStateSuperlog = true;
|
||||||
|
// async watchdog() {
|
||||||
|
// // Check that we're connected to at least N peers. If not, reconnect to the bootstrap server.
|
||||||
|
// if (this.peers.size === 0) {
|
||||||
|
// await this.sendHello2();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
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.peers = new Map();
|
||||||
|
this.routingTable = new Map();
|
||||||
|
this.userID = userID;
|
||||||
|
this.peerID = peerID;
|
||||||
|
}
|
||||||
|
disconnect() {
|
||||||
|
this.websocket?.close();
|
||||||
|
for (let peer of this.peers.values()) {
|
||||||
|
peer.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
connectWebSocket() {
|
||||||
|
try {
|
||||||
|
let hostname = globalThis?.location?.hostname ?? 'ddln.app';
|
||||||
|
let port = globalThis?.location?.port ?? '443';
|
||||||
|
let wsURL = `wss://${hostname}:${port}/ws`;
|
||||||
|
console.log.apply(null, log(`Attempting to connect websocket to URL: ${wsURL}`));
|
||||||
|
this.websocket = new WebSocket(wsURL);
|
||||||
|
// this.websocket.onclose = (e: CloseEvent) => {
|
||||||
|
// let closedUnexpectedly = !e.wasClean;
|
||||||
|
// if (closedUnexpectedly) {
|
||||||
|
// console.log.apply(null, log(`Websocket closed unexpectedly. Will try to reconnect in ${this.reconnectPeriod} seconds`));
|
||||||
|
// // let alreadyReconnecting = this.reconnectTimer !== null;
|
||||||
|
// // if (!alreadyReconnecting) {
|
||||||
|
// this.reconnectTimer = globalThis.setTimeout(() => {
|
||||||
|
// console.log.apply(null, log(`Reconnecting web socket`));
|
||||||
|
// this.reconnectTimer = null;
|
||||||
|
// this.connectWebSocket();
|
||||||
|
// }, this.reconnectPeriod * 1000)
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
this.websocket.onopen = async (event) => {
|
||||||
|
console.log.apply(null, log("peermanager:ws:onopen"));
|
||||||
|
this.sendHello2();
|
||||||
|
};
|
||||||
|
this.websocket.onmessage = this.onWebsocketMessage.bind(this);
|
||||||
|
}
|
||||||
|
connect() {
|
||||||
|
// setInterval(this.watchdog.bind(this), this.watchdogPeriodSeconds * 1000);
|
||||||
|
// Side effects :(
|
||||||
|
if (!this.watchdogInterval) {
|
||||||
|
this.watchdogInterval = setInterval(() => {
|
||||||
|
let numActive = 0;
|
||||||
|
for (let [id, peer] of this.peers) {
|
||||||
|
if ( /*id === this.bootstrapPeerID ||*/peer.rtcPeer?.connectionState === "new" ||
|
||||||
|
peer.rtcPeer?.connectionState === "connecting") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
numActive++;
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
this.sendHello2();
|
||||||
|
}
|
||||||
|
if (this.websocket?.readyState === WebSocket.CLOSED) {
|
||||||
|
this.connectWebSocket();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)}${(peerID === this.bootstrapPeerID) ? "[Bootstrap]" : ""}` + "\n";
|
||||||
|
}
|
||||||
|
output += `numActivePeers: ${numActive}` + "\n";
|
||||||
|
console.log.apply(null, log(output));
|
||||||
|
}, this.reconnectPeriod * 1000);
|
||||||
|
}
|
||||||
|
let connectPromise = this.connectPromise;
|
||||||
|
if (!connectPromise) {
|
||||||
|
connectPromise = new Promise((resolve, reject) => {
|
||||||
|
this.connectPromiseCallbacks = { resolve, reject };
|
||||||
|
});
|
||||||
|
this.connectPromise = connectPromise;
|
||||||
|
}
|
||||||
|
this.connectWebSocket();
|
||||||
|
return connectPromise;
|
||||||
|
// this.signaler = new Signaler(userID, peerID, isBootstrapPeer, this.onConnected.bind(this));
|
||||||
|
// Testing
|
||||||
|
// let dummyPeer = new PeerConnection(this, "dummy_peer", this.websocketSendPeerMessage.bind(this));
|
||||||
|
// this.peers.set("dummy_peer", dummyPeer);
|
||||||
|
}
|
||||||
|
async connectToPeer(remotePeerID) {
|
||||||
|
// Connect to the peer that has the peer id remotePeerID.
|
||||||
|
// TODO how do multiple windows / tabs from the same peer and user work?
|
||||||
|
// Need to decide if they should all get a unique connection. A peer should only be requesting and writing
|
||||||
|
// Data once though, so it probably need to be solved on the client side as the data is shared obv
|
||||||
|
// Maybe use BroadcastChannel to proxy all calls to peermanager? That will probably really complicate things.
|
||||||
|
// What if we just user session+peerID for the connections? Then we might have two windows making requests
|
||||||
|
// For IDs etc, it would probably be best to proxy everything.
|
||||||
|
// Maybe once we put this logic in a web worker, we'll need an interface to it that works over postMessage
|
||||||
|
// anyway, and at that point, we could just use that same interface over a broadcastChannel
|
||||||
|
// let's keep it simple for now and ignore the problem :)
|
||||||
|
let peerConnection = new PeerConnection(this, remotePeerID, this.websocketSendPeerMessage.bind(this));
|
||||||
|
this.peers.set(remotePeerID, peerConnection);
|
||||||
|
await peerConnection.connect();
|
||||||
|
this.onPeerConnected(remotePeerID);
|
||||||
|
return peerConnection;
|
||||||
|
}
|
||||||
|
onPeerConnected(peerID) {
|
||||||
|
this.peerStateSuperlog && console.log.apply(null, log(`PeerManager: Successfully connected to peer ${peerID}`));
|
||||||
|
this.dispatchEvent(PeerEventTypes.PEER_CONNECTED, { peerID: peerID });
|
||||||
|
}
|
||||||
|
dispatchEvent(event, parameters) {
|
||||||
|
let listeners = this.eventListeners.get(event);
|
||||||
|
if (!listeners) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (let listener of listeners) {
|
||||||
|
listener(parameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addEventListener(eventName, func) {
|
||||||
|
let listeners = this.eventListeners.get(eventName);
|
||||||
|
if (!listeners) {
|
||||||
|
this.eventListeners.set(eventName, [func]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onPeerDisconnected(peerID) {
|
||||||
|
let deleted = this.peers.delete(peerID);
|
||||||
|
if (!deleted) {
|
||||||
|
throw new Error(`Can't find peer that disconnected ${peerID}`);
|
||||||
|
}
|
||||||
|
// TODO: What do we do if we lose connection to the bootstrap peer?
|
||||||
|
// If we have other connections, it probably doesn't matter.
|
||||||
|
// Eventually we want the bootstrap peer to be no different than any other peer anyway.
|
||||||
|
// We should disconnect from the websocket once we connect to our intial peers.
|
||||||
|
// If we have no peer connections, try to connect. If connection fails, start a timer to reconnect.
|
||||||
|
if (peerID === this.bootstrapPeerID) {
|
||||||
|
this.bootstrapPeerID = null;
|
||||||
|
this.bootstrapPeerConnection = null;
|
||||||
|
}
|
||||||
|
this.peerStateSuperlog && console.log.apply(null, log(`PeerManager: disconnected from peer ${peerID}`));
|
||||||
|
this.dispatchEvent(PeerEventTypes.PEER_DISCONNECTED, { peerID: peerID });
|
||||||
|
}
|
||||||
|
async disconnectFromPeer(remotePeerID) {
|
||||||
|
let peer = this.peers.get(remotePeerID);
|
||||||
|
if (!peer) {
|
||||||
|
console.log.apply(null, log(`PeerManager.disconnect: couldn't find peer ${remotePeerID}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log.apply(null, log(`PeerManager.disconnect: disconnecting peer ${remotePeerID}`));
|
||||||
|
await peer.disconnect();
|
||||||
|
this.onPeerDisconnected(remotePeerID);
|
||||||
|
}
|
||||||
|
async call(peerID, functionName, args) {
|
||||||
|
let peer = this.peers.get(peerID);
|
||||||
|
if (!peer) {
|
||||||
|
console.log.apply(null, log(`Can't find peer ${peerID}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let returnValues = await peer.call(functionName, args);
|
||||||
|
return returnValues;
|
||||||
|
}
|
||||||
|
async callFromRemote(functionName, args) {
|
||||||
|
let func = this.RPC_remote.get(functionName);
|
||||||
|
if (!func) {
|
||||||
|
throw new Error(`callFromRemote: got RPC we don't know about: ${functionName}, ${args}`);
|
||||||
|
}
|
||||||
|
let returnValues = await func.apply(null, args);
|
||||||
|
return returnValues;
|
||||||
|
}
|
||||||
|
registerRPC(functionName, func) {
|
||||||
|
this.rpc[functionName] = (peerID, ...args) => {
|
||||||
|
return this.call(peerID, functionName, args);
|
||||||
|
};
|
||||||
|
this.RPC_remote.set(functionName, func);
|
||||||
|
}
|
||||||
|
registerSearchQuery(searchType, queryFunction) {
|
||||||
|
this.searchQueryFunctions.set(searchType, queryFunction);
|
||||||
|
}
|
||||||
|
async search(type, message) {
|
||||||
|
let promises = [];
|
||||||
|
for (let peer of this.peers.values()) {
|
||||||
|
promises.push(peer.call(type, message));
|
||||||
|
}
|
||||||
|
return await Promise.allSettled(promises);
|
||||||
|
}
|
||||||
|
onMessage(remotePeerID, message) {
|
||||||
|
console.log.apply(null, log(remotePeerID, message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class PeerConnection {
|
||||||
|
async RPCHandler(message) {
|
||||||
|
}
|
||||||
|
constructor(peerManager, remotePeerID, sendPeerMessage) {
|
||||||
|
this.dataChannel = null;
|
||||||
|
this.messageHandlers = new Map();
|
||||||
|
this.makingOffer = false;
|
||||||
|
this.ignoreOffer = false;
|
||||||
|
this.isSettingRemoteAnswerPending = false;
|
||||||
|
this.polite = true;
|
||||||
|
this.webRTCSuperlog = true;
|
||||||
|
this.dataChannelSuperlog = false;
|
||||||
|
this.chunkSize = (16 * 1024) - 100;
|
||||||
|
this.messageSuperlog = false;
|
||||||
|
this.rpcSuperlog = false;
|
||||||
|
this.pendingRPCs = new Map();
|
||||||
|
this.connectionPromise = null;
|
||||||
|
// private makingOffer:boolean = false;
|
||||||
|
// private ignoreOffer:boolean = false;
|
||||||
|
this.rtcPeer = null;
|
||||||
|
// longMessageQueue: string[] = [];
|
||||||
|
this.longMessages = new Map();
|
||||||
|
this.chunkSuperlog = false;
|
||||||
|
this.sendPeerMessage = sendPeerMessage;
|
||||||
|
this.peerManager = peerManager;
|
||||||
|
this.remotePeerID = remotePeerID;
|
||||||
|
// this.signaler = signaler;
|
||||||
|
// this.signaler.route(remotePeerID, this);
|
||||||
|
}
|
||||||
|
setPolite(polite) {
|
||||||
|
this.polite = polite;
|
||||||
|
}
|
||||||
|
setupDataChannel() {
|
||||||
|
if (!this.dataChannel) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
this.dataChannel.onopen = (e) => {
|
||||||
|
if (!this.dataChannel) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
this.dataChannelSuperlog && console.log.apply(null, log("data channel is open to: ", this.remotePeerID, " from: ", this.peerManager.peerID));
|
||||||
|
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) {
|
||||||
|
this.send({ type: 'initial_peers', from: this.peerManager.peerID, peers: [...this.peerManager.peers.keys()].filter(entry => entry !== this.remotePeerID) });
|
||||||
|
// this.dataChannel.send(JSON.stringify());
|
||||||
|
}
|
||||||
|
this.connectionPromise?.resolve(this.remotePeerID);
|
||||||
|
//globalThis.setTimeout(()=>this.connectionPromise?.resolve(this.remotePeerID), 5000);
|
||||||
|
};
|
||||||
|
this.dataChannel.onmessage = (e) => {
|
||||||
|
this.messageSuperlog && console.log.apply(null, log(`[${logID(this.remotePeerID)}]->datachannel[${logID(this.peerManager.peerID)}]: `, e.data));
|
||||||
|
this.onMessage(e.data);
|
||||||
|
};
|
||||||
|
this.dataChannel.onclose = (e) => {
|
||||||
|
this.dataChannelSuperlog && console.log.apply(null, log(`datachannel from peer ${this.remotePeerID} closed, disconnecting peer.`));
|
||||||
|
this.peerManager.disconnectFromPeer(this.remotePeerID);
|
||||||
|
};
|
||||||
|
this.dataChannel.onerror = (e) => {
|
||||||
|
this.dataChannelSuperlog && console.log.apply(null, log(`datachannel from peer ${this.remotePeerID} error:`, e.error));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async connect() {
|
||||||
|
let connectionPromise = new Promise((resolve, reject) => { this.connectionPromise = { resolve, reject }; });
|
||||||
|
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}`));
|
||||||
|
if (!this.rtcPeer) {
|
||||||
|
throw new Error("onconnectionstatechange");
|
||||||
|
}
|
||||||
|
// When the connection is closed, tell the peer manager that this connection has gone away
|
||||||
|
if (this.rtcPeer.connectionState === "failed") {
|
||||||
|
this.peerManager.onPeerDisconnected(this.remotePeerID);
|
||||||
|
// globalThis.setTimeout(async () => { await this.peerManager.connectToPeer(this.remotePeerID) }, 10_000);
|
||||||
|
}
|
||||||
|
if (this.rtcPeer.connectionState === "connected") {
|
||||||
|
// Check the selected candidates
|
||||||
|
const stats = await this.rtcPeer.getStats();
|
||||||
|
let localIP = '';
|
||||||
|
let remoteIP = '';
|
||||||
|
for (const report of stats.values()) {
|
||||||
|
if (report.type === 'transport') {
|
||||||
|
let candidatePair = stats.get(report.selectedCandidatePairId);
|
||||||
|
let localCandidate = stats.get(candidatePair.localCandidateId);
|
||||||
|
let remoteCandidate = stats.get(candidatePair.remoteCandidateId);
|
||||||
|
this.webRTCSuperlog && console.log.apply(null, log("Connected candidates\n", localCandidate, remoteCandidate));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.rtcPeer.ondatachannel = (e) => {
|
||||||
|
let dataChannel = e.channel;
|
||||||
|
this.dataChannel = dataChannel;
|
||||||
|
this.setupDataChannel();
|
||||||
|
};
|
||||||
|
if (this.polite) {
|
||||||
|
this.dataChannel = this.rtcPeer.createDataChannel("ddln_main");
|
||||||
|
this.setupDataChannel();
|
||||||
|
}
|
||||||
|
if (this.rtcPeer === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// this.rtcPeer.onicecandidate = ({ candidate }) => this.signaler.send(JSON.stringify({ candidate }));
|
||||||
|
// this.rtcPeer.onicecandidate = ({ candidate }) => console.log.apply(null, log(candidate);
|
||||||
|
this.rtcPeer.onicegatheringstatechange = (event) => {
|
||||||
|
this.webRTCSuperlog && console.log.apply(null, log("onicegatheringstatechange:", this.rtcPeer?.iceGatheringState));
|
||||||
|
};
|
||||||
|
this.rtcPeer.oniceconnectionstatechange = (event) => {
|
||||||
|
this.webRTCSuperlog && console.log.apply(null, log("oniceconnectionstatechange:", this.rtcPeer?.iceConnectionState));
|
||||||
|
};
|
||||||
|
this.rtcPeer.onicecandidateerror = (event) => {
|
||||||
|
this.webRTCSuperlog && console.log.apply(null, log(`onicecandidateerror: ${event.errorCode} ${event.errorText} ${event.address} ${event.url}`));
|
||||||
|
};
|
||||||
|
this.rtcPeer.onicecandidate = ({ candidate }) => {
|
||||||
|
this.webRTCSuperlog && console.log.apply(null, log(`onicecandidate`, candidate));
|
||||||
|
this.sendPeerMessage(this.remotePeerID, { type: "rtc_candidate", candidate: candidate });
|
||||||
|
};
|
||||||
|
this.rtcPeer.onnegotiationneeded = async (event) => {
|
||||||
|
this.webRTCSuperlog && console.log.apply(null, log("on negotiation needed fired"));
|
||||||
|
if (!this.rtcPeer) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.makingOffer = true;
|
||||||
|
await this.rtcPeer.setLocalDescription();
|
||||||
|
if (!this.rtcPeer.localDescription) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.sendPeerMessage(this.remotePeerID, { type: "rtc_description", description: this.rtcPeer.localDescription });
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
this.makingOffer = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return connectionPromise;
|
||||||
|
}
|
||||||
|
async onWebsocketMessage(message) {
|
||||||
|
if (message.type == "rtc_connect") {
|
||||||
|
this.rtcPeer?.setRemoteDescription(message.description);
|
||||||
|
}
|
||||||
|
// /*
|
||||||
|
// let ignoreOffer = false;
|
||||||
|
// let isSettingRemoteAnswerPending = false;
|
||||||
|
// signaler.onmessage = async ({ data: { description, candidate } }) => {
|
||||||
|
if (!this.rtcPeer) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
let description = null;
|
||||||
|
if (message.type == "rtc_description") {
|
||||||
|
description = message.description;
|
||||||
|
}
|
||||||
|
let candidate = null;
|
||||||
|
if (message.type == "rtc_candidate") {
|
||||||
|
candidate = message.candidate;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (description) {
|
||||||
|
const readyForOffer = !this.makingOffer &&
|
||||||
|
(this.rtcPeer.signalingState === "stable" || this.isSettingRemoteAnswerPending);
|
||||||
|
const offerCollision = description.type === "offer" && !readyForOffer;
|
||||||
|
this.ignoreOffer = !this.polite && offerCollision;
|
||||||
|
if (this.ignoreOffer) {
|
||||||
|
console.warn(">>>>>>>>>>>>>>>>>IGNORING OFFER");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.isSettingRemoteAnswerPending = description.type == "answer";
|
||||||
|
await this.rtcPeer.setRemoteDescription(description);
|
||||||
|
this.isSettingRemoteAnswerPending = false;
|
||||||
|
if (description.type === "offer") {
|
||||||
|
await this.rtcPeer.setLocalDescription();
|
||||||
|
this.sendPeerMessage(this.remotePeerID, { type: "rtc_description", description: this.rtcPeer.localDescription });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (candidate) {
|
||||||
|
try {
|
||||||
|
await this.rtcPeer.addIceCandidate(candidate);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
if (!this.ignoreOffer) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
// };
|
||||||
|
// */
|
||||||
|
}
|
||||||
|
disconnect() {
|
||||||
|
this.rtcPeer?.close();
|
||||||
|
this.rtcPeer = null;
|
||||||
|
}
|
||||||
|
async send(message) {
|
||||||
|
if (!this.dataChannel) {
|
||||||
|
throw new Error("Send called but datachannel is null");
|
||||||
|
}
|
||||||
|
while (this.dataChannel.bufferedAmount >= 8 * 1024 * 1024) {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
setTimeout(() => resolve(), 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let messageJSON = JSON.stringify(message);
|
||||||
|
this.messageSuperlog && console.log.apply(null, log(`[${logID(this.remotePeerID)}]<-datachannel[${logID(this.peerManager.peerID)}]:`, message.type, message, `message size:${messageJSON.length}`));
|
||||||
|
if (messageJSON.length > this.chunkSize) {
|
||||||
|
this.messageSuperlog && console.log.apply(null, log(`[datachannel] sending long message: `, messageJSON.length));
|
||||||
|
this.sendLongMessage(messageJSON);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.dataChannel?.send(messageJSON);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log.apply(null, log(e));
|
||||||
|
}
|
||||||
|
// this.onMessage(messageJSON);
|
||||||
|
}
|
||||||
|
// Get a polyfill for browsers that don't have this API
|
||||||
|
async hashMessage(message) {
|
||||||
|
let msgUint8 = new TextEncoder().encode(message);
|
||||||
|
const hashBuffer = await crypto.subtle.digest("SHA-256", msgUint8);
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join('');
|
||||||
|
return hashHex;
|
||||||
|
}
|
||||||
|
async sendLongMessage(message) {
|
||||||
|
// message = JSON.parse(message);
|
||||||
|
let chunkSize = this.chunkSize / 2;
|
||||||
|
// let chunkSize = 1024;
|
||||||
|
let chunks = Math.ceil(message.length / chunkSize);
|
||||||
|
let messageID = generateID();
|
||||||
|
let hash = await this.hashMessage(message);
|
||||||
|
for (let i = 0; i < chunks; i++) {
|
||||||
|
let offset = i * chunkSize;
|
||||||
|
let chunk = message?.substring(offset, offset + chunkSize);
|
||||||
|
// this.send(message?.substring(offset, offset + chunkSize-1));
|
||||||
|
// console.log("[chunk]", chunk);
|
||||||
|
let chunkHash = await this.hashMessage(chunk);
|
||||||
|
this.chunkSuperlog && console.log.apply(null, log(`[chunk] chunkHash:${logID(chunkHash)} from:${logID(this.peerManager.peerID)} to:${logID(this.remotePeerID)} messageID:${logID(messageID)} hash:${logID(hash)} ${i + 1}/${chunks}`));
|
||||||
|
let netMessage = { type: 'chunk', message_id: messageID, hash: hash, chunk_index: i, total_chunks: chunks, chunk: chunk, chunk_hash: chunkHash };
|
||||||
|
await this.send(netMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
call(functionName, args) {
|
||||||
|
let transactionID = generateID(); // make this faster as we will only ever have a small number of in-flight queries on a peer
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
|
let message = {
|
||||||
|
type: "rpc_call",
|
||||||
|
transaction_id: transactionID,
|
||||||
|
function_name: functionName,
|
||||||
|
args: args,
|
||||||
|
};
|
||||||
|
this.rpcSuperlog && console.log.apply(null, log(`[${logID(this.remotePeerID)}]<-[rpc][${logID(this.peerManager.peerID)}]`, message.function_name, message.transaction_id, JSON.stringify(message.args, null, 2)));
|
||||||
|
this.send(message);
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
async onMessage(messageJSON) {
|
||||||
|
let message = {};
|
||||||
|
try {
|
||||||
|
message = JSON.parse(messageJSON);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log.apply(null, log("PeerConnection.onMessage:", e));
|
||||||
|
}
|
||||||
|
this.messageSuperlog && console.log.apply(null, log(`[${logID(this.remotePeerID)}]->datachannel[${logID(this.peerManager.peerID)}]`, message.type, message));
|
||||||
|
let type = message.type;
|
||||||
|
if (type === "rpc_response") {
|
||||||
|
this.rpcSuperlog && console.log.apply(null, log(`[${logID(this.remotePeerID)}]<-[rpc][${logID(this.peerManager.peerID)}] response: `, message.function_name, message.transaction_id, JSON.stringify(message.args, null, 2)));
|
||||||
|
let pendingRPC = this.pendingRPCs.get(message.transaction_id);
|
||||||
|
if (!pendingRPC) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
pendingRPC.resolve(message.response);
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
if (type === "initial_peers") {
|
||||||
|
for (let peerID of message.peers) {
|
||||||
|
console.log.apply(null, log("Connecting to initial peer ", peerID));
|
||||||
|
this.peerManager.connectToPeer(peerID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (type === "chunk") {
|
||||||
|
let messageID = message.message_id;
|
||||||
|
if (!this.longMessages.has(messageID)) {
|
||||||
|
this.longMessages.set(messageID, { messageChunks: [], totalChunks: message.total_chunks, hash: message.hash });
|
||||||
|
}
|
||||||
|
let longMessage = this.longMessages.get(messageID);
|
||||||
|
if (!longMessage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let chunkHash = await this.hashMessage(message.chunk_hash);
|
||||||
|
longMessage.messageChunks.push(message.chunk);
|
||||||
|
this.chunkSuperlog && console.log.apply(null, log(`[chunk] chunked message sent chunkHash:${logID(message.chunk_hash)} computed hash: ${logID(chunkHash)} messageId:${logID(messageID)} chunk ${message.chunk_index + 1}/${longMessage.totalChunks}`));
|
||||||
|
if (message.chunk_index === longMessage.totalChunks - 1) {
|
||||||
|
let completeMessage = longMessage.messageChunks.join('');
|
||||||
|
let hash = await this.hashMessage(completeMessage);
|
||||||
|
this.chunkSuperlog && console.log.apply(null, log(`[chunk] hashes match: ${hash === longMessage.hash} sent hash: ${logID(longMessage.hash)} computed hash: ${logID(hash)}`));
|
||||||
|
if (hash !== longMessage.hash) {
|
||||||
|
throw new Error("[chunk] long message hashes don't match.");
|
||||||
|
}
|
||||||
|
this.onMessage(completeMessage);
|
||||||
|
this.longMessages.delete(messageID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// this.peerManger.onMessage(this.remotePeerID, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PeerConnection.config = {
|
||||||
|
iceServers: [
|
||||||
|
{ urls: "stun:ddln.app" },
|
||||||
|
// { urls: "turn:ddln.app", username: "a", credential: "b" },
|
||||||
|
{ urls: "stun:stun.l.google.com" }, // keeping this for now as my STUN server is not returning ipv6
|
||||||
|
// { urls: "stun:stun1.l.google.com" },
|
||||||
|
// { urls: "stun:stun2.l.google.com" },
|
||||||
|
// { urls: "stun:stun3.l.google.com" },
|
||||||
|
// { urls: "stun:stun4.l.google.com" },
|
||||||
|
],
|
||||||
|
};
|
||||||
226
static/Sync.js
Normal file
226
static/Sync.js
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
import { mergeDataArray, checkPostIds, getAllIds, getPostsByIds } from "db";
|
||||||
|
import { log, logID } from "log";
|
||||||
|
async function bytesToBase64DataUrl(bytes, type = "application/octet-stream") {
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
const reader = Object.assign(new FileReader(), {
|
||||||
|
onload: () => resolve(reader.result),
|
||||||
|
onerror: () => reject(reader.error),
|
||||||
|
});
|
||||||
|
reader.readAsDataURL(new File([bytes], "", { type }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function arrayBufferToBase64(buffer) {
|
||||||
|
var bytes = new Uint8Array(buffer);
|
||||||
|
return (await bytesToBase64DataUrl(bytes)).replace("data:application/octet-stream;base64,", "");
|
||||||
|
}
|
||||||
|
async function base64ToArrayBuffer(base64String) {
|
||||||
|
let response;
|
||||||
|
try {
|
||||||
|
response = await fetch("data:application/octet-stream;base64," + base64String);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log("error", e, base64String);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let arrayBuffer = await response.arrayBuffer();
|
||||||
|
return arrayBuffer;
|
||||||
|
}
|
||||||
|
export class Sync {
|
||||||
|
constructor() {
|
||||||
|
this.isArchivePeer = false;
|
||||||
|
this.userID = "";
|
||||||
|
this.userPeers = new Map();
|
||||||
|
this.userIDsToSync = new Set();
|
||||||
|
this.syncSuperlog = false;
|
||||||
|
this.userBlockList = new Set([
|
||||||
|
'5d63f0b2-a842-41bf-bf06-e0e4f6369271',
|
||||||
|
'5f1b85c4-b14c-454c-8df1-2cacc93f8a77',
|
||||||
|
// 'bba3ad24-9181-4e22-90c8-c265c80873ea'
|
||||||
|
]);
|
||||||
|
this.postBlockList = new Set([
|
||||||
|
'1c71f53c-c467-48e4-bc8c-39005b37c0d5',
|
||||||
|
'64203497-f77b-40d6-9e76-34d17372e72a',
|
||||||
|
'243130d8-4a41-471e-8898-5075f1bd7aec',
|
||||||
|
'e01eff89-5100-4b35-af4c-1c1bcb007dd0',
|
||||||
|
'194696a2-d850-4bb0-98f7-47416b3d1662',
|
||||||
|
'f6b21eb1-a0ff-435b-8efc-6a3dd70c0dca',
|
||||||
|
'dd1d92aa-aa24-4166-a925-94ba072a9048'
|
||||||
|
]);
|
||||||
|
// async getPostIdsForUserHandler(data: any) {
|
||||||
|
// let message = data.message;
|
||||||
|
// let postIds = await getAllIds(message.user_id) ?? [];
|
||||||
|
// postIds = postIds.filter((postID: string) => !this.postBlockList.has(postID));
|
||||||
|
// if (postIds.length === 0) {
|
||||||
|
// console.log.apply(null, log(`Net: I know about user ${logID(message.user_id)} but I have 0 posts, so I'm not sending any to to peer ${logID(data.from)}`));;
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// console.log.apply(null, log(`Net: Sending ${postIds.length} post Ids for user ${logID(message.user_id)} to peer ${logID(data.from)}`));
|
||||||
|
// let responseMessage = { type: "peer_message", from: app.peerID, to: data.from, from_username: app.username, from_peername: app.peername, message: { type: "get_post_ids_for_user_response", post_ids: postIds, user_id: message.user_id } }
|
||||||
|
// this.send(responseMessage);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
setArchive(isArchive) {
|
||||||
|
this.isArchivePeer = isArchive;
|
||||||
|
}
|
||||||
|
setUserID(userID) {
|
||||||
|
this.userID = userID;
|
||||||
|
this.userIDsToSync = new Set(this.getFollowing(userID));
|
||||||
|
}
|
||||||
|
shouldSyncUserID(userID) {
|
||||||
|
let shouldSyncAllUsers = this.isArchivePeer;
|
||||||
|
if (shouldSyncAllUsers) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return this.userIDsToSync.has(userID);
|
||||||
|
}
|
||||||
|
getPeersForUser(userID) {
|
||||||
|
let peers = this.userPeers.get(userID);
|
||||||
|
if (!peers) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [...peers.keys()];
|
||||||
|
}
|
||||||
|
addUserPeer(userID, peerID) {
|
||||||
|
this.syncSuperlog && console.log.apply(null, log(`[sync] addUserPeer user:${logID(userID)} peer:${logID(peerID)}`));
|
||||||
|
;
|
||||||
|
if (!this.userPeers.has(userID)) {
|
||||||
|
this.userPeers.set(userID, new Set());
|
||||||
|
}
|
||||||
|
let peers = this.userPeers.get(userID);
|
||||||
|
peers.add(peerID);
|
||||||
|
// this.syncSuperlog && console.log.apply(null, log(this.userPeers));
|
||||||
|
}
|
||||||
|
deleteUserPeer(peerIDToDelete) {
|
||||||
|
for (const peers of this.userPeers.values()) {
|
||||||
|
for (const peerID of peers) {
|
||||||
|
if (peerID === peerIDToDelete) {
|
||||||
|
peers.delete(peerIDToDelete);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// shouldSyncUserID(userID: string) {
|
||||||
|
// if (app.isHeadless) {
|
||||||
|
// return true;
|
||||||
|
// }
|
||||||
|
// return this.UserIDsTothis.has(userID);
|
||||||
|
// }
|
||||||
|
async getKnownUsers() {
|
||||||
|
let knownUsers = [...(await indexedDB.databases())].map(db => db.name?.replace('user_', '')).filter(userID => userID !== undefined);
|
||||||
|
knownUsers = knownUsers
|
||||||
|
.filter(userID => this.shouldSyncUserID(userID))
|
||||||
|
.filter(userID => !this.userBlockList.has(userID))
|
||||||
|
.filter(async (userID) => (await getAllIds(userID)).length > 0); // TODO:EASYOPT getting all the IDs is unecessary, replace it with a test to get a single ID.
|
||||||
|
return knownUsers;
|
||||||
|
}
|
||||||
|
getFollowing(userID) {
|
||||||
|
let following = ['a0e42390-08b5-4b07-bc2b-787f8e5f1297']; // Follow BMO by default :)
|
||||||
|
following.push(this.userID);
|
||||||
|
// Hazel
|
||||||
|
if (userID == '622ecc28-2eff-44b9-b89d-fdea7c8dd2d5') {
|
||||||
|
following.push(...[
|
||||||
|
'8f6802be-c3b6-46c1-969c-5f90cbe01479', // Fiona
|
||||||
|
'622ecc28-2eff-44b9-b89d-fdea7c8dd2d5', // Hazel
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
// Rob
|
||||||
|
if (userID === 'b38b623c-c3fa-4351-9cab-50233c99fa4e') {
|
||||||
|
following.push(...[
|
||||||
|
'6d774268-16cd-4e86-8bbe-847a0328893d', // Sean
|
||||||
|
'05a495a0-0dd8-4186-94c3-b8309ba6fc4c', // Martin
|
||||||
|
'bba3ad24-9181-4e22-90c8-c265c80873ea', // Harry
|
||||||
|
'8f6802be-c3b6-46c1-969c-5f90cbe01479', // Fiona
|
||||||
|
'622ecc28-2eff-44b9-b89d-fdea7c8dd2d5', // Hazel
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
// Martin
|
||||||
|
if (userID === '05a495a0-0dd8-4186-94c3-b8309ba6fc4c') {
|
||||||
|
following.push(...[
|
||||||
|
'b38b623c-c3fa-4351-9cab-50233c99fa4e', // Rob
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
// Fiona
|
||||||
|
if (userID === '8f6802be-c3b6-46c1-969c-5f90cbe01479') {
|
||||||
|
following.push(...[
|
||||||
|
'b38b623c-c3fa-4351-9cab-50233c99fa4e', // Rob
|
||||||
|
'05a495a0-0dd8-4186-94c3-b8309ba6fc4c', // Martin
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return following;
|
||||||
|
}
|
||||||
|
async getPostIdsForUser(userID) {
|
||||||
|
let postIds = await getAllIds(userID) ?? [];
|
||||||
|
postIds = postIds.filter((postID) => !this.postBlockList.has(postID));
|
||||||
|
if (postIds.length === 0) {
|
||||||
|
console.log.apply(null, log(`Net: I know about user ${logID(userID)} but I have 0 posts`));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return postIds;
|
||||||
|
}
|
||||||
|
async checkPostIds(userID, peerID, postIDs) {
|
||||||
|
let startTime = performance.now();
|
||||||
|
let neededPostIds = await checkPostIds(userID, postIDs);
|
||||||
|
this.syncSuperlog && console.log.apply(null, log(`[sync] ID Check for user ${logID(userID)} with IDs from peer[${logID(peerID)}] took ${(performance.now() - startTime).toFixed(2)}ms`));
|
||||||
|
if (neededPostIds.length > 0) {
|
||||||
|
this.syncSuperlog && console.log.apply(null, log(`[sync] Need posts (${neededPostIds.length}) for user[${logID(userID)}] from peer[${logID(peerID)}]`));
|
||||||
|
;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.syncSuperlog && console.log.apply(null, log(`[sync] Don't need any posts for user[${logID(userID)}] from peer[${logID(peerID)}]`));
|
||||||
|
;
|
||||||
|
}
|
||||||
|
// if (postIds.length === 0) {
|
||||||
|
// return [];
|
||||||
|
// }
|
||||||
|
return neededPostIds;
|
||||||
|
}
|
||||||
|
async getPostsForUser(userID, postIDs) {
|
||||||
|
let posts = await getPostsByIds(userID, postIDs) ?? [];
|
||||||
|
console.log.apply(null, log(`[sync] got ${posts.length} posts for user ${logID(userID)}`));
|
||||||
|
;
|
||||||
|
// app.timerStart();
|
||||||
|
let output = [];
|
||||||
|
console.log.apply(null, log("Serializing images"));
|
||||||
|
for (let post of posts) {
|
||||||
|
let newPost = post.data;
|
||||||
|
if (newPost.image_data) {
|
||||||
|
// let compressedData = await wsConnection.compressArrayBuffer(newPost.image_data);
|
||||||
|
// console.log.apply(null, log((newPost.image_data.byteLength - compressedData.byteLength) / 1024 / 1024);
|
||||||
|
// TODO don't do this, use Blobs direclty!
|
||||||
|
// https://developer.chrome.com/blog/blob-support-for-Indexeddb-landed-on-chrome-dev
|
||||||
|
newPost.image_data = await arrayBufferToBase64(newPost.image_data);
|
||||||
|
}
|
||||||
|
// let megs = JSON.stringify(newPost).length/1024/1024;
|
||||||
|
// console.log.apply(null, log(`getPostsForUserHandler id:${newPost.post_id} post length:${megs}`);
|
||||||
|
output.push(newPost);
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
// console.log.apply(null, log(`getPostsForUser`,output));
|
||||||
|
}
|
||||||
|
async writePostForUser(userID, post) {
|
||||||
|
// HACK: Some posts have insanely large images, so I'm gonna skip them.
|
||||||
|
// Once we support delete then we we could delete these posts in a sensible way.
|
||||||
|
if (this.postBlockList.has(post.post_id)) {
|
||||||
|
console.log.apply(null, log(`Skipping blocked post: ${post.post_id}`));
|
||||||
|
;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// HACK - some posts had the wrong author ID
|
||||||
|
if (userID === this.userID) {
|
||||||
|
post.author_id = this.userID;
|
||||||
|
}
|
||||||
|
post.post_timestamp = new Date(post.post_timestamp);
|
||||||
|
if (post.image_data) {
|
||||||
|
let imageDataArrayBuffer = await base64ToArrayBuffer(post.image_data);
|
||||||
|
if (imageDataArrayBuffer === null) {
|
||||||
|
this.syncSuperlog && console.log(`[sync] Failed to create arraybuffer for image for post userID:${userID} postID:${post.post_id} `);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
post.image_data = imageDataArrayBuffer;
|
||||||
|
// skip posts with images for now.
|
||||||
|
// return;
|
||||||
|
}
|
||||||
|
console.log.apply(null, log(`Merging same user peer posts...`));
|
||||||
|
await mergeDataArray(userID, [post]);
|
||||||
|
}
|
||||||
|
}
|
||||||
1643
static/bootstrap_main.js
vendored
Normal file
1643
static/bootstrap_main.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
71
static/dataUtils.js
Normal file
71
static/dataUtils.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
export async function bytesToBase64DataUrl(bytes, type = "application/octet-stream") {
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
const reader = Object.assign(new FileReader(), {
|
||||||
|
onload: () => resolve(reader.result),
|
||||||
|
onerror: () => reject(reader.error),
|
||||||
|
});
|
||||||
|
reader.readAsDataURL(new File([bytes], "", { type }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export async function arrayBufferToBase64(buffer) {
|
||||||
|
var bytes = new Uint8Array(buffer);
|
||||||
|
return (await bytesToBase64DataUrl(bytes)).replace("data:application/octet-stream;base64,", "");
|
||||||
|
}
|
||||||
|
// async function base64ToArrayBuffer(base64String: string) {
|
||||||
|
// let response = await fetch("data:application/octet-stream;base64," + base64String);
|
||||||
|
// let arrayBuffer = await response.arrayBuffer();
|
||||||
|
// return arrayBuffer;
|
||||||
|
// }
|
||||||
|
export async function compressString(input) {
|
||||||
|
// Convert the string to a Uint8Array
|
||||||
|
const textEncoder = new TextEncoder();
|
||||||
|
const inputArray = textEncoder.encode(input);
|
||||||
|
// Create a CompressionStream
|
||||||
|
const compressionStream = new CompressionStream('gzip');
|
||||||
|
const writer = compressionStream.writable.getWriter();
|
||||||
|
// Write the data and close the stream
|
||||||
|
writer.write(inputArray);
|
||||||
|
writer.close();
|
||||||
|
// Read the compressed data from the stream
|
||||||
|
const compressedArray = await new Response(compressionStream.readable).arrayBuffer();
|
||||||
|
// Convert the compressed data to a Uint8Array
|
||||||
|
return new Uint8Array(compressedArray);
|
||||||
|
}
|
||||||
|
// Base58 character set
|
||||||
|
// const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
||||||
|
// Base58 encoding
|
||||||
|
// Base58 encoding
|
||||||
|
// function encodeBase58(buffer: Uint8Array): string {
|
||||||
|
// let carry;
|
||||||
|
// const digits = [0];
|
||||||
|
// for (const byte of buffer) {
|
||||||
|
// carry = byte;
|
||||||
|
// for (let i = 0; i < digits.length; i++) {
|
||||||
|
// carry += digits[i] << 8;
|
||||||
|
// digits[i] = carry % 58;
|
||||||
|
// carry = Math.floor(carry / 58);
|
||||||
|
// }
|
||||||
|
// while (carry > 0) {
|
||||||
|
// digits.push(carry % 58);
|
||||||
|
// carry = Math.floor(carry / 58);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// let result = '';
|
||||||
|
// for (const digit of digits.reverse()) {
|
||||||
|
// result += BASE58_ALPHABET[digit];
|
||||||
|
// }
|
||||||
|
// // Handle leading zero bytes
|
||||||
|
// for (const byte of buffer) {
|
||||||
|
// if (byte === 0x00) {
|
||||||
|
// result = BASE58_ALPHABET[0] + result;
|
||||||
|
// } else {
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return result;
|
||||||
|
// }
|
||||||
|
// Convert UUID v4 to Base58
|
||||||
|
// function uuidToBase58(uuid: string): string {
|
||||||
|
// const bytes = uuidToBytes(uuid);
|
||||||
|
// return encodeBase58(bytes);
|
||||||
|
// }
|
||||||
39
static/log.js
Normal file
39
static/log.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
let logLines = [];
|
||||||
|
let logLength = 100;
|
||||||
|
let logVisible = false;
|
||||||
|
export function logID(ID) {
|
||||||
|
if (!ID) {
|
||||||
|
return "badID";
|
||||||
|
}
|
||||||
|
return ID.substring(0, 5);
|
||||||
|
}
|
||||||
|
export function setLogVisibility(visible) {
|
||||||
|
logVisible = visible;
|
||||||
|
}
|
||||||
|
export function renderLog() {
|
||||||
|
if (!logVisible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let log = document.getElementById("log");
|
||||||
|
if (!log) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
log.innerText = logLines.join("\n");
|
||||||
|
}
|
||||||
|
export function log(...args) {
|
||||||
|
// console.log(...args);
|
||||||
|
let logLine = `[${new Date().toLocaleTimeString()}]: `;
|
||||||
|
for (let arg of args) {
|
||||||
|
let completeLine = (typeof arg === "string" || arg instanceof String) ? arg : JSON.stringify(arg, null, 4);
|
||||||
|
if (completeLine === undefined) {
|
||||||
|
completeLine = "undefined";
|
||||||
|
}
|
||||||
|
logLine += completeLine.substring(0, 500);
|
||||||
|
}
|
||||||
|
logLines.push(logLine + "\n");
|
||||||
|
if (logLines.length > logLength) {
|
||||||
|
logLines = logLines.slice(logLines.length - logLength);
|
||||||
|
}
|
||||||
|
renderLog();
|
||||||
|
return [logLine]; // [...args];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user