helia-coord 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.on-save.json +8 -0
- package/PEDIGREE.md +3 -0
- package/README.md +3 -0
- package/config/bootstrap-circuit-relays.js +48 -0
- package/examples/start-external.js +56 -0
- package/index.js +129 -0
- package/lib/adapters/bch-adapter.js +94 -0
- package/lib/adapters/encryption-adapter.js +123 -0
- package/lib/adapters/gist.js +42 -0
- package/lib/adapters/index.js +66 -0
- package/lib/adapters/ipfs-adapter.js +263 -0
- package/lib/adapters/logs-adapter.js +44 -0
- package/lib/adapters/pubsub-adapter/README.md +86 -0
- package/lib/adapters/pubsub-adapter/about-adapter.js +117 -0
- package/lib/adapters/pubsub-adapter/index.js +275 -0
- package/lib/adapters/pubsub-adapter/messaging.js +389 -0
- package/lib/adapters/pubsub-adapter/msg-router.js +58 -0
- package/lib/adapters/pubsub-adapter/resend-msg.js +58 -0
- package/lib/controllers/index.js +24 -0
- package/lib/controllers/timer-controller.js +417 -0
- package/lib/entities/this-node-entity.js +102 -0
- package/lib/use-cases/index.js +36 -0
- package/lib/use-cases/peer-use-cases.js +146 -0
- package/lib/use-cases/pubsub-use-cases.js +56 -0
- package/lib/use-cases/relay-use-cases.js +479 -0
- package/lib/use-cases/schema.js +158 -0
- package/lib/use-cases/this-node-use-cases.js +443 -0
- package/lib/util/utils.js +12 -0
- package/package.json +52 -0
- package/test/mocks/adapter-mock.js +119 -0
- package/test/mocks/circuit-relay-mocks.js +67 -0
- package/test/mocks/ipfs-mock.js +46 -0
- package/test/mocks/peers-mock.js +75 -0
- package/test/mocks/pubsub-mocks.js +37 -0
- package/test/mocks/thisnode-mocks.js +82 -0
- package/test/mocks/use-case-mocks.js +24 -0
- package/test/unit/adapters/bch-adapter-unit.js +96 -0
- package/test/unit/adapters/encryption-adapter-unit.js +129 -0
- package/test/unit/adapters/gist.unit.adapters.js +58 -0
- package/test/unit/adapters/index-adapters-unit.js +79 -0
- package/test/unit/adapters/ipfs-adapter-unit.js +215 -0
- package/test/unit/adapters/logs-adapter-unit.js +55 -0
- package/test/unit/adapters/pubsub/about-adapter-unit.js +129 -0
- package/test/unit/adapters/pubsub/messaging-adapter-unit.js +576 -0
- package/test/unit/adapters/pubsub/pubsub-adapter-unit.js +367 -0
- package/test/unit/adapters/pubsub/resend-msg-adapter-unit.js +58 -0
- package/test/unit/controllers/controllers-index-unit.js +30 -0
- package/test/unit/controllers/timer-controller-unit.js +261 -0
- package/test/unit/entities/this-node.unit.entity.js +157 -0
- package/test/unit/index-unit.js +160 -0
- package/test/unit/use-cases/peer.unit.use-cases.js +186 -0
- package/test/unit/use-cases/pubsub.unit.use-cases.js +114 -0
- package/test/unit/use-cases/relay-use-cases-unit.js +658 -0
- package/test/unit/use-cases/schema-use-case-unit.js +101 -0
- package/test/unit/use-cases/this-node-use-cases-unit.js +427 -0
- package/test/unit/use-cases/use-cases-index-unit.js +47 -0
- package/test/unit/util/utils-unit.js +31 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Adapter library for working with pubsub channels and messages
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Local libraries
|
|
6
|
+
import Messaging from './messaging.js'
|
|
7
|
+
import AboutAdapter from './about-adapter.js'
|
|
8
|
+
import { BroadcastRouter, PrivateChannelRouter } from './msg-router.js'
|
|
9
|
+
|
|
10
|
+
class PubsubAdapter {
|
|
11
|
+
constructor (localConfig = {}) {
|
|
12
|
+
// Dependency Injection
|
|
13
|
+
this.ipfs = localConfig.ipfsAdapter
|
|
14
|
+
if (!this.ipfs) {
|
|
15
|
+
console.log('this.ipfs: ', this.ipfs)
|
|
16
|
+
throw new Error(
|
|
17
|
+
'Instance of IPFS adapter required when instantiating Pubsub Adapter.'
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
this.log = localConfig.log
|
|
21
|
+
if (!this.log) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
'A status log handler function required when instantitating Pubsub Adapter'
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
this.encryption = localConfig.encryption
|
|
27
|
+
if (!this.encryption) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
'An instance of the encryption Adapter must be passed when instantiating the Pubsub Adapter library.'
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
this.privateLog = localConfig.privateLog
|
|
33
|
+
if (!this.privateLog) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
'A private log handler must be passed when instantiating the Pubsub Adapter library.'
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Encapsulate dependencies
|
|
40
|
+
this.messaging = new Messaging(localConfig)
|
|
41
|
+
this.about = new AboutAdapter(localConfig)
|
|
42
|
+
|
|
43
|
+
// 'embedded' node type used as default, will use embedded js-ipfs.
|
|
44
|
+
// Alternative is 'external' which will use ipfs-http-client to control an
|
|
45
|
+
// external IPFS node.
|
|
46
|
+
this.nodeType = localConfig.nodeType
|
|
47
|
+
if (!this.nodeType) {
|
|
48
|
+
// console.log('No node type specified. Assuming embedded js-ipfs.')
|
|
49
|
+
this.nodeType = 'embedded'
|
|
50
|
+
}
|
|
51
|
+
// console.log(`PubsubAdapter contructor node type: ${this.nodeType}`)
|
|
52
|
+
|
|
53
|
+
// Bind functions that are called by event handlers
|
|
54
|
+
this.parsePubsubMessage = this.parsePubsubMessage.bind(this)
|
|
55
|
+
this.handleNewMessage = this.handleNewMessage.bind(this)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Subscribe to a pubsub channel. Any data received on that channel is passed
|
|
59
|
+
// to the handler.
|
|
60
|
+
async subscribeToPubsubChannel (chanName, handler, thisNode) {
|
|
61
|
+
try {
|
|
62
|
+
// console.log('thisNode: ', thisNode)
|
|
63
|
+
const thisNodeId = thisNode.ipfsId
|
|
64
|
+
|
|
65
|
+
// Normal use-case, where the pubsub channel is NOT the receiving channel
|
|
66
|
+
// for this node. This applies to general broadcast channels like
|
|
67
|
+
// the coordination channel that all nodes use to annouce themselves.
|
|
68
|
+
if (chanName !== thisNodeId) {
|
|
69
|
+
// console.log('this.ipfs: ', this.ipfs)
|
|
70
|
+
// await this.ipfs.ipfs.pubsub.subscribe(chanName.toString(), async (msg) => {
|
|
71
|
+
// await this.parsePubsubMessage(msg, handler, thisNode)
|
|
72
|
+
// })
|
|
73
|
+
|
|
74
|
+
// Instantiate the Broadcast message router library
|
|
75
|
+
const bRouterOptions = {
|
|
76
|
+
handler,
|
|
77
|
+
thisNode,
|
|
78
|
+
parsePubsubMessage: this.parsePubsubMessage
|
|
79
|
+
}
|
|
80
|
+
const broadcastRouter = new BroadcastRouter(bRouterOptions)
|
|
81
|
+
|
|
82
|
+
// Subscribe to the pubsub channel. Route any incoming messages to the
|
|
83
|
+
// appropriate handler.
|
|
84
|
+
// await this.ipfs.ipfs.pubsub.subscribe(chanName.toString(), broadcastRouter.route)
|
|
85
|
+
await this.ipfs.ipfs.libp2p.services.pubsub.subscribe(chanName.toString(), broadcastRouter.route)
|
|
86
|
+
|
|
87
|
+
//
|
|
88
|
+
} else {
|
|
89
|
+
// Subscribing to our own pubsub channel. This is the channel other nodes
|
|
90
|
+
// will use to send RPC commands and send private messages.
|
|
91
|
+
|
|
92
|
+
// Instantiate the Broadcast message router library
|
|
93
|
+
const pRouterOptions = {
|
|
94
|
+
thisNode,
|
|
95
|
+
messaging: this.messaging,
|
|
96
|
+
handleNewMessage: this.handleNewMessage
|
|
97
|
+
}
|
|
98
|
+
const privateRouter = new PrivateChannelRouter(pRouterOptions)
|
|
99
|
+
|
|
100
|
+
// Subscribe to the pubsbu channel. Route any incoming messages to the
|
|
101
|
+
// this library.
|
|
102
|
+
// await this.ipfs.ipfs.pubsub.subscribe(chanName.toString(), privateRouter.route)
|
|
103
|
+
await this.ipfs.ipfs.libp2p.services.pubsub.subscribe(chanName.toString(), privateRouter.route)
|
|
104
|
+
|
|
105
|
+
// await this.ipfs.ipfs.pubsub.subscribe(chanName.toString(), async (msg) => {
|
|
106
|
+
// const msgObj = await this.messaging.handleIncomingData(msg, thisNode)
|
|
107
|
+
//
|
|
108
|
+
// // If msgObj is false, then ignore it. Typically indicates an already
|
|
109
|
+
// // processed message.
|
|
110
|
+
// if (msgObj) { await this.handleNewMessage(msgObj, thisNode) }
|
|
111
|
+
// })
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
this.log.statusLog(
|
|
115
|
+
0,
|
|
116
|
+
`status: Subscribed to pubsub channel: ${chanName}`
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return true
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.error('Error in subscribeToPubsubChannel()')
|
|
122
|
+
throw err
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// After the messaging.js library does the lower-level message handling and
|
|
127
|
+
// decryption, it passes the message on to this function, which does any
|
|
128
|
+
// additional parsing needed, and
|
|
129
|
+
// then routes the parsed data on to the user-specified handler.
|
|
130
|
+
async handleNewMessage (msgObj, thisNode) {
|
|
131
|
+
try {
|
|
132
|
+
// console.log('handleNewMessage() will forward this data onto the handler: ', msgObj)
|
|
133
|
+
|
|
134
|
+
// Check to see if this is metrics data or user-requested data.
|
|
135
|
+
// If it the response to a metrics query, trigger the handler for that.
|
|
136
|
+
// Dev note: This only handles the response. The JSON RPC must pass
|
|
137
|
+
// through this function to the privateLog, to be handled by the service
|
|
138
|
+
// being measured.
|
|
139
|
+
const isAbout = await this.captureMetrics(msgObj.data.payload, msgObj.from, thisNode)
|
|
140
|
+
|
|
141
|
+
// Pass the JSON RPC data to the private log to be handled by the app
|
|
142
|
+
// consuming this library.
|
|
143
|
+
if (!isAbout) {
|
|
144
|
+
// console.log('handleNewMessage() forwarding payload on to handler.')
|
|
145
|
+
this.privateLog(msgObj.data.payload, msgObj.from)
|
|
146
|
+
|
|
147
|
+
return true
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return false
|
|
151
|
+
} catch (err) {
|
|
152
|
+
console.error('Error in handleNewMessage()')
|
|
153
|
+
throw err
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Scans input data. If the data is determined to be an 'about' JSON RPC
|
|
158
|
+
// reponse used for metrics, then the relayMetrics event is triggered and
|
|
159
|
+
// true is returned. Otherwise, false is returned.
|
|
160
|
+
async captureMetrics (decryptedStr, from, thisNode) {
|
|
161
|
+
try {
|
|
162
|
+
// console.log('decryptedStr: ', decryptedStr)
|
|
163
|
+
// console.log('thisNode: ', thisNode)
|
|
164
|
+
|
|
165
|
+
const data = JSON.parse(decryptedStr)
|
|
166
|
+
// console.log('data: ', data)
|
|
167
|
+
|
|
168
|
+
// Handle /about JSON RPC queries.
|
|
169
|
+
if (data.id.includes('metrics') && data.method === 'about') {
|
|
170
|
+
// Request recieved, send response.
|
|
171
|
+
|
|
172
|
+
// console.log('/about JSON RPC captured. Sending back announce object.')
|
|
173
|
+
|
|
174
|
+
// console.log('thisNode.schema.state.announceJsonLd: ', thisNode.schema.state.announceJsonLd)
|
|
175
|
+
|
|
176
|
+
const jsonResponse = `{"jsonrpc": "2.0", "id": "${data.id}", "result": {"method": "about", "receiver": "${from}", "value": ${JSON.stringify(thisNode.schema.state)}}}`
|
|
177
|
+
// console.log(`Responding with this JSON RPC response: ${jsonResponse}`)
|
|
178
|
+
|
|
179
|
+
// Encrypt the string with the peers public key.
|
|
180
|
+
const peerData = thisNode.peerData.filter(x => x.from === from)
|
|
181
|
+
const payload = await this.encryption.encryptMsg(
|
|
182
|
+
peerData[0],
|
|
183
|
+
jsonResponse
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
await this.messaging.sendMsg(from, payload, thisNode)
|
|
187
|
+
|
|
188
|
+
return true
|
|
189
|
+
|
|
190
|
+
//
|
|
191
|
+
} else if (data.id.includes('metrics') && data.result && data.result.method === 'about') {
|
|
192
|
+
// Response received.
|
|
193
|
+
|
|
194
|
+
// console.log('JSON RPC /about response aquired.')
|
|
195
|
+
|
|
196
|
+
// This event is handled by the about-adapter.js. It measures the
|
|
197
|
+
// latency between peers.
|
|
198
|
+
this.about.relayMetricsReceived(decryptedStr)
|
|
199
|
+
|
|
200
|
+
return data.result.value
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// This is not an /about JSON RPC query.
|
|
204
|
+
// console.log('JSON RPC is not targeting the /about endpoint')
|
|
205
|
+
return false
|
|
206
|
+
} catch (err) {
|
|
207
|
+
console.error('Error in captureMetrics: ', err)
|
|
208
|
+
return false
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Attempts to parse data coming in from a pubsub channel. It is assumed that
|
|
213
|
+
// the data is a string in JSON format. If it isn't, parsing will throw an
|
|
214
|
+
// error and the message will be ignored.
|
|
215
|
+
async parsePubsubMessage (msg, handler, thisNode) {
|
|
216
|
+
try {
|
|
217
|
+
// console.log('message data: ', msg.data)
|
|
218
|
+
// console.log('parsePubsubMessage msg: ', msg)
|
|
219
|
+
// console.log('thisNode: ', thisNode)
|
|
220
|
+
|
|
221
|
+
const thisNodeId = thisNode.ipfsId
|
|
222
|
+
|
|
223
|
+
// Get data about the message.
|
|
224
|
+
const from = msg.from.toString()
|
|
225
|
+
const channel = msg.topic
|
|
226
|
+
|
|
227
|
+
// Used for debugging.
|
|
228
|
+
this.log.statusLog(
|
|
229
|
+
2,
|
|
230
|
+
`Broadcast pubsub message recieved from ${from} on channel ${channel}`
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
// Ignore this message if it originated from this IPFS node.
|
|
234
|
+
if (from === thisNodeId) return true
|
|
235
|
+
|
|
236
|
+
// The data on browsers comes through as a uint8array, and on node.js
|
|
237
|
+
// implementiong it comes through as a string. Browsers need to
|
|
238
|
+
// convert the message from a uint8array to a string.
|
|
239
|
+
// console.log('this.nodeType: ', this.nodeType)
|
|
240
|
+
if (thisNode.type === 'browser' || this.nodeType === 'external') {
|
|
241
|
+
// console.log('Node type is browser or external')
|
|
242
|
+
msg.data = new TextDecoder('utf-8').decode(msg.data)
|
|
243
|
+
}
|
|
244
|
+
// console.log('msg.data: ', JSON.parse(msg.data.toString()))
|
|
245
|
+
|
|
246
|
+
let data
|
|
247
|
+
try {
|
|
248
|
+
// Parse the data into a JSON object. It starts as a Buffer that needs
|
|
249
|
+
// to be converted to a string, then parsed to a JSON object.
|
|
250
|
+
// For some reason I have to JSON parse it twice. Not sure why.
|
|
251
|
+
data = JSON.parse(JSON.parse(msg.data.toString()))
|
|
252
|
+
// console.log('data: ', data)
|
|
253
|
+
} catch (err) {
|
|
254
|
+
throw new Error(`Failed to parse JSON in message from ${from} in pubsub channel ${channel}.`)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const retObj = { from, channel, data }
|
|
258
|
+
// console.log(`new pubsub message received: ${JSON.stringify(retObj, null, 2)}`)
|
|
259
|
+
|
|
260
|
+
// Hand retObj to the callback.
|
|
261
|
+
handler(retObj)
|
|
262
|
+
|
|
263
|
+
return true
|
|
264
|
+
} catch (err) {
|
|
265
|
+
// console.error('Error in parsePubsubMessage(): ', err.message)
|
|
266
|
+
this.log.statusLog(2, `Error in parsePubsubMessage(): ${err.message}`)
|
|
267
|
+
// Do not throw an error. This is a top-level function.
|
|
268
|
+
|
|
269
|
+
return false
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// module.exports = PubsubAdapter
|
|
275
|
+
export default PubsubAdapter
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/*
|
|
2
|
+
A library for broadcasting messages over pubsub and managing 'lost messages'.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Global npm libraries
|
|
6
|
+
import { v4 as uid } from 'uuid'
|
|
7
|
+
|
|
8
|
+
// Local libraries
|
|
9
|
+
import ResendMsg from './resend-msg.js'
|
|
10
|
+
|
|
11
|
+
// Constants
|
|
12
|
+
const TIME_BETWEEN_RETRIES = 5000 // time in milliseconds
|
|
13
|
+
// const RETRY_LIMIT = 3
|
|
14
|
+
|
|
15
|
+
class Messaging {
|
|
16
|
+
constructor (localConfig = {}) {
|
|
17
|
+
// Dependency Injection
|
|
18
|
+
this.ipfs = localConfig.ipfsAdapter
|
|
19
|
+
if (!this.ipfs) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
'Instance of IPFS adapter required when instantiating Messaging Adapter.'
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
this.log = localConfig.log
|
|
25
|
+
if (!this.log) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
'A status log handler function required when instantitating Messaging Adapter'
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
this.encryption = localConfig.encryption
|
|
31
|
+
if (!this.encryption) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
'An instance of the encryption Adapter must be passed when instantiating the Messaging Adapter library.'
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 'embedded' node type used as default, will use embedded js-ipfs.
|
|
38
|
+
// Alternative is 'external' which will use ipfs-http-client to control an
|
|
39
|
+
// external IPFS node.
|
|
40
|
+
this.nodeType = localConfig.nodeType
|
|
41
|
+
if (!this.nodeType) {
|
|
42
|
+
// console.log('No node type specified. Assuming embedded js-ipfs.')
|
|
43
|
+
this.nodeType = 'embedded'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Encapsulate dependencies
|
|
47
|
+
this.uid = uid
|
|
48
|
+
|
|
49
|
+
// State
|
|
50
|
+
this.msgQueue = []
|
|
51
|
+
// Cache to store UUIDs of processed messages. Used to prevent duplicate
|
|
52
|
+
// processing.
|
|
53
|
+
this.msgCache = []
|
|
54
|
+
this.MSG_CACHE_SIZE = 30
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Send a message to a peer
|
|
58
|
+
// The message is added to a queue that will automatically track ACK messages
|
|
59
|
+
// and re-send the message if an ACK message is not received.
|
|
60
|
+
async sendMsg (receiver, payload, thisNode) {
|
|
61
|
+
try {
|
|
62
|
+
// console.log(`sendMsg thisNode: `, thisNode)
|
|
63
|
+
|
|
64
|
+
// Generate a message object
|
|
65
|
+
const sender = thisNode.ipfsId
|
|
66
|
+
const inMsgObj = {
|
|
67
|
+
sender,
|
|
68
|
+
receiver,
|
|
69
|
+
payload
|
|
70
|
+
}
|
|
71
|
+
const msgObj = this.generateMsgObj(inMsgObj)
|
|
72
|
+
|
|
73
|
+
// const msgId = msgObj.uuid
|
|
74
|
+
|
|
75
|
+
// Send message
|
|
76
|
+
await this.publishToPubsubChannel(receiver, msgObj)
|
|
77
|
+
|
|
78
|
+
// Add the message to the retry queue
|
|
79
|
+
this.addMsgToQueue(msgObj)
|
|
80
|
+
|
|
81
|
+
return true
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error('Error in messaging.js/sendMsg()')
|
|
84
|
+
throw err
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Publish an ACK (acknowldge) message. Does not wait for any reply. Just fires
|
|
89
|
+
// and returns.
|
|
90
|
+
async sendAck (data, thisNode) {
|
|
91
|
+
try {
|
|
92
|
+
const ackMsgObj = await this.generateAckMsg(data, thisNode)
|
|
93
|
+
|
|
94
|
+
// Send Ack message
|
|
95
|
+
await this.publishToPubsubChannel(data.sender, ackMsgObj)
|
|
96
|
+
|
|
97
|
+
return true
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error('Error in sendAck()')
|
|
100
|
+
throw err
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// A handler function that is called when a new message is recieved on the
|
|
105
|
+
// pubsub channel for this IPFS node. It does the following:
|
|
106
|
+
// - Decrypts the message.
|
|
107
|
+
// - Sends an ACK message to the sender of the message.
|
|
108
|
+
// - Returns an object containing the message and metadata.
|
|
109
|
+
async handleIncomingData (msg, thisNode) {
|
|
110
|
+
try {
|
|
111
|
+
// console.log('handleIncomingData() msg: ', msg)
|
|
112
|
+
// console.log('thisNode: ', thisNode)
|
|
113
|
+
|
|
114
|
+
const thisNodeId = thisNode.ipfsId
|
|
115
|
+
|
|
116
|
+
// Get data about the message.
|
|
117
|
+
const from = msg.from.toString()
|
|
118
|
+
const channel = msg.topic
|
|
119
|
+
|
|
120
|
+
// Ignore this message if it originated from this IPFS node.
|
|
121
|
+
if (from === thisNodeId) return false
|
|
122
|
+
|
|
123
|
+
// The data on browsers comes through as a uint8array, and on node.js
|
|
124
|
+
// implementiong it comes through as a string when using js-ipfs. But when
|
|
125
|
+
// using ipfs-http-client with an external go-ipfs node, it comes thorugh
|
|
126
|
+
// as a uint8Array too. Browsers need to
|
|
127
|
+
// convert the message from a uint8array to a string.
|
|
128
|
+
if (thisNode.type === 'browser' || this.nodeType === 'external') {
|
|
129
|
+
// console.log('Node type is browser')
|
|
130
|
+
msg.data = new TextDecoder('utf-8').decode(msg.data)
|
|
131
|
+
}
|
|
132
|
+
// console.log('msg.data: ', msg.data)
|
|
133
|
+
|
|
134
|
+
// Parse the data into a JSON object. It starts as a Buffer that needs
|
|
135
|
+
// to be converted to a string, then parsed to a JSON object.
|
|
136
|
+
// For some reason I have to JSON parse it twice. Not sure why.
|
|
137
|
+
const data = JSON.parse(msg.data.toString())
|
|
138
|
+
// console.log('message data: ', data)
|
|
139
|
+
|
|
140
|
+
// Decrypt the payload
|
|
141
|
+
const decryptedPayload = await this.encryption.decryptMsg(data.payload)
|
|
142
|
+
// console.log(`decrypted payload: ${decryptedPayload}`)
|
|
143
|
+
|
|
144
|
+
// Filter ACK messages from other messages
|
|
145
|
+
if (decryptedPayload.includes('"apiName":"ACK"')) {
|
|
146
|
+
this.log.statusLog(2, `ACK message received for ${data.uuid}`)
|
|
147
|
+
|
|
148
|
+
this.delMsgFromQueue(data)
|
|
149
|
+
|
|
150
|
+
return false
|
|
151
|
+
} else {
|
|
152
|
+
this.log.statusLog(
|
|
153
|
+
2,
|
|
154
|
+
`Private pubsub message recieved from ${from} on channel ${channel} with message ID ${data.uuid}`
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Debug logs from an 'about' JSON RPC command.
|
|
159
|
+
if (decryptedPayload.includes('"id"')) {
|
|
160
|
+
const obj = JSON.parse(decryptedPayload)
|
|
161
|
+
// console.log(`debug log obj: ${JSON.stringify(obj, null, 2)}`)
|
|
162
|
+
|
|
163
|
+
if (obj.result) {
|
|
164
|
+
// Response
|
|
165
|
+
this.log.statusLog(2, `Message ID ${data.uuid} contains RPC response with ID ${obj.id}`)
|
|
166
|
+
} else {
|
|
167
|
+
// Request
|
|
168
|
+
this.log.statusLog(2, `Message ID ${data.uuid} contains RPC request with ID ${obj.id}`)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Send an ACK message
|
|
173
|
+
await this.sendAck(data, thisNode)
|
|
174
|
+
|
|
175
|
+
// Ignore message if its already been processed.
|
|
176
|
+
const alreadyProcessed = this._checkIfAlreadyProcessed(data.uuid)
|
|
177
|
+
if (alreadyProcessed) {
|
|
178
|
+
console.log(`Message ${data.uuid} already processed`)
|
|
179
|
+
return false
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Replace the encrypted data with the decrypted data.
|
|
183
|
+
data.payload = decryptedPayload
|
|
184
|
+
|
|
185
|
+
const retObj = { from, channel, data }
|
|
186
|
+
// console.log(
|
|
187
|
+
// `new pubsub message received: ${JSON.stringify(retObj, null, 2)}`
|
|
188
|
+
// )
|
|
189
|
+
|
|
190
|
+
return retObj
|
|
191
|
+
} catch (err) {
|
|
192
|
+
console.error('Error in handleIncomingData(): ', err)
|
|
193
|
+
// throw err
|
|
194
|
+
// Do not throw an error. This is a top-level function called by an Interval.
|
|
195
|
+
return false
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Generate a message object with UUID and timestamp.
|
|
200
|
+
generateMsgObj (inMsgObj = {}) {
|
|
201
|
+
try {
|
|
202
|
+
const { sender, receiver, payload } = inMsgObj
|
|
203
|
+
|
|
204
|
+
// Input validation
|
|
205
|
+
if (!sender) {
|
|
206
|
+
throw new Error('Sender required when calling generateMsgObj()')
|
|
207
|
+
}
|
|
208
|
+
if (!receiver) {
|
|
209
|
+
throw new Error('Receiver required when calling generateMsgObj()')
|
|
210
|
+
}
|
|
211
|
+
if (!payload) {
|
|
212
|
+
throw new Error('Payload required when calling generateMsgObj()')
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const uuid = this.uid()
|
|
216
|
+
|
|
217
|
+
const now = new Date()
|
|
218
|
+
const timestamp = now.toISOString()
|
|
219
|
+
|
|
220
|
+
const outMsgObj = {
|
|
221
|
+
timestamp,
|
|
222
|
+
uuid,
|
|
223
|
+
sender,
|
|
224
|
+
receiver,
|
|
225
|
+
payload
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return outMsgObj
|
|
229
|
+
} catch (err) {
|
|
230
|
+
console.log('Error in generateMsgObj()')
|
|
231
|
+
throw err
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Generate an ACK (acknowledge) message.
|
|
236
|
+
async generateAckMsg (data, thisNode) {
|
|
237
|
+
try {
|
|
238
|
+
// console.log('thisNode: ', thisNode)
|
|
239
|
+
|
|
240
|
+
// The sender of the original messages is the receiver of the ACK message.
|
|
241
|
+
const receiver = data.sender
|
|
242
|
+
const uuid = data.uuid
|
|
243
|
+
|
|
244
|
+
const ackMsg = {
|
|
245
|
+
apiName: 'ACK'
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const peerData = thisNode.peerData.filter(x => x.from === receiver)
|
|
249
|
+
|
|
250
|
+
// Encrypt the string with the peers public key.
|
|
251
|
+
const payload = await this.encryption.encryptMsg(
|
|
252
|
+
peerData[0],
|
|
253
|
+
JSON.stringify(ackMsg)
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
const sender = thisNode.ipfsId
|
|
257
|
+
|
|
258
|
+
const inMsgObj = {
|
|
259
|
+
sender,
|
|
260
|
+
receiver,
|
|
261
|
+
payload
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const outMsgObj = this.generateMsgObj(inMsgObj)
|
|
265
|
+
|
|
266
|
+
// Replace the message UUID with the UUID from the original message.
|
|
267
|
+
outMsgObj.uuid = uuid
|
|
268
|
+
|
|
269
|
+
this.log.statusLog(
|
|
270
|
+
2,
|
|
271
|
+
`Sending ACK message for ID ${uuid}`
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
return outMsgObj
|
|
275
|
+
} catch (err) {
|
|
276
|
+
console.error('Error in generateAckMsg()')
|
|
277
|
+
throw err
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Converts an input string to a Buffer and then broadcasts it to the given
|
|
282
|
+
// pubsub room.
|
|
283
|
+
async publishToPubsubChannel (chanName, msgObj) {
|
|
284
|
+
try {
|
|
285
|
+
const msgBuf = Buffer.from(JSON.stringify(msgObj))
|
|
286
|
+
|
|
287
|
+
// Publish the message to the pubsub channel.
|
|
288
|
+
// await this.ipfs.ipfs.pubsub.publish(chanName, msgBuf)
|
|
289
|
+
await this.ipfs.ipfs.libp2p.services.pubsub.publish(chanName, msgBuf)
|
|
290
|
+
|
|
291
|
+
// console.log('msgObj: ', msgObj)
|
|
292
|
+
|
|
293
|
+
// Used for debugging.
|
|
294
|
+
if (msgObj.uuid) {
|
|
295
|
+
this.log.statusLog(
|
|
296
|
+
2,
|
|
297
|
+
`New message published to private channel ${chanName} with ID ${msgObj.uuid}`
|
|
298
|
+
)
|
|
299
|
+
} else {
|
|
300
|
+
this.log.statusLog(
|
|
301
|
+
2,
|
|
302
|
+
`New announcement message published to broadcast channel ${chanName}`
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return true
|
|
307
|
+
} catch (err) {
|
|
308
|
+
console.error('Error in publishToPubsubChannel()')
|
|
309
|
+
throw err
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Checks the UUID to see if the message has already been processed. Returns
|
|
314
|
+
// true if the UUID exists in the list of processed messages.
|
|
315
|
+
_checkIfAlreadyProcessed (uuid) {
|
|
316
|
+
// Check if the hash is in the array of already processed message.
|
|
317
|
+
const alreadyProcessed = this.msgCache.includes(uuid)
|
|
318
|
+
|
|
319
|
+
// Update the msgCache if this is a new message.
|
|
320
|
+
if (!alreadyProcessed) {
|
|
321
|
+
// Add the uuid to the array.
|
|
322
|
+
this.msgCache.push(uuid)
|
|
323
|
+
|
|
324
|
+
// If the array is at its max size, then remove the oldest element.
|
|
325
|
+
if (this.msgCache.length > this.MSG_CACHE_SIZE) {
|
|
326
|
+
this.msgCache.shift()
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return alreadyProcessed
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Stops the Interval Timer and deletes a message from the queue when an
|
|
334
|
+
// ACK message is received.
|
|
335
|
+
delMsgFromQueue (msgObj) {
|
|
336
|
+
// Loop through the message queue
|
|
337
|
+
for (let i = 0; i < this.msgQueue.length; i++) {
|
|
338
|
+
const thisMsg = this.msgQueue[i]
|
|
339
|
+
|
|
340
|
+
// Find the matching entry.
|
|
341
|
+
if (msgObj.uuid === thisMsg.uuid) {
|
|
342
|
+
// console.log(`thisMsg: `, thisMsg)
|
|
343
|
+
|
|
344
|
+
// Stop the Interval
|
|
345
|
+
try {
|
|
346
|
+
clearInterval(thisMsg.intervalHandle)
|
|
347
|
+
// console.log('Interval stopped')
|
|
348
|
+
} catch (err) { /* exit quietly */ }
|
|
349
|
+
|
|
350
|
+
// Delete the entry from the msgQueue array.
|
|
351
|
+
this.msgQueue.splice(i, 1)
|
|
352
|
+
break
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return true
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Adds a message object to the message queue. Starts an interval timer that
|
|
360
|
+
// will repeat the message periodically until an ACK message is received.
|
|
361
|
+
addMsgToQueue (msgObj = {}) {
|
|
362
|
+
try {
|
|
363
|
+
msgObj.retryCnt = 1
|
|
364
|
+
|
|
365
|
+
const resendMsg = new ResendMsg({ msgObj, msgLib: this })
|
|
366
|
+
|
|
367
|
+
// Start interval for repeating message
|
|
368
|
+
// const intervalHandle = setInterval(function () {
|
|
369
|
+
// _this.resendMsg(msgObj)
|
|
370
|
+
// }, TIME_BETWEEN_RETRIES)
|
|
371
|
+
|
|
372
|
+
const intervalHandle = setInterval(resendMsg.resend, TIME_BETWEEN_RETRIES)
|
|
373
|
+
|
|
374
|
+
// Add interval handle to message object.
|
|
375
|
+
msgObj.intervalHandle = intervalHandle
|
|
376
|
+
|
|
377
|
+
// Add message object to the queue.
|
|
378
|
+
this.msgQueue.push(msgObj)
|
|
379
|
+
|
|
380
|
+
return msgObj
|
|
381
|
+
} catch (err) {
|
|
382
|
+
console.error('Error in addMsgToQueue')
|
|
383
|
+
throw err
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// module.exports = Messaging
|
|
389
|
+
export default Messaging
|