ipfs-coord
Version:
A JS library for helping IPFS peers coordinate, find a common interest, and stay connected around that interest.
236 lines (193 loc) • 8.37 kB
JavaScript
/*
Adapter library for working with pubsub channels and messages
*/
// Local libraries
const Messaging = require('./messaging')
const AboutAdapter = require('./about-adapter')
class PubsubAdapter {
constructor (localConfig = {}) {
// Dependency Injection
this.ipfs = localConfig.ipfs
if (!this.ipfs) {
throw new Error(
'Instance of IPFS adapter required when instantiating Pubsub Adapter.'
)
}
this.log = localConfig.log
if (!this.log) {
throw new Error(
'A status log handler function required when instantitating Pubsub Adapter'
)
}
this.encryption = localConfig.encryption
if (!this.encryption) {
throw new Error(
'An instance of the encryption Adapter must be passed when instantiating the Pubsub Adapter library.'
)
}
this.privateLog = localConfig.privateLog
if (!this.privateLog) {
throw new Error(
'A private log handler must be passed when instantiating the Pubsub Adapter library.'
)
}
// Encapsulate dependencies
this.messaging = new Messaging(localConfig)
this.about = new AboutAdapter(localConfig)
// 'embedded' node type used as default, will use embedded js-ipfs.
// Alternative is 'external' which will use ipfs-http-client to control an
// external IPFS node.
this.nodeType = localConfig.nodeType
if (!this.nodeType) {
// console.log('No node type specified. Assuming embedded js-ipfs.')
this.nodeType = 'embedded'
}
// console.log(`PubsubAdapter contructor node type: ${this.nodeType}`)
}
// Subscribe to a pubsub channel. Any data received on that channel is passed
// to the handler.
async subscribeToPubsubChannel (chanName, handler, thisNode) {
try {
// console.log('thisNode: ', thisNode)
const thisNodeId = thisNode.ipfsId
// Normal use-case, where the pubsub channel is NOT the receiving channel
// for this node. This applies to general broadcast channels like
// the coordination channel that all nodes use to annouce themselves.
if (chanName !== thisNodeId) {
// console.log('this.ipfs: ', this.ipfs)
await this.ipfs.ipfs.pubsub.subscribe(chanName, async (msg) => {
await this.parsePubsubMessage(msg, handler, thisNode)
})
//
} else {
// Subscribing to our own pubsub channel. This is the channel other nodes
// will use to send RPC commands and send private messages.
await this.ipfs.ipfs.pubsub.subscribe(chanName, async (msg) => {
const msgObj = await this.messaging.handleIncomingData(msg, thisNode)
// If msgObj is false, then ignore it. Typically indicates an already
// processed message.
if (msgObj) { await this.handleNewMessage(msgObj, thisNode) }
})
}
this.log.statusLog(
0,
`status: Subscribed to pubsub channel: ${chanName}`
)
return true
} catch (err) {
console.error('Error in subscribeToPubsubChannel()')
throw err
}
}
// After the messaging.js library does the lower-level message handling and
// decryption, it passes the message on to this function, which does any
// additional parsing needed, and
// then routes the parsed data on to the user-specified handler.
async handleNewMessage (msgObj, thisNode) {
try {
// console.log('handleNewMessage() will forward this data onto the handler: ', msgObj)
// Check to see if this is metrics data or user-requested data.
// If it the response to a metrics query, trigger the handler for that.
// Dev note: This only handles the response. The JSON RPC must pass
// through this function to the privateLog, to be handled by the service
// being measured.
const isAbout = await this.captureMetrics(msgObj.data.payload, msgObj.from, thisNode)
// Pass the JSON RPC data to the private log to be handled by the app
// consuming this library.
if (!isAbout) {
// console.log('handleNewMessage() forwarding payload on to handler.')
this.privateLog(msgObj.data.payload, msgObj.from)
return true
}
return false
} catch (err) {
console.error('Error in handleNewMessage()')
throw err
}
}
// Scans input data. If the data is determined to be an 'about' JSON RPC
// reponse used for metrics, then the relayMetrics event is triggered and
// true is returned. Otherwise, false is returned.
async captureMetrics (decryptedStr, from, thisNode) {
try {
// console.log('decryptedStr: ', decryptedStr)
// console.log('thisNode: ', thisNode)
const data = JSON.parse(decryptedStr)
// console.log('data: ', data)
// Handle /about JSON RPC queries.
if (data.id.includes('metrics') && data.method === 'about') {
// Request recieved, send response.
// console.log('/about JSON RPC captured. Sending back announce object.')
// console.log('thisNode.schema.state.announceJsonLd: ', thisNode.schema.state.announceJsonLd)
const jsonResponse = `{"jsonrpc": "2.0", "id": "${data.id}", "result": {"method": "about", "receiver": "${from}", "value": ${JSON.stringify(thisNode.schema.state)}}}`
// console.log(`Responding with this JSON RPC response: ${jsonResponse}`)
// Encrypt the string with the peers public key.
const peerData = thisNode.peerData.filter(x => x.from === from)
const payload = await this.encryption.encryptMsg(
peerData[0],
jsonResponse
)
await this.messaging.sendMsg(from, payload, thisNode)
return true
//
} else if (data.id.includes('metrics') && data.result && data.result.method === 'about') {
// Response received.
// console.log('JSON RPC /about response aquired.')
// This event is handled by the about-adapter.js. It measures the
// latency between peers.
this.about.relayMetricsReceived(decryptedStr)
return data.result.value
}
// This is not an /about JSON RPC query.
// console.log('JSON RPC is not targeting the /about endpoint')
return false
} catch (err) {
console.error('Error in captureMetrics: ', err)
return false
}
}
// Attempts to parse data coming in from a pubsub channel. It is assumed that
// the data is a string in JSON format. If it isn't, parsing will throw an
// error and the message will be ignored.
async parsePubsubMessage (msg, handler, thisNode) {
try {
// console.log('message data: ', msg.data)
// console.log('parsePubsubMessage msg: ', msg)
// console.log('thisNode: ', thisNode)
const thisNodeId = thisNode.ipfsId
// Get data about the message.
const from = msg.from
const channel = msg.topicIDs[0]
// Used for debugging.
this.log.statusLog(
2,
`Broadcast pubsub message recieved from ${from} on channel ${channel}`
)
// Ignore this message if it originated from this IPFS node.
if (from === thisNodeId) return true
// The data on browsers comes through as a uint8array, and on node.js
// implementiong it comes through as a string. Browsers need to
// convert the message from a uint8array to a string.
// console.log('this.nodeType: ', this.nodeType)
if (thisNode.type === 'browser' || this.nodeType === 'external') {
// console.log('Node type is browser')
msg.data = new TextDecoder('utf-8').decode(msg.data)
}
// Parse the data into a JSON object. It starts as a Buffer that needs
// to be converted to a string, then parsed to a JSON object.
// For some reason I have to JSON parse it twice. Not sure why.
const data = JSON.parse(JSON.parse(msg.data.toString()))
// console.log('data: ', data)
const retObj = { from, channel, data }
// console.log(`new pubsub message received: ${JSON.stringify(retObj, null, 2)}`)
// Hand retObj to the callback.
handler(retObj)
return true
} catch (err) {
console.error('Error in parsePubsubMessage(): ', err.message)
// Do not throw an error. This is a top-level function.
return false
}
}
}
module.exports = PubsubAdapter