hive-p2p 1.0.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/LICENSE +674 -0
- package/README.md +512 -0
- package/browser.mjs +4 -0
- package/core/arbiter.mjs +125 -0
- package/core/crypto-codex.mjs +249 -0
- package/core/gossip.mjs +159 -0
- package/core/ice-offer-manager.mjs +175 -0
- package/core/node-services.mjs +126 -0
- package/core/node.mjs +146 -0
- package/core/parameters.mjs +142 -0
- package/core/peer-store.mjs +252 -0
- package/core/route-builder.mjs +176 -0
- package/core/topologist.mjs +254 -0
- package/core/unicast.mjs +155 -0
- package/index.mjs +4 -0
- package/libs/argon2-ES6.min.mjs +1 -0
- package/libs/socket.io-4-8-1.min.js +7 -0
- package/libs/three-4.5.min.js +6 -0
- package/libs/xxhash32.mjs +65 -0
- package/package.json +44 -0
- package/services/clock.mjs +144 -0
- package/services/converter.mjs +64 -0
- package/services/cryptos.mjs +83 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { CLOCK, SIMULATION, NODE, IDENTITY, GOSSIP, UNICAST, LOG_CSS } from './parameters.mjs';
|
|
2
|
+
import { GossipMessage } from './gossip.mjs';
|
|
3
|
+
import { DirectMessage, ReroutedDirectMessage } from './unicast.mjs';
|
|
4
|
+
import { Converter } from '../services/converter.mjs';
|
|
5
|
+
import { ed25519, Argon2Unified } from '../services/cryptos.mjs';
|
|
6
|
+
|
|
7
|
+
export class CryptoCodex {
|
|
8
|
+
argon2 = new Argon2Unified();
|
|
9
|
+
converter = new Converter();
|
|
10
|
+
AVOID_CRYPTO = false;
|
|
11
|
+
verbose = NODE.DEFAULT_VERBOSE;
|
|
12
|
+
/** @type {string} */ id;
|
|
13
|
+
/** @type {Uint8Array} */ publicKey;
|
|
14
|
+
/** @type {Uint8Array} */ privateKey;
|
|
15
|
+
|
|
16
|
+
/** @param {string} [nodeId] If provided: used to generate a fake keypair > disable crypto operations */
|
|
17
|
+
constructor(nodeId) {
|
|
18
|
+
if (!nodeId) return; // IF NOT PROVIDED: generate() should be called.
|
|
19
|
+
this.AVOID_CRYPTO = true;
|
|
20
|
+
this.id = nodeId.padEnd(IDENTITY.ID_LENGTH, ' ').slice(0, IDENTITY.ID_LENGTH);
|
|
21
|
+
this.privateKey = new Uint8Array(32).fill(0); this.publicKey = new Uint8Array(32).fill(0);
|
|
22
|
+
const idBytes = new TextEncoder().encode(this.id); // use nodeId to create a fake public key
|
|
23
|
+
for (let i = 0; i < IDENTITY.ID_LENGTH; i++) this.publicKey[i] = idBytes[i];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// IDENTITY
|
|
27
|
+
/** @param {string} id Check the first character against the PUBLIC_PREFIX */
|
|
28
|
+
static isPublicNode(id) { return (IDENTITY.ARE_IDS_HEX ? Converter.hexToBits(id[0]) : id).startsWith(IDENTITY.PUBLIC_PREFIX); }
|
|
29
|
+
/** @param {string} id */
|
|
30
|
+
get idLength() { return IDENTITY.ARE_IDS_HEX ? IDENTITY.ID_LENGTH / 2 : IDENTITY.ID_LENGTH; }
|
|
31
|
+
isPublicNode(id) { return CryptoCodex.isPublicNode(id); }
|
|
32
|
+
/** @param {boolean} asPublicNode @param {Uint8Array} [seed] The privateKey. DON'T USE IN SIMULATION */
|
|
33
|
+
async generate(asPublicNode, seed) { // Generate Ed25519 keypair cross-platform | set id only for simulator
|
|
34
|
+
if (this.nodeId) return;
|
|
35
|
+
await this.#generateAntiSybilIdentity(seed, asPublicNode);
|
|
36
|
+
if (!this.id) throw new Error('Failed to generate identity');
|
|
37
|
+
}
|
|
38
|
+
/** Check if the pubKey meets the difficulty using Argon2 derivation @param {Uint8Array} publicKey */
|
|
39
|
+
async pubkeyDifficultyCheck(publicKey) {
|
|
40
|
+
if (this.AVOID_CRYPTO || !IDENTITY.DIFFICULTY) return true;
|
|
41
|
+
const { bitsString } = await this.argon2.hash(publicKey, 'HiveP2P', IDENTITY.ARGON2_MEM) || {};
|
|
42
|
+
if (bitsString && bitsString.startsWith('0'.repeat(IDENTITY.DIFFICULTY))) return true;
|
|
43
|
+
}
|
|
44
|
+
#idFromPublicKey(publicKey) {
|
|
45
|
+
if (IDENTITY.ARE_IDS_HEX) return this.converter.bytesToHex(publicKey.slice(0, this.idLength), IDENTITY.ID_LENGTH);
|
|
46
|
+
return this.converter.bytesToString(publicKey.slice(0, IDENTITY.ID_LENGTH));
|
|
47
|
+
}
|
|
48
|
+
/** @param {Uint8Array} seed The privateKey. @param {boolean} asPublicNode */
|
|
49
|
+
async #generateAntiSybilIdentity(seed, asPublicNode) {
|
|
50
|
+
const maxIterations = (2 ** IDENTITY.DIFFICULTY) * 100; // avoid infinite loop
|
|
51
|
+
for (let i = 0; i < maxIterations; i++) { // avoid infinite loop
|
|
52
|
+
const { secretKey, publicKey } = ed25519.keygen(seed);
|
|
53
|
+
const id = this.#idFromPublicKey(publicKey);
|
|
54
|
+
if (asPublicNode && !this.isPublicNode(id)) continue; // Check prefix
|
|
55
|
+
if (!asPublicNode && this.isPublicNode(id)) continue; // Check prefix
|
|
56
|
+
if (!await this.pubkeyDifficultyCheck(publicKey)) continue; // Check difficulty
|
|
57
|
+
|
|
58
|
+
this.id = id;
|
|
59
|
+
this.privateKey = secretKey; this.publicKey = publicKey;
|
|
60
|
+
if (this.verbose > 2) console.log(`%cNode generated id: ${this.id} (isPublic: ${asPublicNode}, difficulty: ${IDENTITY.DIFFICULTY}) after ${((i + 1) / 2).toFixed(1)} iterations`, LOG_CSS.CRYPTO_CODEX);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (this.verbose > 0) console.log(`%cFAILED to generate id after ${maxIterations} iterations. Try lowering the difficulty.`, LOG_CSS.CRYPTO_CODEX);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// MESSSAGE CREATION (SERIALIZATION AND SIGNATURE INCLUDED)
|
|
67
|
+
signBufferViewAndAppendSignature(bufferView, privateKey, signaturePosition = bufferView.length - IDENTITY.SIGNATURE_LENGTH) {
|
|
68
|
+
if (this.AVOID_CRYPTO) return;
|
|
69
|
+
const dataToSign = bufferView.subarray(0, signaturePosition);
|
|
70
|
+
bufferView.set(ed25519.sign(dataToSign, privateKey), signaturePosition);
|
|
71
|
+
}
|
|
72
|
+
/** @param {string} topic @param {string | Uint8Array | Object} data @param {number} [HOPS] @param {string[]} route @param {string[]} [neighbors] */
|
|
73
|
+
createGossipMessage(topic, data, HOPS = 3, neighbors = [], timestamp = CLOCK.time) {
|
|
74
|
+
const MARKER = GOSSIP.MARKERS_BYTES[topic];
|
|
75
|
+
if (MARKER === undefined) throw new Error(`Failed to create gossip message: unknown topic '${topic}'.`);
|
|
76
|
+
|
|
77
|
+
const neighborsBytes = this.#idsToBytes(neighbors);
|
|
78
|
+
const { dataCode, dataBytes } = this.#dataToBytes(data);
|
|
79
|
+
const totalBytes = 1 + 1 + 1 + 8 + 4 + 32 + neighborsBytes.length + dataBytes.length + IDENTITY.SIGNATURE_LENGTH + 1;
|
|
80
|
+
const buffer = new ArrayBuffer(totalBytes);
|
|
81
|
+
const bufferView = new Uint8Array(buffer);
|
|
82
|
+
this.#setBufferHeader(bufferView, MARKER, dataCode, neighbors.length, timestamp, dataBytes, this.publicKey);
|
|
83
|
+
|
|
84
|
+
bufferView.set(neighborsBytes, 47); // X bytes for neighbors
|
|
85
|
+
bufferView.set(dataBytes, 47 + neighborsBytes.length); // X bytes for data
|
|
86
|
+
bufferView.set([Math.min(255, HOPS)], totalBytes - 1); // 1 byte for HOPS (Unsigned)
|
|
87
|
+
this.signBufferViewAndAppendSignature(bufferView, this.privateKey, totalBytes - IDENTITY.SIGNATURE_LENGTH - 1);
|
|
88
|
+
return bufferView;
|
|
89
|
+
}
|
|
90
|
+
/** @param {Uint8Array} serializedMessage */
|
|
91
|
+
decrementGossipHops(serializedMessage) { // Here we just need to decrement the HOPS value => last byte of the message
|
|
92
|
+
const clone = new Uint8Array(serializedMessage); // avoid modifying the original message
|
|
93
|
+
const hops = serializedMessage[serializedMessage.length - 1];
|
|
94
|
+
clone[serializedMessage.length - 1] = Math.max(0, hops - 1);
|
|
95
|
+
return clone;
|
|
96
|
+
}
|
|
97
|
+
/** @param {string} type @param {string | Uint8Array | Object} data @param {string[]} route @param {string[]} [neighbors] */
|
|
98
|
+
createUnicastMessage(type, data, route, neighbors = [], timestamp = CLOCK.time) {
|
|
99
|
+
const MARKER = UNICAST.MARKERS_BYTES[type];
|
|
100
|
+
if (MARKER === undefined) throw new Error(`Failed to create unicast message: unknown type '${type}'.`);
|
|
101
|
+
if (route.length < 2) throw new Error('Failed to create unicast message: route must have at least 2 nodes (next hop and target).');
|
|
102
|
+
if (route.length > UNICAST.MAX_HOPS) throw new Error(`Failed to create unicast message: route exceeds max hops (${UNICAST.MAX_HOPS}).`);
|
|
103
|
+
|
|
104
|
+
const neighborsBytes = this.#idsToBytes(neighbors);
|
|
105
|
+
const { dataCode, dataBytes } = this.#dataToBytes(data);
|
|
106
|
+
const routeBytes = this.#idsToBytes(route);
|
|
107
|
+
const totalBytes = 1 + 1 + 1 + 8 + 4 + 32 + neighborsBytes.length + 1 + routeBytes.length + dataBytes.length + IDENTITY.SIGNATURE_LENGTH;
|
|
108
|
+
const buffer = new ArrayBuffer(totalBytes);
|
|
109
|
+
const bufferView = new Uint8Array(buffer);
|
|
110
|
+
this.#setBufferHeader(bufferView, MARKER, dataCode, neighbors.length, timestamp, dataBytes, this.publicKey);
|
|
111
|
+
|
|
112
|
+
const NDBL = neighborsBytes.length + dataBytes.length;
|
|
113
|
+
bufferView.set(neighborsBytes, 47); // X bytes for neighbors
|
|
114
|
+
bufferView.set(dataBytes, 47 + neighborsBytes.length); // X bytes for data
|
|
115
|
+
bufferView.set([route.length], 47 + NDBL); // 1 byte for route length
|
|
116
|
+
bufferView.set(routeBytes, 47 + 1 + NDBL); // X bytes for route
|
|
117
|
+
|
|
118
|
+
this.signBufferViewAndAppendSignature(bufferView, this.privateKey, totalBytes - IDENTITY.SIGNATURE_LENGTH);
|
|
119
|
+
return bufferView;
|
|
120
|
+
}
|
|
121
|
+
/** @param {Uint8Array} serialized @param {string[]} newRoute */
|
|
122
|
+
createReroutedUnicastMessage(serialized, newRoute) {
|
|
123
|
+
if (newRoute.length < 2) throw new Error('Failed to create rerouted unicast message: route must have at least 2 nodes (next hop and target).');
|
|
124
|
+
if (newRoute.length > UNICAST.MAX_HOPS) throw new Error(`Failed to create rerouted unicast message: route exceeds max hops (${UNICAST.MAX_HOPS}).`);
|
|
125
|
+
|
|
126
|
+
const routeBytesArray = newRoute.map(id => this.converter.stringToBytes(id));
|
|
127
|
+
const totalBytes = serialized.length + 32 + (IDENTITY.ID_LENGTH * routeBytesArray.length) + IDENTITY.SIGNATURE_LENGTH;
|
|
128
|
+
const buffer = new ArrayBuffer(totalBytes);
|
|
129
|
+
const bufferView = new Uint8Array(buffer);
|
|
130
|
+
bufferView.set(serialized, 0); // original serialized message
|
|
131
|
+
bufferView.set(this.publicKey, serialized.length); // 32 bytes for new public key
|
|
132
|
+
for (let i = 0; i < routeBytesArray.length; i++) bufferView.set(routeBytesArray[i], serialized.length + 32 + (i * IDENTITY.ID_LENGTH)); // new route
|
|
133
|
+
|
|
134
|
+
this.signBufferViewAndAppendSignature(bufferView, this.privateKey, totalBytes - IDENTITY.SIGNATURE_LENGTH);
|
|
135
|
+
return bufferView;
|
|
136
|
+
}
|
|
137
|
+
/** @param {string[]} ids */
|
|
138
|
+
#idsToBytes(ids) {
|
|
139
|
+
if (IDENTITY.ARE_IDS_HEX) return this.converter.hexToBytes(ids.join(''), IDENTITY.ID_LENGTH * ids.length);
|
|
140
|
+
return this.converter.stringToBytes(ids.join(''));
|
|
141
|
+
}
|
|
142
|
+
/** @param {string | Uint8Array | Object} data */
|
|
143
|
+
#dataToBytes(data) { // typeCodes: 1=string, 2=Uint8Array, 3=JSON
|
|
144
|
+
if (typeof data === 'string') return { dataCode: 1, dataBytes: this.converter.stringToBytes(data) };
|
|
145
|
+
if (data instanceof Uint8Array) return { dataCode: 2, dataBytes: data };
|
|
146
|
+
return { dataCode: 3, dataBytes: this.converter.stringToBytes(JSON.stringify(data)) };
|
|
147
|
+
}
|
|
148
|
+
/** @param {Uint8Array} bufferView @param {number} marker @param {number} dataCode @param {number} neighborsCount @param {number} timestamp @param {Uint8Array} dataBytes @param {Uint8Array} publicKey */
|
|
149
|
+
#setBufferHeader(bufferView, marker, dataCode, neighborsCount, timestamp, dataBytes, publicKey) {
|
|
150
|
+
const timestampBytes = this.converter.numberTo8Bytes(timestamp);
|
|
151
|
+
const dataLengthBytes = this.converter.numberTo4Bytes(dataBytes.length);
|
|
152
|
+
bufferView.set([marker], 0); // 1 byte for marker
|
|
153
|
+
bufferView.set([dataCode], 1); // 1 byte for data type code
|
|
154
|
+
bufferView.set([neighborsCount], 2); // 1 byte for neighbors length
|
|
155
|
+
bufferView.set(timestampBytes, 3); // 8 bytes for timestamp
|
|
156
|
+
bufferView.set(dataLengthBytes, 11); // 4 bytes for data length
|
|
157
|
+
bufferView.set(publicKey, 15); // 32 bytes for pubkey
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// MESSSAGE READING (DESERIALIZATION AND SIGNATURE VERIFICATION INCLUDED)
|
|
161
|
+
/** @param {Uint8Array} publicKey @param {Uint8Array} dataToVerify @param {Uint8Array} signature */
|
|
162
|
+
verifySignature(publicKey, dataToVerify, signature) {
|
|
163
|
+
if (this.AVOID_CRYPTO) return true;
|
|
164
|
+
return ed25519.verify(dataToVerify, signature, publicKey);
|
|
165
|
+
}
|
|
166
|
+
/** @param {Uint8Array} bufferView */
|
|
167
|
+
readBufferHeader(bufferView, readAssociatedId = true) {
|
|
168
|
+
const marker = bufferView[0]; // 1 byte for marker
|
|
169
|
+
const dataCode = bufferView[1]; // 1 byte for data type code
|
|
170
|
+
const neighborsCount = bufferView[2]; // 1 byte for neighbors length
|
|
171
|
+
const tBytes = bufferView.slice(3, 11); // 8 bytes for timestamp
|
|
172
|
+
const lBytes = bufferView.slice(11, 15); // 4 bytes for data length
|
|
173
|
+
const pubkey = bufferView.slice(15, 47); // 32 bytes for pubkey
|
|
174
|
+
const associatedId = readAssociatedId ? this.#idFromPublicKey(pubkey) : null;
|
|
175
|
+
const neighLength = neighborsCount * this.idLength;
|
|
176
|
+
const timestamp = this.converter.bytes8ToNumber(tBytes);
|
|
177
|
+
const dataLength = this.converter.bytes4ToNumber(lBytes);
|
|
178
|
+
return { marker, dataCode, neighLength, timestamp, dataLength, pubkey, associatedId };
|
|
179
|
+
}
|
|
180
|
+
/** @param {Uint8Array | ArrayBuffer} serialized @return {GossipMessage | null } */
|
|
181
|
+
readGossipMessage(serialized) {
|
|
182
|
+
if (this.verbose > 3) console.log(`%creadGossipMessage ${serialized.byteLength} bytes`, LOG_CSS.CRYPTO_CODEX);
|
|
183
|
+
if (this.verbose > 3) console.log(`%c${serialized}`, LOG_CSS.CRYPTO_CODEX);
|
|
184
|
+
try { // 1, 1, 1, 8, 4, 32, X, 64, 1
|
|
185
|
+
const { marker, dataCode, neighLength, timestamp, dataLength, pubkey, associatedId } = this.readBufferHeader(serialized);
|
|
186
|
+
const topic = GOSSIP.MARKERS_BYTES[marker];
|
|
187
|
+
if (topic === undefined) throw new Error(`Failed to deserialize gossip message: unknown marker byte ${d[0]}.`);
|
|
188
|
+
const NDBL = neighLength + dataLength;
|
|
189
|
+
const neighbors = this.#bytesToIds(serialized.slice(47, 47 + neighLength));
|
|
190
|
+
const deserializedData = this.#bytesToData(dataCode, serialized.slice(47 + neighLength, 47 + NDBL));
|
|
191
|
+
const signatureStart = 47 + NDBL;
|
|
192
|
+
const signature = serialized.slice(signatureStart, signatureStart + IDENTITY.SIGNATURE_LENGTH);
|
|
193
|
+
const HOPS = serialized[serialized.length - 1];
|
|
194
|
+
const expectedEnd = signatureStart + IDENTITY.SIGNATURE_LENGTH + 1;
|
|
195
|
+
const senderId = associatedId;
|
|
196
|
+
return new GossipMessage(topic, timestamp, neighbors, HOPS, senderId, pubkey, deserializedData, signature, signatureStart, expectedEnd);
|
|
197
|
+
} catch (error) { if (this.verbose > 1) console.warn(`Error deserializing ${topic || 'unknown'} gossip message:`, error.stack); }
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
/** @param {Uint8Array | ArrayBuffer} serialized @return {DirectMessage | ReroutedDirectMessage | null} */
|
|
201
|
+
readUnicastMessage(serialized) {
|
|
202
|
+
if (this.verbose > 3) console.log(`%creadUnicastMessage ${serialized.byteLength} bytes`, LOG_CSS.CRYPTO_CODEX);
|
|
203
|
+
if (this.verbose > 3) console.log(`%c${serialized}`, LOG_CSS.CRYPTO_CODEX);
|
|
204
|
+
try { // 1, 1, 1, 8, 4, 32, X, 1, X, 64
|
|
205
|
+
const { marker, dataCode, neighLength, timestamp, dataLength, pubkey } = this.readBufferHeader(serialized, false);
|
|
206
|
+
const type = UNICAST.MARKERS_BYTES[marker];
|
|
207
|
+
if (type === undefined) throw new Error(`Failed to deserialize unicast message: unknown marker byte ${d[0]}.`);
|
|
208
|
+
const NDBL = neighLength + dataLength;
|
|
209
|
+
const neighbors = this.#bytesToIds(serialized.slice(47, 47 + neighLength));
|
|
210
|
+
const deserializedData = this.#bytesToData(dataCode, serialized.slice(47 + neighLength, 47 + NDBL));
|
|
211
|
+
const routeLength = serialized[47 + NDBL];
|
|
212
|
+
const routeBytesLength = routeLength * this.idLength;
|
|
213
|
+
const signatureStart = 47 + NDBL + 1 + routeBytesLength;
|
|
214
|
+
const routeBytes = serialized.slice(47 + NDBL + 1, signatureStart);
|
|
215
|
+
const route = this.#bytesToIds(routeBytes);
|
|
216
|
+
const initialMessageEnd = signatureStart + IDENTITY.SIGNATURE_LENGTH;
|
|
217
|
+
const signature = serialized.slice(signatureStart, initialMessageEnd);
|
|
218
|
+
const isPatched = (serialized.length > initialMessageEnd);
|
|
219
|
+
|
|
220
|
+
if (!isPatched) return new DirectMessage(type, timestamp, neighbors, route, pubkey, deserializedData, signature, signatureStart, initialMessageEnd);
|
|
221
|
+
|
|
222
|
+
const rerouterPubkey = serialized.slice(initialMessageEnd, initialMessageEnd + 32);
|
|
223
|
+
const newRoute = this.#bytesToIds(serialized.slice(initialMessageEnd + 32, serialized.length - IDENTITY.SIGNATURE_LENGTH));
|
|
224
|
+
const rerouterSignature = serialized.slice(serialized.length - IDENTITY.SIGNATURE_LENGTH);
|
|
225
|
+
return new ReroutedDirectMessage(type, timestamp, neighbors, route, pubkey, deserializedData, signature, rerouterPubkey, newRoute, rerouterSignature, serialized.length);
|
|
226
|
+
} catch (error) { if (this.verbose > 1) console.warn(`Error deserializing ${type || 'unknown'} unicast message:`, error.stack); }
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
/** @param {Uint8Array} serialized */
|
|
230
|
+
#bytesToIds(serialized) {
|
|
231
|
+
const ids = [];
|
|
232
|
+
const idLength = this.idLength;
|
|
233
|
+
if (serialized.length % idLength !== 0) throw new Error('Failed to parse ids: invalid serialized length.');
|
|
234
|
+
|
|
235
|
+
for (let i = 0; i < serialized.length / idLength; i++) {
|
|
236
|
+
const idBytes = serialized.slice(i * idLength, (i + 1) * idLength);
|
|
237
|
+
if (IDENTITY.ARE_IDS_HEX) ids.push(this.converter.bytesToHex(idBytes, IDENTITY.ID_LENGTH));
|
|
238
|
+
else ids.push(this.converter.bytesToString(idBytes));
|
|
239
|
+
}
|
|
240
|
+
return ids;
|
|
241
|
+
}
|
|
242
|
+
/** @param {1 | 2 | 3} dataCode @param {Uint8Array} dataBytes @return {string | Uint8Array | Object} */
|
|
243
|
+
#bytesToData(dataCode, dataBytes) {
|
|
244
|
+
if (dataCode === 1) return this.converter.bytesToString(dataBytes);
|
|
245
|
+
if (dataCode === 2) return dataBytes;
|
|
246
|
+
if (dataCode === 3) return JSON.parse(this.converter.bytesToString(dataBytes));
|
|
247
|
+
throw new Error(`Failed to parse data: unknown data code '${dataCode}'.`);
|
|
248
|
+
}
|
|
249
|
+
}
|
package/core/gossip.mjs
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { CLOCK, GOSSIP, DISCOVERY } from './parameters.mjs';
|
|
2
|
+
import { xxHash32 } from '../libs/xxhash32.mjs';
|
|
3
|
+
|
|
4
|
+
export class GossipMessage { // TYPE DEFINITION
|
|
5
|
+
topic = 'gossip';
|
|
6
|
+
timestamp;
|
|
7
|
+
neighborsList;
|
|
8
|
+
HOPS;
|
|
9
|
+
senderId;
|
|
10
|
+
pubkey;
|
|
11
|
+
data;
|
|
12
|
+
signature;
|
|
13
|
+
signatureStart; // position in the serialized message where the signature starts
|
|
14
|
+
expectedEnd; // expected length of the serialized message
|
|
15
|
+
|
|
16
|
+
/** @param {string} topic @param {number} timestamp @param {string[]} neighborsList @param {number} HOPS @param {string} senderId @param {string} pubkey @param {string | Uint8Array | Object} data @param {string | undefined} signature @param {number} signatureStart @param {number} expectedEnd */
|
|
17
|
+
constructor(topic, timestamp, neighborsList, HOPS, senderId, pubkey, data, signature, signatureStart, expectedEnd) {
|
|
18
|
+
this.topic = topic; this.timestamp = timestamp; this.neighborsList = neighborsList;
|
|
19
|
+
this.HOPS = HOPS; this.senderId = senderId; this.pubkey = pubkey; this.data = data;
|
|
20
|
+
this.signature = signature; this.signatureStart = signatureStart; this.expectedEnd = expectedEnd;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** - 'BloomFilterCacheEntry' Definition
|
|
25
|
+
* @typedef {Object} BloomFilterCacheEntry
|
|
26
|
+
* @property {string} hash
|
|
27
|
+
* @property {string} senderId
|
|
28
|
+
* @property {string} topic
|
|
29
|
+
* @property {Uint8Array} serializedMessage
|
|
30
|
+
* @property {number} expiration
|
|
31
|
+
*/
|
|
32
|
+
class DegenerateBloomFilter {
|
|
33
|
+
cryptoCodex;
|
|
34
|
+
xxHash32UsageCount = 0;
|
|
35
|
+
/** @type {Record<string, number>} */
|
|
36
|
+
seenTimeouts = {}; // Map of message hashes to their expiration timestamps
|
|
37
|
+
|
|
38
|
+
/** @type {BloomFilterCacheEntry[]} */ cache = [];
|
|
39
|
+
cleanupFrequency = 100;
|
|
40
|
+
cleanupIn = 100;
|
|
41
|
+
cleanupDurationWarning = 10;
|
|
42
|
+
|
|
43
|
+
/** @param {import('./crypto-codex.mjs').CryptoCodex} cryptoCodex */
|
|
44
|
+
constructor(cryptoCodex) { this.cryptoCodex = cryptoCodex; }
|
|
45
|
+
|
|
46
|
+
// PUBLIC API
|
|
47
|
+
/** @param {'asc' | 'desc'} order */
|
|
48
|
+
getGossipHistoryByTime(order = 'asc') {
|
|
49
|
+
const lightenHistory = this.cache.map(e => ({ senderId: e.senderId, topic: e.topic, data: e.serializedMessage }));
|
|
50
|
+
return order === 'asc' ? lightenHistory : lightenHistory.reverse();
|
|
51
|
+
}
|
|
52
|
+
/** @param {Uint8Array} serializedMessage */
|
|
53
|
+
addMessage(serializedMessage) {
|
|
54
|
+
const n = CLOCK.time;
|
|
55
|
+
const { marker, neighLength, timestamp, dataLength, pubkey, associatedId } = this.cryptoCodex.readBufferHeader(serializedMessage);
|
|
56
|
+
if (n - timestamp > GOSSIP.EXPIRATION) return;
|
|
57
|
+
|
|
58
|
+
const hashableData = serializedMessage.subarray(0, 47 + neighLength + dataLength);
|
|
59
|
+
const h = xxHash32(hashableData);
|
|
60
|
+
this.xxHash32UsageCount++;
|
|
61
|
+
if (this.seenTimeouts[h]) return;
|
|
62
|
+
|
|
63
|
+
const topic = GOSSIP.MARKERS_BYTES[marker];
|
|
64
|
+
const senderId = associatedId;
|
|
65
|
+
const expiration = n + GOSSIP.CACHE_DURATION;
|
|
66
|
+
this.cache.push({ hash: h, senderId, topic, serializedMessage, timestamp, expiration });
|
|
67
|
+
this.seenTimeouts[h] = expiration;
|
|
68
|
+
|
|
69
|
+
if (--this.cleanupIn <= 0) this.#cleanupOldestEntries(n);
|
|
70
|
+
return { hash: h, isNew: !this.seenTimeouts[h] };
|
|
71
|
+
}
|
|
72
|
+
#cleanupOldestEntries(n = CLOCK.time) {
|
|
73
|
+
let firstValidIndex = -1;
|
|
74
|
+
for (let i = 0; i < this.cache.length; i++)
|
|
75
|
+
if (this.cache[i].expiration <= n) delete this.seenTimeouts[this.cache[i].hash];
|
|
76
|
+
else if (firstValidIndex === -1) firstValidIndex = i;
|
|
77
|
+
|
|
78
|
+
if (firstValidIndex > 0) this.cache = this.cache.slice(firstValidIndex);
|
|
79
|
+
else if (firstValidIndex === -1) this.cache = [];
|
|
80
|
+
this.cleanupIn = this.cleanupFrequency;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export class Gossip {
|
|
85
|
+
/** @type {Record<string, Function[]>} */ callbacks = { message_handle: [] };
|
|
86
|
+
id; cryptoCodex; arbiter; peerStore; verbose; bloomFilter;
|
|
87
|
+
|
|
88
|
+
/** @param {string} selfId @param {import('./crypto-codex.mjs').CryptoCodex} cryptoCodex @param {import('./arbiter.mjs').Arbiter} arbiter @param {import('./peer-store.mjs').PeerStore} peerStore */
|
|
89
|
+
constructor(selfId, cryptoCodex, arbiter, peerStore, verbose = 0) {
|
|
90
|
+
this.id = selfId;
|
|
91
|
+
this.cryptoCodex = cryptoCodex;
|
|
92
|
+
this.arbiter = arbiter;
|
|
93
|
+
this.peerStore = peerStore;
|
|
94
|
+
this.verbose = verbose;
|
|
95
|
+
this.bloomFilter = new DegenerateBloomFilter(cryptoCodex);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** @param {string} callbackType @param {Function} callback */
|
|
99
|
+
on(callbackType, callback) {
|
|
100
|
+
if (!this.callbacks[callbackType]) this.callbacks[callbackType] = [callback];
|
|
101
|
+
else this.callbacks[callbackType].push(callback);
|
|
102
|
+
}
|
|
103
|
+
/** Gossip a message to all connected peers > will be forwarded to all peers
|
|
104
|
+
* @param {string | Uint8Array | Object} data @param {string} topic @param {number} [HOPS] */
|
|
105
|
+
broadcastToAll(data, topic = 'gossip', HOPS) {
|
|
106
|
+
const hops = HOPS || GOSSIP.HOPS[topic] || GOSSIP.HOPS.default;
|
|
107
|
+
const serializedMessage = this.cryptoCodex.createGossipMessage(topic, data, hops, this.peerStore.neighborsList);
|
|
108
|
+
if (!this.bloomFilter.addMessage(serializedMessage)) return; // avoid sending duplicate messages
|
|
109
|
+
if (this.verbose > 3) console.log(`(${this.id}) Gossip ${topic}, to ${JSON.stringify(this.peerStore.neighborsList)}: ${data}`);
|
|
110
|
+
for (const peerId of this.peerStore.neighborsList) this.#broadcastToPeer(peerId, serializedMessage);
|
|
111
|
+
}
|
|
112
|
+
/** @param {string} targetId @param {any} serializedMessage */
|
|
113
|
+
#broadcastToPeer(targetId, serializedMessage) {
|
|
114
|
+
if (targetId === this.id) throw new Error(`Refusing to send a gossip message to self (${this.id}).`);
|
|
115
|
+
const transportInstance = this.peerStore.connected[targetId]?.transportInstance;
|
|
116
|
+
if (!transportInstance) return { success: false, reason: `Transport instance is not available for peer ${targetId}.` };
|
|
117
|
+
try { transportInstance.send(serializedMessage); }
|
|
118
|
+
catch (error) { this.peerStore.connected[targetId]?.close(); }
|
|
119
|
+
}
|
|
120
|
+
sendGossipHistoryToPeer(peerId) {
|
|
121
|
+
const gossipHistory = this.bloomFilter.getGossipHistoryByTime('asc');
|
|
122
|
+
for (const entry of gossipHistory) this.#broadcastToPeer(peerId, entry.data);
|
|
123
|
+
}
|
|
124
|
+
/** @param {string} from @param {Uint8Array} serialized @returns {void} */
|
|
125
|
+
async handleGossipMessage(from, serialized) {
|
|
126
|
+
if (this.arbiter.isBanished(from)) return this.verbose >= 3 ? console.info(`%cReceived gossip message from banned peer ${from}, ignoring.`, 'color: red;') : null;
|
|
127
|
+
if (!this.arbiter.countMessageBytes(from, serialized.byteLength, 'gossip')) return; // ignore if flooding/banished
|
|
128
|
+
for (const cb of this.callbacks.message_handle || []) cb(serialized); // Simulator counter before filtering
|
|
129
|
+
if (!this.bloomFilter.addMessage(serialized)) return; // already processed this message
|
|
130
|
+
|
|
131
|
+
const message = this.cryptoCodex.readGossipMessage(serialized);
|
|
132
|
+
if (!message) return this.arbiter.countPeerAction(from, 'WRONG_SERIALIZATION');
|
|
133
|
+
await this.arbiter.digestMessage(from, message, serialized);
|
|
134
|
+
if (this.arbiter.isBanished(from)) return; // ignore messages from banished peers
|
|
135
|
+
if (this.arbiter.isBanished(message.senderId)) return; // ignore messages from banished peers
|
|
136
|
+
|
|
137
|
+
const { topic, timestamp, neighborsList, HOPS, senderId, data } = message;
|
|
138
|
+
if (senderId === this.id) throw new Error(`#${this.id}#${from}# Received our own message back from peer ${from}.`);
|
|
139
|
+
|
|
140
|
+
if (this.verbose > 3) // LOGGING
|
|
141
|
+
if (senderId === from) console.log(`(${this.id}) Gossip ${topic} from ${senderId}: ${data}`);
|
|
142
|
+
else console.log(`(${this.id}) Gossip ${topic} from ${senderId} (by: ${from}): ${data}`);
|
|
143
|
+
|
|
144
|
+
this.peerStore.digestPeerNeighbors(senderId, neighborsList);
|
|
145
|
+
for (const cb of this.callbacks[topic] || []) cb(senderId, data, HOPS, message); // specific topic callback
|
|
146
|
+
if (HOPS < 1) return; // stop forwarding if HOPS is 0
|
|
147
|
+
|
|
148
|
+
const nCount = this.peerStore.neighborsList.length;
|
|
149
|
+
const trm = Math.max(1, nCount / GOSSIP.TRANSMISSION_RATE.NEIGHBOURS_PONDERATION);
|
|
150
|
+
const tRateBase = GOSSIP.TRANSMISSION_RATE[topic] || GOSSIP.TRANSMISSION_RATE.default;
|
|
151
|
+
const transmissionRate = Math.pow(tRateBase, trm);
|
|
152
|
+
const avoidTransmissionRate = nCount < GOSSIP.TRANSMISSION_RATE.MIN_NEIGHBOURS_TO_APPLY_PONDERATION;
|
|
153
|
+
const serializedToTransmit = this.cryptoCodex.decrementGossipHops(serialized);
|
|
154
|
+
for (const peerId of this.peerStore.neighborsList)
|
|
155
|
+
if (peerId === from) continue; // avoid sending back to sender
|
|
156
|
+
else if (!avoidTransmissionRate && Math.random() > transmissionRate) continue; // apply gossip transmission rate
|
|
157
|
+
else this.#broadcastToPeer(peerId, serializedToTransmit);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { CLOCK, NODE, TRANSPORTS, LOG_CSS } from './parameters.mjs';
|
|
2
|
+
import { xxHash32 } from '../libs/xxhash32.mjs';
|
|
3
|
+
import wrtc from 'wrtc';
|
|
4
|
+
|
|
5
|
+
/** - 'OfferObj' Definition
|
|
6
|
+
* @typedef {Object} OfferObj
|
|
7
|
+
* @property {number} timestamp
|
|
8
|
+
* @property {boolean} isUsed // => if true => should be deleted
|
|
9
|
+
* @property {number} sentCounter
|
|
10
|
+
* @property {Object} signal
|
|
11
|
+
* @property {import('simple-peer').Instance} offererInstance
|
|
12
|
+
* @property {boolean} isDigestingOneAnswer Flag to avoid multiple answers handling at the same time (DISCOVERY.LOOP_DELAY (2.5s) will be doubled (5s) between two answers handling)
|
|
13
|
+
* @property {Array<{peerId: string, signal: any, timestamp: number, used: boolean}>} answers
|
|
14
|
+
* @property {Record<string, boolean>} answerers key: peerId, value: true */
|
|
15
|
+
|
|
16
|
+
export class OfferManager { // Manages the creation of SDP offers and handling of answers
|
|
17
|
+
id;
|
|
18
|
+
verbose;
|
|
19
|
+
stunUrls;
|
|
20
|
+
|
|
21
|
+
/** @param {string} id @param {Array<{urls: string}>} stunUrls */
|
|
22
|
+
constructor(id, stunUrls, verbose = 0) { this.id = id; this.verbose = verbose; this.stunUrls = stunUrls; }
|
|
23
|
+
|
|
24
|
+
onSignalAnswer = null; // function(remoteId, signalData, offerHash)
|
|
25
|
+
onConnect = null; // function(remoteId, transportInstance)
|
|
26
|
+
|
|
27
|
+
/** @type {Record<number, import('simple-peer').Instance>} key: expiration timestamp */
|
|
28
|
+
offerInstanceByExpiration = {};
|
|
29
|
+
creatingOffer = false; // flag to avoid multiple simultaneous creations (shared between all offers)
|
|
30
|
+
offerCreationTimeout = null; // sequential creation timeout (shared between all offers)
|
|
31
|
+
offersToCreate = TRANSPORTS.MAX_SDP_OFFERS;
|
|
32
|
+
/** @type {Record<string, OfferObj>} key: offerHash **/ offers = {};
|
|
33
|
+
|
|
34
|
+
tick() { // called in peerStore to avoid multiple intervals
|
|
35
|
+
const now = CLOCK.time;
|
|
36
|
+
// CLEAR EXPIRED CREATOR OFFER INSTANCES
|
|
37
|
+
for (const expiration in this.offerInstanceByExpiration) {
|
|
38
|
+
const instance = this.offerInstanceByExpiration[expiration];
|
|
39
|
+
if (now < expiration) continue; // not expired yet
|
|
40
|
+
instance?.destroy();
|
|
41
|
+
delete this.offerInstanceByExpiration[expiration];
|
|
42
|
+
this.creatingOffer = false; // release flag
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// CLEAR USED AND EXPIRED OFFERS
|
|
46
|
+
for (const hash in this.offers) {
|
|
47
|
+
const offer = this.offers[hash];
|
|
48
|
+
if (offer.offererInstance.destroyed) { delete this.offers[hash]; continue; } // offerer destroyed
|
|
49
|
+
if (offer.isUsed) { delete this.offers[hash]; continue; } // used offer => remove it (handled by peerStore)
|
|
50
|
+
if (offer.timestamp + TRANSPORTS.SDP_OFFER_EXPIRATION > now) continue; // not expired yet
|
|
51
|
+
offer.offererInstance?.destroy();
|
|
52
|
+
delete this.offers[hash];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// TRY TO USE AVAILABLE ANSWERS
|
|
56
|
+
let offerCount = 0;
|
|
57
|
+
for (const hash in this.offers) {
|
|
58
|
+
offerCount++; // [live at the first line of the loop] used just below -> avoid Object.keys() call
|
|
59
|
+
const offer = this.offers[hash];
|
|
60
|
+
if (offer.isDigestingOneAnswer) { offer.isDigestingOneAnswer = false; continue; }
|
|
61
|
+
if (offer.offererInstance.destroyed) continue; // offerer destroyed
|
|
62
|
+
|
|
63
|
+
const unusedAnswers = offer.answers.filter(a => !a.used);
|
|
64
|
+
if (!unusedAnswers.length) continue; // no answers available
|
|
65
|
+
|
|
66
|
+
const newestAnswer = unusedAnswers.reduce((a, b) => a.timestamp > b.timestamp ? a : b);
|
|
67
|
+
if (!newestAnswer) continue; // all answers are used
|
|
68
|
+
|
|
69
|
+
newestAnswer.used = true;
|
|
70
|
+
const receivedSince = now - newestAnswer.timestamp;
|
|
71
|
+
if (receivedSince > NODE.CONNECTION_UPGRADE_TIMEOUT / 2) continue; // remote peer will break the connection soon, don't use this answer
|
|
72
|
+
offer.offererInstance.signal(newestAnswer.signal);
|
|
73
|
+
offer.isDigestingOneAnswer = true;
|
|
74
|
+
if (this.verbose > 2) console.log(`(${this.id}) Using answer from ${newestAnswer.peerId} for offer ${hash} (received since ${receivedSince} ms)`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (this.creatingOffer) return; // already creating one or unable to send
|
|
78
|
+
if (offerCount >= this.offersToCreate) return; // already have enough offers
|
|
79
|
+
|
|
80
|
+
// CREATE NEW OFFER
|
|
81
|
+
this.creatingOffer = true;
|
|
82
|
+
const expiration = now + (TRANSPORTS.SIGNAL_CREATION_TIMEOUT || 8_000);
|
|
83
|
+
const instance = this.#createOffererInstance(expiration);
|
|
84
|
+
this.offerInstanceByExpiration[expiration] = instance;
|
|
85
|
+
};
|
|
86
|
+
#createOffererInstance(expiration) {
|
|
87
|
+
const instance = new TRANSPORTS.PEER({ initiator: true, trickle: false, wrtc, config: { iceServers: this.stunUrls } });
|
|
88
|
+
instance.on('error', error => this.#onError(error));
|
|
89
|
+
instance.on('signal', data => { // trickle: false => only one signal event with the full offer
|
|
90
|
+
const { candidate, type } = data; // with trickle, we need to adapt the approach.
|
|
91
|
+
if (!data || candidate) throw new Error('Unexpected signal data from offerer instance: ' + JSON.stringify(data));
|
|
92
|
+
if (type !== 'offer') throw new Error('Unexpected signal type from offerer instance: ' + type);
|
|
93
|
+
|
|
94
|
+
// OFFER READY
|
|
95
|
+
delete this.offerInstanceByExpiration[expiration];
|
|
96
|
+
const offerHash = xxHash32(JSON.stringify(data)); // UN PEU BLOQUE ICI (connect on voudrait identifer le peer)
|
|
97
|
+
instance.on('connect', () => { // cb > peerStore > Node > Node.#onConnect()
|
|
98
|
+
if (this.offers[offerHash]) this.offers[offerHash].isUsed = true;
|
|
99
|
+
this.onConnect(undefined, instance);
|
|
100
|
+
});
|
|
101
|
+
this.offers[offerHash] = { timestamp: CLOCK.time, sentCounter: 0, signal: data, offererInstance: instance, answers: [], answerers: {}, isUsed: false };
|
|
102
|
+
this.creatingOffer = false; // release flag
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return instance;
|
|
106
|
+
}
|
|
107
|
+
/** @param {Error} error @param {string} incl @param {number} level @param {'includes' | 'startsWith'} searchMode (Prefer 'startsWith' for performance) */
|
|
108
|
+
#logAndOrIgnore(error, incl = '', level = 2, searchMode = 'includes') { // if false => log it fully, if true => ignore it (message logged or ignored based on level)
|
|
109
|
+
if (searchMode[0] === 'i' && !error.message.includes(incl)) return false;
|
|
110
|
+
else if (!error.message.startsWith(incl)) return false;
|
|
111
|
+
if (this.verbose >= level) console.info(`%cOfferManager => ${error.message}`, LOG_CSS.PEER_STORE);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
#onError = (error) => {
|
|
115
|
+
if (this.verbose < 1) return; // avoid logging
|
|
116
|
+
// PRODUCTION (SimplePeer ERRORS) --|
|
|
117
|
+
if (this.#logAndOrIgnore(error, 'Ice connection failed', 2)) return;
|
|
118
|
+
if (this.#logAndOrIgnore(error, 'Connection failed', 2)) return;
|
|
119
|
+
// --PRODUCTION ----------------- --|
|
|
120
|
+
|
|
121
|
+
if (this.#logAndOrIgnore(error, 'Remote transport instance', 3, 'startsWith')) return;
|
|
122
|
+
if (this.#logAndOrIgnore(error, 'Simulated failure', 4, 'startsWith')) return;
|
|
123
|
+
if (this.#logAndOrIgnore(error, 'No peer found', 4, 'startsWith')) return;
|
|
124
|
+
if (this.#logAndOrIgnore(error, 'Missing transport instance', 2, 'startsWith')) return;
|
|
125
|
+
if (this.#logAndOrIgnore(error, 'Failed to create answer', 2, 'startsWith')) return;
|
|
126
|
+
if (this.#logAndOrIgnore(error, 'Transport instance', 3, 'startsWith')) return;
|
|
127
|
+
if (this.#logAndOrIgnore(error, 'cannot signal after peer is destroyed', 3, 'startsWith')) return;
|
|
128
|
+
if (this.#logAndOrIgnore(error, 'No pending', 3)) return;
|
|
129
|
+
if (this.#logAndOrIgnore(error, 'is already linked', 3)) return;
|
|
130
|
+
if (this.#logAndOrIgnore(error, 'There is already a pending', 3)) return;
|
|
131
|
+
if (this.#logAndOrIgnore(error, 'closed the connection', 3)) return;
|
|
132
|
+
if (this.#logAndOrIgnore(error, 'No transport instance found for id:', 3)) return;
|
|
133
|
+
|
|
134
|
+
if (this.verbose > 0) console.error(`transportInstance ERROR => `, error.stack);
|
|
135
|
+
};
|
|
136
|
+
/** @param {string} remoteId @param {{type: 'answer', sdp: Record<string, string>}} signal @param {string} offerHash @param {number} timestamp receptionTimestamp */
|
|
137
|
+
addSignalAnswer(remoteId, signal, offerHash, timestamp) {
|
|
138
|
+
if (!signal || signal.type !== 'answer' || !offerHash) return; // ignore non-answers or missing offerHash
|
|
139
|
+
if (!this.offers[offerHash] || this.offers[offerHash].answerers[remoteId]) return; // already have an answer from this peerId
|
|
140
|
+
this.offers[offerHash].answerers[remoteId] = true; // mark as having answered - one answer per peerId
|
|
141
|
+
this.offers[offerHash].answers.push({ peerId: remoteId, signal, timestamp });
|
|
142
|
+
if (this.verbose > 3) console.log(`(OfferManager) Added answer from ${remoteId} for offer ${offerHash}`);
|
|
143
|
+
}
|
|
144
|
+
/** @param {string} remoteId @param {{type: 'offer' | 'answer', sdp: Record<string, string>}} signal @param {string} [offerHash] offer only */
|
|
145
|
+
getTransportInstanceForSignal(remoteId, signal, offerHash) {
|
|
146
|
+
try {
|
|
147
|
+
if (!signal || !signal.type || !signal.sdp) throw new Error('Wrong remote SDP provided');
|
|
148
|
+
|
|
149
|
+
const { type, sdp } = signal;
|
|
150
|
+
if (type !== 'offer' && type !== 'answer') throw new Error('Invalid remote SDP type');
|
|
151
|
+
if (type === 'offer' && !sdp) throw new Error('No SDP in the remote SDP offer');
|
|
152
|
+
if (type === 'answer' && !sdp) throw new Error('No SDP in the remote SDP answer');
|
|
153
|
+
|
|
154
|
+
if (type === 'answer') { // NEED TO FIND THE PENDING OFFERER INSTANCE
|
|
155
|
+
const instance = offerHash ? this.offers[offerHash]?.offererInstance : null;
|
|
156
|
+
if (!instance) throw new Error('No pending offer found for the given offer hash to accept the answer');
|
|
157
|
+
return instance;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// type === 'offer' => CREATE ANSWERER INSTANCE
|
|
161
|
+
const instance = new TRANSPORTS.PEER({ initiator: false, trickle: false, wrtc, config: { iceServers: this.stunUrls } });
|
|
162
|
+
instance.on('error', (error) => this.#onError(error));
|
|
163
|
+
instance.on('signal', (data) => this.onSignalAnswer(remoteId, data, offerHash));
|
|
164
|
+
instance.on('connect', () => this.onConnect(remoteId, instance));
|
|
165
|
+
return instance;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (error.message.startsWith('No pending offer found') && this.verbose < 2) return null; // avoid logging
|
|
168
|
+
if (this.verbose > 1 && error.message.startsWith('No pending offer found')) return console.info(`%c${error.message}`, LOG_CSS.PEER_STORE);
|
|
169
|
+
if (this.verbose > 0) console.error(error.stack);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
destroy() {
|
|
173
|
+
for (const offerHash in this.offers) this.offers[offerHash].offererInstance?.destroy();
|
|
174
|
+
}
|
|
175
|
+
}
|