@browser-network/switchboard
Advanced tools
Comparing version
@@ -8,4 +8,5 @@ #!/usr/bin/env node | ||
var http_1 = __importDefault(require("http")); | ||
var MAX_NEGOTIATIONS = 500; | ||
var MAX_NEGOTIATION_AGE = 1000 * 60 * 3; | ||
var CLEAN_INTERVAL = 1000 * 60 * 1; | ||
var MAX_NEGOTIATIONS_ITEMS_PER_NETWORK = 500; | ||
var MAX_ADDRESS_AGE = 1000 * 30; // Keep it short | ||
var CORS_HEADERS = { | ||
@@ -22,3 +23,4 @@ 'Access-Control-Allow-Origin': '*', | ||
var server = http_1["default"].createServer(function (req, res) { | ||
var ok = function (json) { | ||
var ok = function (networkId, json) { | ||
console.log('ok for', networkId, json); | ||
res.writeHead(200, HEADERS); | ||
@@ -51,129 +53,64 @@ res.end(JSON.stringify(json)); | ||
req.on('end', function () { | ||
var body; | ||
var _a; | ||
var request; | ||
try { | ||
body = JSON.parse(stringifiedBody); | ||
request = JSON.parse(stringifiedBody); | ||
} | ||
catch (_a) { | ||
catch (_b) { | ||
return nope('json'); | ||
} | ||
// Just the minimum | ||
if (!body.sdp) | ||
return nope('sdp'); | ||
if (!body.address) | ||
// Shape check (if only typescript had runtime types) | ||
if (!request.address) | ||
return nope('address'); | ||
if (!body.networkId) | ||
if (!request.networkId) | ||
return nope('networkId'); | ||
if (!body.connectionId) | ||
return nope('connectionId'); | ||
if (!['offer', 'answer'].includes(body.type)) | ||
return nope('type'); | ||
console.log('receiving request from', body.address); | ||
// No random objects | ||
var negotiation = { | ||
sdp: body.sdp, | ||
address: body.address, | ||
networkId: body.networkId, | ||
type: body.type, | ||
timestamp: Date.now(), | ||
connectionId: body.connectionId | ||
}; | ||
// Now it's time to process the negotiation | ||
var address = negotiation.address, networkId = negotiation.networkId, connectionId = negotiation.connectionId; | ||
// Ensure the network exists | ||
if (!book[networkId]) { | ||
book[networkId] = {}; | ||
request.negotiationItems.forEach(function (item) { | ||
if (!item.from) | ||
return nope('from'); | ||
if (!item["for"]) | ||
return nope('for'); | ||
if (!item.negotiation) | ||
return nope('negotiation'); | ||
var negotiation = item.negotiation; | ||
if (!['offer', 'answer'].includes(negotiation.type)) | ||
return nope('type'); | ||
if (!negotiation.connectionId) | ||
return nope('connectionId'); | ||
if (!negotiation.sdp) | ||
return nope('sdp'); | ||
}); | ||
// The switchboard's actions | ||
// Upon getting a request: | ||
// 1) Take the address, and bring it into our list of addresses | ||
// { [address: string]: number } | ||
// 2) Add the negotiationItems to an array | ||
// * Don't need to dedup or nothin, each will only be sent once. | ||
// 3) Cull the expired addresses and networks | ||
// 4) Accumulate negotiationItems for the requesting address | ||
// 5) Send back response with all addresses and accumulated negotiationItems | ||
// First We make sure the network has been seen before and set it up if it hasn't | ||
if (!book[request.networkId]) { | ||
book[request.networkId] = { | ||
addresses: {}, | ||
negotiationItems: [] | ||
}; | ||
} | ||
// Send back the pool. Convenience method for console verbosity. | ||
var verboseOk = function () { | ||
var okData = Object.values(book[networkId]); | ||
console.log('returning ok: ' + networkId, okData.map(function (j) { return [j.type, j.address]; })); | ||
ok(okData); | ||
}; | ||
var getOfferByConId = function (conId, networkId, book) { | ||
return Object.values(book[networkId]).find(function (negotiation) { | ||
return negotiation.connectionId === conId && negotiation.type === 'offer'; | ||
}); | ||
}; | ||
var getAnswerByConId = function (conId, networkId, book) { | ||
return Object.values(book[networkId]).find(function (negotiation) { | ||
return negotiation.connectionId === conId && negotiation.type === 'answer'; | ||
}); | ||
}; | ||
// Here's the scheme: | ||
// | ||
// If we receive an answer, we remove the offer | ||
// that answer is in response to and post the answer. | ||
// This will ensure nobody else tries to connect to that offer | ||
// for the immediate time being to preserve them from waiting | ||
// on a futile connection. | ||
// | ||
// If we receive an offer and there's an existing answer to that offer, | ||
// then we assume the two will connect and we remove both the answer and | ||
// the offer with the answer's connectionId from the book. Of course this | ||
// is after sending back the answer. | ||
if (negotiation.type === 'answer') { | ||
var relatedOffer = getOfferByConId(connectionId, networkId, book); | ||
if (relatedOffer) { | ||
// remove related offer | ||
delete book[networkId][relatedOffer.address]; | ||
} | ||
// post this answer | ||
book[networkId][address] = negotiation; | ||
// send back book | ||
verboseOk(); | ||
// 1) Add address to our list | ||
book[request.networkId].addresses[request.address] = Date.now(); | ||
// 2) Add negotiationItems to our list | ||
(_a = book[request.networkId].negotiationItems).push.apply(_a, request.negotiationItems); | ||
// 3) Clean the expired addresses in this network | ||
cleanExpiredAddressesForNetworkId(request.networkId); | ||
// 4) Accumulate negotiationItems for the requesting address | ||
var negotiationItemsForRequester = book[request.networkId].negotiationItems.filter(function (item) { return item["for"] === request.address; }); | ||
// 5) Send back response with all addresses and accumulated negotiationItems | ||
ok(request.networkId, { | ||
addresses: Object.keys(book[request.networkId].addresses), | ||
negotiationItems: negotiationItemsForRequester | ||
}); | ||
// Now we trim the fat and remove all the oldest negotiationItems | ||
var items = book[request.networkId].negotiationItems; | ||
if (book[request.networkId].negotiationItems.length > MAX_NEGOTIATIONS_ITEMS_PER_NETWORK) { | ||
items.splice(MAX_NEGOTIATIONS_ITEMS_PER_NETWORK, items.length); | ||
} | ||
else { | ||
var relatedAnswer = getAnswerByConId(connectionId, networkId, book); | ||
// If there's an existing answer to this offer | ||
if (relatedAnswer) { | ||
// - send back the book with that answer | ||
verboseOk(); | ||
// - remove the answer | ||
delete book[networkId][relatedAnswer.address]; | ||
// - remove the existing offer | ||
delete book[networkId][address]; | ||
// - don't post the new offer | ||
} | ||
else { | ||
// - add this to our book | ||
book[networkId][address] = negotiation; | ||
// - send back the book | ||
verboseOk(); | ||
} | ||
} | ||
/** Garbage Collection **/ | ||
// Remove old ones from this network. We'll do just the current | ||
// network. | ||
for (var address_1 in book[networkId]) { | ||
var negotiation_1 = book[networkId][address_1]; | ||
if (Date.now() - negotiation_1.timestamp > MAX_NEGOTIATION_AGE) { | ||
delete book[networkId][address_1]; | ||
} | ||
} | ||
// Trim the fat (aka remove negotiations in excess of MAX_NEGOTIATIONS) | ||
// Contrary to popular belief, we actually want to trim the _newer_ | ||
// negotiations. This is because in an active network, the negotiation | ||
// needs time to go through. And this will only ever happen in an active | ||
// network. In fact you could even define a network as active by whether | ||
// this function is running or not. | ||
var addresses = Object.keys(book[networkId]); | ||
if (addresses.length > MAX_NEGOTIATIONS) { | ||
// Ok so we're too long. We'll nix the entry with the most recent | ||
// timestamp | ||
var now = Date.now(); | ||
// Loop through each, remembering the most recent | ||
// TODO just remember the highest date... | ||
var mostRecentAddress = void 0; | ||
var mostRecentDifference = Infinity; | ||
for (var _i = 0, addresses_1 = addresses; _i < addresses_1.length; _i++) { | ||
var address_2 = addresses_1[_i]; | ||
var difference = now - book[networkId][address_2].timestamp; | ||
if (difference < mostRecentDifference) { | ||
mostRecentAddress = address_2; | ||
mostRecentDifference = difference; | ||
} | ||
} | ||
// Finally, remove the newest entry | ||
delete book[mostRecentAddress]; | ||
} | ||
}); | ||
@@ -185,1 +122,28 @@ }); | ||
}); | ||
// We need to periodically clean this otherwise any transient networks will rock this thing. Testing... | ||
setInterval(function () { | ||
for (var networkId in book) { | ||
cleanExpiredAddressesForNetworkId(networkId); | ||
cleanNetworkIfEmpty(networkId); | ||
} | ||
}, CLEAN_INTERVAL); | ||
function cleanExpiredAddressesForNetworkId(networkId) { | ||
var _loop_1 = function (address) { | ||
var expiry = book[networkId].addresses[address]; | ||
var isExpired = Date.now() - expiry > MAX_ADDRESS_AGE; | ||
if (isExpired) { | ||
// remove the address from our book | ||
delete book[networkId].addresses[address]; | ||
// and remove all of the addresses lingering negotiations as well | ||
book[networkId].negotiationItems = book[networkId].negotiationItems.filter(function (item) { return item.from !== address; }); | ||
} | ||
}; | ||
for (var address in book[networkId].addresses) { | ||
_loop_1(address); | ||
} | ||
} | ||
function cleanNetworkIfEmpty(networkId) { | ||
if (book[networkId].addresses.length === 0) { | ||
delete book[networkId]; | ||
} | ||
} |
{ | ||
"name": "@browser-network/switchboard", | ||
"version": "0.0.2", | ||
"version": "0.1.0", | ||
"description": "The switching server required for the onboarding of new nodes into a @browser-network/network", | ||
@@ -40,2 +40,3 @@ "main": "dist/index.js", | ||
"devDependencies": { | ||
"nodemon": "^2.0.20", | ||
"np": "^7.6.1", | ||
@@ -42,0 +43,0 @@ "shx": "^0.3.4", |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
10561
-11.93%5
25%145
-19.89%2
100%