use multiaddr instead of multiserver addresses

This commit is contained in:
Andre Staltz 2024-01-16 10:58:57 +02:00
parent 4101e023cd
commit c2d1adb19e
No known key found for this signature in database
GPG Key ID: 9EDE23EA7E8A4890
11 changed files with 194 additions and 119 deletions

View File

@ -2,10 +2,11 @@ const debug = require('debug')('ppppp:net:connections')
const createNotify = require('pull-notify') const createNotify = require('pull-notify')
const run = require('promisify-tuple') const run = require('promisify-tuple')
const IP = require('ip') const IP = require('ip')
const Multiaddr = require('./multiaddr')
/** /**
* @typedef {import('./index').RpcConnectListener} RpcConnectListener * @typedef {import('./index').RpcConnectListener} RpcConnectListener
* @typedef {import('./index').Address} Address * @typedef {import('./index').Multiaddr} Multiaddr
* @typedef {import('./index').RPC} RPC * @typedef {import('./index').RPC} RPC
* @typedef {import('./index').Peer} Peer * @typedef {import('./index').Peer} Peer
* @typedef {import('./infos').Info} Info * @typedef {import('./infos').Info} Info
@ -17,7 +18,7 @@ const IP = require('ip')
* | 'connecting-failed' * | 'connecting-failed'
* | 'disconnecting' * | 'disconnecting'
* | 'disconnected'; * | 'disconnected';
* address: Address; * multiaddr: Multiaddr;
* pubkey: string | undefined; * pubkey: string | undefined;
* details?: any; * details?: any;
* }} ConnectionEvent * }} ConnectionEvent
@ -37,12 +38,12 @@ class Connections {
#closed #closed
/** @type {NotifyEvent} */ /** @type {NotifyEvent} */
#notifyEvent #notifyEvent
/** @type {Map<Address, RPC>} */ /** @type {Map<Multiaddr, RPC>} */
#rpcs #rpcs
/** /**
* Used only to schedule a connect when a disconnect is in progress. * Used only to schedule a connect when a disconnect is in progress.
* @type {Set<Address>} * @type {Set<Multiaddr>}
*/ */
#connectRetries #connectRetries
@ -62,7 +63,7 @@ class Connections {
} }
/** /**
* @param {Address} address * @param {string} address
* @returns {Info['inferredType']} * @returns {Info['inferredType']}
*/ */
static inferPeerType(address) { static inferPeerType(address) {
@ -111,28 +112,29 @@ class Connections {
} }
/** /**
* @type {(address: Address, rpc: RPC, weAreClient: boolean) => void} * @type {(address: string, rpc: RPC, weAreClient: boolean) => void}
*/ */
#prepareConnectedRPC = (address, rpc, weAreClient) => { #prepareConnectedRPC = (address, rpc, weAreClient) => {
const initiator = weAreClient ? 'we' : 'they' const initiator = weAreClient ? 'we' : 'they'
debug('Connected to %s, %s initiated it', address, initiator) const multiaddr = Multiaddr.fromMs(address)
debug('Connected to %s, %s initiated it', multiaddr, initiator)
const pubkey = Connections.extractSHSEPubkey(address) const pubkey = Connections.extractSHSEPubkey(address)
this.#rpcs.set(address, rpc) this.#rpcs.set(multiaddr, rpc)
rpc.once('closed', () => { rpc.once('closed', () => {
debug('Disconnected from %s', address) debug('Disconnected from %s', multiaddr)
this.#rpcs.delete(address) this.#rpcs.delete(multiaddr)
this.#infos.update(address, { state: 'disconnected' }) this.#infos.update(multiaddr, { state: 'disconnected' })
this.#notifyEvent({ type: 'disconnected', address, pubkey }) this.#notifyEvent({ type: 'disconnected', multiaddr, pubkey })
this.#infos.emit() this.#infos.emit()
}) })
const state = /**@type {Info['state']}*/ ('connected') const state = /**@type {Info['state']}*/ ('connected')
const inferredType = Connections.inferPeerType(address) const inferredType = Connections.inferPeerType(address)
this.#infos.update(address, { state, inferredType }) this.#infos.update(multiaddr, { state, inferredType })
this.#notifyEvent({ this.#notifyEvent({
type: state, type: state,
address, multiaddr,
pubkey, pubkey,
details: { rpc, weAreClient }, details: { rpc, weAreClient },
}) })
@ -140,37 +142,38 @@ class Connections {
} }
/** /**
* @param {string} address * @param {Multiaddr} multiaddr
* @returns {Promise<RPC>} * @returns {Promise<RPC>}
*/ */
async connect(address) { async connect(multiaddr) {
this.#assertNotClosed() this.#assertNotClosed()
const prevInfo = this.#infos.get(address) const address = Multiaddr.toMs(multiaddr)
const prevInfo = this.#infos.get(multiaddr)
switch (prevInfo?.state ?? 'disconnected') { switch (prevInfo?.state ?? 'disconnected') {
case 'connected': { case 'connected': {
const rpc = this.#rpcs.get(address) const rpc = this.#rpcs.get(multiaddr)
if (!rpc) { if (!rpc) {
// prettier-ignore // prettier-ignore
throw new Error(`Failed to connect to ${address} due to inconsistent internal state`); throw new Error(`Failed to connect to ${multiaddr} due to inconsistent internal state`);
} }
return rpc return rpc
} }
case 'disconnecting': { case 'disconnecting': {
// If disconnecting, schedule a connect() after disconnection completed // If disconnecting, schedule a connect() after disconnection completed
this.#connectRetries.add(address) this.#connectRetries.add(multiaddr)
// note: control flow should fall through below! // note: control flow should fall through below!
} }
case 'connecting': { case 'connecting': {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let timeout = 100 let timeout = 100
const checkAgain = () => { const checkAgain = () => {
const rpc = this.#rpcs.get(address) const rpc = this.#rpcs.get(multiaddr)
if (rpc) resolve(rpc) if (rpc) resolve(rpc)
else if (timeout > 5 * 60e3) { else if (timeout > 5 * 60e3) {
// prettier-ignore // prettier-ignore
reject(new Error(`Failed to connect to ${address} after waiting a long time`)) reject(new Error(`Failed to connect to ${multiaddr} after waiting a long time`))
} else { } else {
timeout *= 2 timeout *= 2
setTimeout(checkAgain, timeout) setTimeout(checkAgain, timeout)
@ -181,20 +184,20 @@ class Connections {
} }
case 'disconnected': { case 'disconnected': {
debug('Connecting to %s', address) debug('Connecting to %s', multiaddr)
const state = /**@type {Info['state']}*/ ('connecting') const state = /**@type {Info['state']}*/ ('connecting')
const pubkey = Connections.extractSHSEPubkey(address) const pubkey = Connections.extractSHSEPubkey(address)
this.#infos.update(address, { state }) this.#infos.update(multiaddr, { state })
this.#notifyEvent({ type: state, address, pubkey }) this.#notifyEvent({ type: state, multiaddr, pubkey })
this.#infos.emit() this.#infos.emit()
const [err, rpc] = await run(this.#peer.connect)(address) const [err, rpc] = await run(this.#peer.connect)(address)
if (err) { if (err) {
this.#infos.update(address, { state: 'disconnected' }) this.#infos.update(multiaddr, { state: 'disconnected' })
debug('Failed to connect to %s because: %s', address, err.message) debug('Failed to connect to %s because: %s', multiaddr, err.message)
this.#notifyEvent({ this.#notifyEvent({
type: 'connecting-failed', type: 'connecting-failed',
address, multiaddr,
pubkey, pubkey,
details: err, details: err,
}) })
@ -202,15 +205,15 @@ class Connections {
throw err throw err
} }
const concurrentInfo = this.#infos.get(address) const concurrentInfo = this.#infos.get(multiaddr)
if (!concurrentInfo || concurrentInfo.state !== 'connected') { if (!concurrentInfo || concurrentInfo.state !== 'connected') {
this.#prepareConnectedRPC(address, rpc, true) this.#prepareConnectedRPC(address, rpc, true)
return rpc return rpc
} else { } else {
const rpc2 = this.#rpcs.get(address) const rpc2 = this.#rpcs.get(multiaddr)
if (!rpc2) { if (!rpc2) {
// prettier-ignore // prettier-ignore
throw new Error(`Failed to connect to ${address} due to inconsistent internal state`); throw new Error(`Failed to connect to ${multiaddr} due to inconsistent internal state`);
} }
return rpc2 return rpc2
} }
@ -218,20 +221,21 @@ class Connections {
default: { default: {
// prettier-ignore // prettier-ignore
debug('Unexpected control flow, peer %s has bad state %o', address, prevInfo) debug('Unexpected control flow, peer %s has bad state %o', multiaddr, prevInfo)
// prettier-ignore // prettier-ignore
throw new Error(`Unexpected control flow, peer ${address} has bad state "${prevInfo?.state ?? '?'}"`) throw new Error(`Unexpected control flow, peer ${multiaddr} has bad state "${prevInfo?.state ?? '?'}"`)
} }
} }
} }
/** /**
* @param {Address} address * @param {Multiaddr} multiaddr
* @returns {Promise<boolean>} * @returns {Promise<boolean>}
*/ */
async disconnect(address) { async disconnect(multiaddr) {
this.#assertNotClosed() this.#assertNotClosed()
const prevInfo = this.#infos.get(address) const address = Multiaddr.toMs(multiaddr)
const prevInfo = this.#infos.get(multiaddr)
if (!prevInfo || prevInfo?.state === 'disconnected') return false if (!prevInfo || prevInfo?.state === 'disconnected') return false
if (prevInfo.state === 'disconnecting') return false if (prevInfo.state === 'disconnecting') return false
@ -241,7 +245,7 @@ class Connections {
rpc = await new Promise((resolve) => { rpc = await new Promise((resolve) => {
let timeout = 100 let timeout = 100
const checkAgain = () => { const checkAgain = () => {
const rpc = this.#rpcs.get(address) const rpc = this.#rpcs.get(multiaddr)
if (rpc) resolve(rpc) if (rpc) resolve(rpc)
else { else {
timeout *= 2 timeout *= 2
@ -252,20 +256,20 @@ class Connections {
checkAgain() checkAgain()
}) })
} else if (prevInfo.state === 'connected') { } else if (prevInfo.state === 'connected') {
const maybeRPC = this.#rpcs.get(address) const maybeRPC = this.#rpcs.get(multiaddr)
if (!maybeRPC) { if (!maybeRPC) {
// prettier-ignore // prettier-ignore
throw new Error(`Failed to disconnect from ${address} due to inconsistent internal state`); throw new Error(`Failed to disconnect from ${multiaddr} due to inconsistent internal state`);
} else { } else {
rpc = maybeRPC rpc = maybeRPC
} }
} }
debug('Disconnecting from %s', address) debug('Disconnecting from %s', multiaddr)
const state = /**@type {Info['state']}*/ ('disconnecting') const state = /**@type {Info['state']}*/ ('disconnecting')
const pubkey = Connections.extractSHSEPubkey(address) const pubkey = Connections.extractSHSEPubkey(address)
this.#infos.update(address, { state }) this.#infos.update(multiaddr, { state })
this.#notifyEvent({ type: state, address, pubkey }) this.#notifyEvent({ type: state, multiaddr, pubkey })
this.#infos.emit() this.#infos.emit()
// @ts-ignore // @ts-ignore
await run(rpc.close)(true) await run(rpc.close)(true)
@ -273,9 +277,9 @@ class Connections {
// Re-connect because while disconnect() was running, // Re-connect because while disconnect() was running,
// someone called connect() // someone called connect()
if (this.#connectRetries.has(address)) { if (this.#connectRetries.has(multiaddr)) {
this.#connectRetries.delete(address) this.#connectRetries.delete(multiaddr)
this.connect(address) this.connect(multiaddr)
} }
return true return true

View File

@ -3,7 +3,7 @@ const stats = require('statistics')
const ping = require('pull-ping') const ping = require('pull-ping')
/** /**
* @typedef {import('./index').Address} Address * @typedef {import('./index').Multiaddr} Multiaddr
* @typedef {import('./index').RPC} RPC * @typedef {import('./index').RPC} RPC
* @typedef {import('./index').Peer} Peer * @typedef {import('./index').Peer} Peer
* @typedef {import('./connections')} Connections * @typedef {import('./connections')} Connections
@ -19,13 +19,13 @@ const PROGRAM_STARTUP = Date.now()
*/ */
function glue(infos, connections) { function glue(infos, connections) {
/** /**
* @param {Address} address * @param {Multiaddr} multiaddr
* @param {RPC} rpc * @param {RPC} rpc
*/ */
function setupPing(address, rpc) { function setupPing(multiaddr, rpc) {
const PING_TIMEOUT = 5 * 6e4 // 5 minutes const PING_TIMEOUT = 5 * 6e4 // 5 minutes
const pp = ping({ serve: true, timeout: PING_TIMEOUT }, () => {}) const pp = ping({ serve: true, timeout: PING_TIMEOUT }, () => {})
infos.updateStats(address, () => ({ infos.updateStats(multiaddr, () => ({
ping: { ping: {
rtt: pp.rtt, rtt: pp.rtt,
skew: pp.skew, skew: pp.skew,
@ -36,7 +36,7 @@ function glue(infos, connections) {
rpc.net.ping({ timeout: PING_TIMEOUT }, (err, _) => { rpc.net.ping({ timeout: PING_TIMEOUT }, (err, _) => {
console.warn('remote peer ping err', err) console.warn('remote peer ping err', err)
// if (err?.name === 'TypeError') { // if (err?.name === 'TypeError') {
// infos.update(address, {stats: {ping: {fail: true}}}); // infos.update(multiaddr, {stats: {ping: {fail: true}}});
// } // }
}), }),
pp pp
@ -47,7 +47,7 @@ function glue(infos, connections) {
* @param {Event} ev * @param {Event} ev
*/ */
function onConnectingFailed(ev) { function onConnectingFailed(ev) {
infos.updateStats(ev.address, (prevStats) => ({ infos.updateStats(ev.multiaddr, (prevStats) => ({
failure: (prevStats?.failure ?? 0) + 1, failure: (prevStats?.failure ?? 0) + 1,
stateChange: Date.now(), stateChange: Date.now(),
duration: stats(prevStats?.duration, 0), duration: stats(prevStats?.duration, 0),
@ -58,18 +58,18 @@ function glue(infos, connections) {
* @param {Event} ev * @param {Event} ev
*/ */
function onConnected(ev) { function onConnected(ev) {
infos.updateStats(ev.address, () => ({ infos.updateStats(ev.multiaddr, () => ({
stateChange: Date.now(), stateChange: Date.now(),
failure: 0, failure: 0,
})) }))
if (ev.details.weAreClient) setupPing(ev.address, ev.details.rpc) if (ev.details.weAreClient) setupPing(ev.multiaddr, ev.details.rpc)
} }
/** /**
* @param {Event} ev * @param {Event} ev
*/ */
function bumpStateChange(ev) { function bumpStateChange(ev) {
infos.updateStats(ev.address, () => ({ infos.updateStats(ev.multiaddr, () => ({
stateChange: Date.now(), stateChange: Date.now(),
})) }))
} }
@ -78,7 +78,7 @@ function glue(infos, connections) {
* @param {Event} ev * @param {Event} ev
*/ */
function onDisconnected(ev) { function onDisconnected(ev) {
infos.updateStats(ev.address, (prevStats) => ({ infos.updateStats(ev.multiaddr, (prevStats) => ({
stateChange: Date.now(), stateChange: Date.now(),
duration: stats( duration: stats(
prevStats?.duration, prevStats?.duration,

View File

@ -9,7 +9,7 @@ const glue = require('./glue')
/** /**
* @typedef {import('pull-stream').Duplex<unknown, unknown>} Duplex * @typedef {import('pull-stream').Duplex<unknown, unknown>} Duplex
* @typedef {import('./connections').ConnectionEvent} ConnectionEvent * @typedef {import('./connections').ConnectionEvent} ConnectionEvent
* @typedef {string} Address * @typedef {`/${string}`} Multiaddr
* @typedef {(rpc: RPC, weAreClient: boolean) => void} RpcConnectListener * @typedef {(rpc: RPC, weAreClient: boolean) => void} RpcConnectListener
* @typedef {{ * @typedef {{
* shse: {pubkey: string}; * shse: {pubkey: string};
@ -24,12 +24,14 @@ const glue = require('./glue')
* multiserver: { * multiserver: {
* parse(address: string): any * parse(address: string): any
* }, * },
* }} Peer
* @typedef {Peer & {
* stream: {address: string}
* net: { * net: {
* ping(opts: {timeout: number}, cb: CB<void>): Duplex; * ping(opts: {timeout: number}, cb: CB<void>): Duplex;
* listen(): import('pull-stream').Source<ConnectionEvent>; * listen(): import('pull-stream').Source<ConnectionEvent>;
* }, * },
* }} Peer * }} RPC
* @typedef {Peer & {stream: {address: string}}} RPC
* @typedef {{ * @typedef {{
* global: { * global: {
* path?: string * path?: string
@ -97,56 +99,56 @@ function initNet(peer, config) {
} }
/** /**
* @param {Address} address * @param {Multiaddr} multiaddr
* @param {Partial<Info>} info * @param {Partial<Info>} info
*/ */
function stage(address, info) { function stage(multiaddr, info) {
if (info.state) throw new Error('Cannot stage peer info with "state" field') if (info.state) throw new Error('Cannot stage peer info with "state" field')
if (infos.has(address)) { if (infos.has(multiaddr)) {
return false return false
} else { } else {
infos.update(address, info) infos.update(multiaddr, info)
return true return true
} }
} }
/** /**
* @param {Address} address * @param {Multiaddr} multiaddr
* @param {CB<RPC>=} cb * @param {CB<RPC>=} cb
*/ */
function connect(address, cb) { function connect(multiaddr, cb) {
connections.connect(address).then( connections.connect(multiaddr).then(
(result) => cb?.(null, result), (result) => cb?.(null, result),
(err) => cb?.(err) (err) => cb?.(err)
) )
} }
/** /**
* @param {Address} address * @param {Multiaddr} multiaddr
* @param {CB<boolean>=} cb * @param {CB<boolean>=} cb
*/ */
function disconnect(address, cb) { function disconnect(multiaddr, cb) {
return connections.disconnect(address).then( return connections.disconnect(multiaddr).then(
(result) => cb?.(null, result), (result) => cb?.(null, result),
(err) => cb?.(err) (err) => cb?.(err)
) )
} }
/** /**
* @param {Address} address * @param {Multiaddr} multiaddr
*/ */
function forget(address) { function forget(multiaddr) {
disconnect(address, () => { disconnect(multiaddr, () => {
infos.remove(address) infos.remove(multiaddr)
}) })
} }
/** /**
* @param {Address} address * @param {Multiaddr} multiaddr
* @param {Info} info * @param {Info} info
*/ */
function updateInfo(address, info) { function updateInfo(multiaddr, info) {
infos.update(address, info) infos.update(multiaddr, info)
} }
function listen() { function listen() {

View File

@ -9,7 +9,7 @@ const Obz = require('obz')
*/ */
/** /**
* @typedef {import('./index').Address} Address * @typedef {import('./index').Multiaddr} Multiaddr
* @typedef {import('./stats').StatsInfo} StatsInfo * @typedef {import('./stats').StatsInfo} StatsInfo
* @typedef {{ * @typedef {{
* state: 'connected' | 'disconnected' | 'connecting' | 'disconnecting', * state: 'connected' | 'disconnected' | 'connecting' | 'disconnecting',
@ -19,11 +19,11 @@ const Obz = require('obz')
*/ */
class Infos { class Infos {
/** @type {Map<Address, Info>} */ /** @type {Map<Multiaddr, Info>} */
#map #map
/** @type {ReturnType<createNotify>} */ /** @type {ReturnType<createNotify>} */
#notify #notify
/** @type {Obz<Address>} */ /** @type {Obz<Multiaddr>} */
#onStatsUpdated #onStatsUpdated
constructor() { constructor() {
@ -33,76 +33,76 @@ class Infos {
} }
/** /**
* @param {Address} address * @param {Multiaddr} multiaddr
* @returns {Info | undefined} * @returns {Info | undefined}
*/ */
get(address) { get(multiaddr) {
return this.#map.get(address) return this.#map.get(multiaddr)
} }
/** /**
* @param {Address} address * @param {Multiaddr} multiaddr
* @returns {boolean} * @returns {boolean}
*/ */
has(address) { has(multiaddr) {
return this.#map.has(address) return this.#map.has(multiaddr)
} }
/** /**
* @param {Address} address * @param {Multiaddr} multiaddr
* @param {Partial<Info>} info * @param {Partial<Info>} info
* @returns {void} * @returns {void}
*/ */
update(address, info) { update(multiaddr, info) {
const hasNewStats = !!info.stats const hasNewStats = !!info.stats
const prevInfo = this.#map.get(address) const prevInfo = this.#map.get(multiaddr)
if (prevInfo) { if (prevInfo) {
for (const key of Object.keys(info)) { for (const key of Object.keys(info)) {
const k = /**@type {keyof Info}*/ (key) const k = /**@type {keyof Info}*/ (key)
if (typeof info[k] === 'undefined') delete info[k] if (typeof info[k] === 'undefined') delete info[k]
} }
this.#map.set(address, { ...prevInfo, ...info }) this.#map.set(multiaddr, { ...prevInfo, ...info })
} else if (!info.state) { } else if (!info.state) {
this.#map.set(address, { ...info, state: 'disconnected' }) this.#map.set(multiaddr, { ...info, state: 'disconnected' })
} else { } else {
this.#map.set(address, /**@type {Info}*/ (info)) this.#map.set(multiaddr, /**@type {Info}*/ (info))
} }
if (hasNewStats) { if (hasNewStats) {
this.#onStatsUpdated.set(address) this.#onStatsUpdated.set(multiaddr)
} }
} }
/** /**
* @param {Address} address * @param {Multiaddr} multiaddr
* @param {(prevStats: Partial<Info['stats']>) => Partial<Info['stats']>} getStats * @param {(prevStats: Partial<Info['stats']>) => Partial<Info['stats']>} getStats
* @returns {void} * @returns {void}
*/ */
updateStats(address, getStats) { updateStats(multiaddr, getStats) {
const prevInfo = this.#map.get(address) const prevInfo = this.#map.get(multiaddr)
if (!prevInfo) return if (!prevInfo) return
this.#map.set(address, { this.#map.set(multiaddr, {
...prevInfo, ...prevInfo,
stats: { stats: {
...prevInfo?.stats, ...prevInfo?.stats,
...getStats(prevInfo?.stats), ...getStats(prevInfo?.stats),
}, },
}) })
this.#onStatsUpdated.set(address) this.#onStatsUpdated.set(multiaddr)
} }
/** /**
* @param {Parameters<Obz<Address>>[0]} listener * @param {Parameters<Obz<Multiaddr>>[0]} listener
*/ */
onStatsUpdated(listener) { onStatsUpdated(listener) {
return this.#onStatsUpdated(listener) return this.#onStatsUpdated(listener)
} }
/** /**
* @param {Address} address * @param {Multiaddr} multiaddr
*/ */
remove(address) { remove(multiaddr) {
this.#map.delete(address) this.#map.delete(multiaddr)
this.#onStatsUpdated.set(address) this.#onStatsUpdated.set(multiaddr)
} }
size() { size() {
@ -118,7 +118,7 @@ class Infos {
} }
/** /**
* @returns {pull.Source<[Address, Info]>} * @returns {pull.Source<[Multiaddr, Info]>}
*/ */
liveEntries() { liveEntries() {
return pullConcat([ return pullConcat([

67
lib/multiaddr.js Normal file
View File

@ -0,0 +1,67 @@
const IP = require('ip')
const Multiaddr = {
/**
* Converts (legacy) [multiserver](https://github.com/ssbc/multiserver-address)
* addresses to [multiaddr](https://multiformats.io/multiaddr/) (modern).
* @param {string} msaddr
* @returns {`/${string}`}
*/
fromMs(msaddr) {
const [msTransport, msTransform] = msaddr.split('~')
const [label1, host, port] = msTransport.split(':')
const hostFormat = IP.isV4Format(host)
? 'ip4'
: IP.isV6Format('ipv6')
? 'ip6'
: 'dns'
const transport = label1 === 'net' ? 'tcp' : label1 === 'ws' ? 'ws' : null
if (!transport) throw new Error(`Unknown transport "${label1}"`)
const soFar = `${hostFormat}/${host}/${transport}/${port}`
if (msTransform) {
const [label2, pubkey, token] = msTransform.split(':')
if (label2 !== 'shse') throw new Error(`Unknown transform "${label2}"`)
if (token) {
return `/${soFar}/shse/${pubkey}.${token}`
} else {
return `/${soFar}/shse/${pubkey}`
}
} else {
return `/${soFar}`
}
},
/**
* Converts [multiaddr](https://multiformats.io/multiaddr/) (modern) to
* [multiserver](https://github.com/ssbc/multiserver-address) address (legacy).
* @param {`/${string}`} multiaddr
* @returns {string}
*/
toMs(multiaddr) {
if (!multiaddr.startsWith('/')) {
// prettier-ignore
throw new Error(`Invalid multiaddr "${multiaddr}"`)
}
const [, , host, transport, port, transform, cred] = multiaddr.split('/')
const label1 =
transport === 'tcp' ? 'net' : transport === 'ws' ? 'ws' : null
if (!label1) throw new Error(`Unknown transport "${transport}"`)
const soFar = `${label1}:${host}:${port}`
if (transform) {
// prettier-ignore
if (transform !== 'shse') throw new Error(`Unknown transform "${transform}"`)
const [pubkey, token] = cred.split('.')
if (token) {
return `${soFar}~shse:${pubkey}:${token}`
} else {
return `${soFar}~shse:${pubkey}`
}
} else {
return soFar
}
},
}
module.exports = Multiaddr

View File

@ -4,7 +4,7 @@ const debug = require('debug')('ppppp:net:stats')
const atomic = require('atomic-file-rw') const atomic = require('atomic-file-rw')
/** /**
* @typedef {import('./index').Address} Address * @typedef {import('./index').Multiaddr} Multiaddr
* @typedef {import('./infos')} Infos * @typedef {import('./infos')} Infos
* @typedef {import('statistics').Statistics} Statistics * @typedef {import('statistics').Statistics} Statistics
* @typedef {{ * @typedef {{
@ -42,7 +42,7 @@ const SelfHealingJSONCodec = {
}, },
/** /**
* @param {any} input * @param {any} input
* @returns {Record<string, any>} * @returns {Record<`/${string}`, any>}
*/ */
decode(input) { decode(input) {
if (!input) return {} if (!input) return {}
@ -114,8 +114,10 @@ class Stats {
return return
} else if (fileContents) { } else if (fileContents) {
const vals = SelfHealingJSONCodec.decode(fileContents) const vals = SelfHealingJSONCodec.decode(fileContents)
for (const [address, statsInfo] of Object.entries(vals)) { for (const [multiaddr, statsInfo] of Object.entries(vals)) {
this.#infos.update(address, { stats: statsInfo }) this.#infos.update(/**@type {`/${string}`}*/ (multiaddr), {
stats: statsInfo,
})
} }
this.#loadedResolve(true) this.#loadedResolve(true)
debug('Loaded conn.json into ConnDB in memory') debug('Loaded conn.json into ConnDB in memory')
@ -180,10 +182,10 @@ class Stats {
*/ */
#writeToDisk(cb) { #writeToDisk(cb) {
debug(`Begun serializing and writing ${Stats.FILENAME}`) debug(`Begun serializing and writing ${Stats.FILENAME}`)
const record = /**@type {Record<Address, StatsInfo>}*/ ({}) const record = /**@type {Record<Multiaddr, StatsInfo>}*/ ({})
for (let [address, info] of this.#infos.entries()) { for (let [multiaddr, info] of this.#infos.entries()) {
if (info.stats) { if (info.stats) {
record[address] = info.stats record[multiaddr] = info.stats
} }
} }
const json = SelfHealingJSONCodec.encode(record) const json = SelfHealingJSONCodec.encode(record)
@ -198,7 +200,7 @@ class Stats {
this.#closed = true this.#closed = true
this.#cancelScheduleWrite() this.#cancelScheduleWrite()
this.#writeToDisk() this.#writeToDisk()
;/**@type {any}*/ (this).#infos = void 0 ;/**@type {any}*/ (this.#infos) = void 0
debug('Closed the Stats instance') debug('Closed the Stats instance')
} }

View File

@ -1,5 +1,5 @@
{ {
"net:staltz.com:8008~noauth": { "/dns/staltz.com/tcp/8008": {
"source": "stored" "source": "stored"
} }
}, },

View File

@ -1,5 +1,5 @@
{ {
"net:staltz.com:8008~noauth": { "/dns/staltz.com/tcp/8008": {
"duration": { "duration": {
"mean": 0, "mean": 0,
"stdev": 0, "stdev": 0,

View File

@ -5,8 +5,8 @@ const Path = require('node:path')
const p = require('node:util').promisify const p = require('node:util').promisify
const { createPeerMock } = require('./util') const { createPeerMock } = require('./util')
const TEST_ADDR = const PUBKEY = 'EqTMFv7zm8hpPyAkj789qdJgqtz81AEbcinpAs24RRUC'
'net:localhost:9752~shse:EqTMFv7zm8hpPyAkj789qdJgqtz81AEbcinpAs24RRUC' const TEST_ADDR = `/ip4/127.0.0.1/tcp/9752/shse/${PUBKEY}`
test('Glueing together stats with connections', async (t) => { test('Glueing together stats with connections', async (t) => {
await t.test('stage() is ignored when peer already connected', async () => { await t.test('stage() is ignored when peer already connected', async () => {

View File

@ -5,7 +5,7 @@ const pull = require('pull-stream')
const { createPeer } = require('./util') const { createPeer } = require('./util')
const PUBKEY = 'EqTMFv7zm8hpPyAkj789qdJgqtz81AEbcinpAs24RRUC' const PUBKEY = 'EqTMFv7zm8hpPyAkj789qdJgqtz81AEbcinpAs24RRUC'
const TEST_ADDR = `net:localhost:9752~shse:${PUBKEY}` const TEST_ADDR = `/ip4/127.0.0.1/tcp/9752/shse/${PUBKEY}`
test('net', async (t) => { test('net', async (t) => {
await t.test('connect() rejects given unreachable address', async () => { await t.test('connect() rejects given unreachable address', async () => {
@ -68,11 +68,11 @@ test('net', async (t) => {
++i ++i
if (i === 1) { if (i === 1) {
assert.equal(ev.type, 'connecting', 'event.type ok') assert.equal(ev.type, 'connecting', 'event.type ok')
assert.equal(ev.address, TEST_ADDR, 'event.address ok') assert.equal(ev.multiaddr, TEST_ADDR, 'event.address ok')
assert.equal(ev.pubkey, PUBKEY, 'event.pubkey ok') assert.equal(ev.pubkey, PUBKEY, 'event.pubkey ok')
} else if (i === 2) { } else if (i === 2) {
assert.equal(ev.type, 'connecting-failed', 'event.type ok') assert.equal(ev.type, 'connecting-failed', 'event.type ok')
assert.equal(ev.address, TEST_ADDR, 'event.address ok') assert.equal(ev.multiaddr, TEST_ADDR, 'event.address ok')
assert.ok(ev.details, 'event.details ok') assert.ok(ev.details, 'event.details ok')
assert.equal(ev.details.code, 'ECONNREFUSED', 'event.details err') assert.equal(ev.details.code, 'ECONNREFUSED', 'event.details err')
queueMicrotask(resolve) queueMicrotask(resolve)

View File

@ -24,7 +24,7 @@ test('Stats', async (t) => {
const entriesAfter = Array.from(infos.entries()) const entriesAfter = Array.from(infos.entries())
assert.equal(entriesAfter.length, 1, 'after loaded(), there is data') assert.equal(entriesAfter.length, 1, 'after loaded(), there is data')
const [address, info] = entriesAfter[0] const [address, info] = entriesAfter[0]
assert.equal(address, 'net:staltz.com:8008~noauth', 'the address looks ok') assert.equal(address, '/dns/staltz.com/tcp/8008', 'the address looks ok')
assert.equal(info.stats.source, 'stored', 'the info looks ok') assert.equal(info.stats.source, 'stored', 'the info looks ok')
stats.close() stats.close()
@ -64,7 +64,7 @@ test('Stats', async (t) => {
const entries = Array.from(infos.entries()) const entries = Array.from(infos.entries())
assert.equal(entries.length === 1, true, 'stats has one entry') assert.equal(entries.length === 1, true, 'stats has one entry')
assert.equal(entries[0][0], 'net:staltz.com:8008~noauth', 'entry addr ok') assert.equal(entries[0][0], '/dns/staltz.com/tcp/8008', 'entry addr ok')
assert.ok(entries[0][1].stats.duration, 'entry stats.duration ok') assert.ok(entries[0][1].stats.duration, 'entry stats.duration ok')
}) })