@omindu/yaksha 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.
@@ -0,0 +1,394 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Yaksha Multi-Path Routing
5
+ * Establish multiple connections and distribute traffic
6
+ */
7
+
8
+ const EventEmitter = require('events');
9
+ const Connection = require('../core/connection');
10
+ const Logger = require('../utils/logger');
11
+
12
+ // Path selection strategies
13
+ const STRATEGIES = {
14
+ ROUND_ROBIN: 'round-robin',
15
+ LEAST_LATENCY: 'least-latency',
16
+ LOAD_BALANCED: 'load-balanced',
17
+ WEIGHTED: 'weighted'
18
+ };
19
+
20
+ class MultiPath extends EventEmitter {
21
+ constructor(securityLevel = 'medium', options = {}) {
22
+ super();
23
+
24
+ this.securityLevel = securityLevel;
25
+ this.pathCount = this._selectPathCount(securityLevel);
26
+ this.strategy = options.strategy || STRATEGIES.LOAD_BALANCED;
27
+ this.enabled = options.enabled !== false;
28
+
29
+ this.paths = [];
30
+ this.pathStats = [];
31
+ this.currentPathIndex = 0;
32
+ this.sequenceNumber = 0;
33
+ this.receivedPackets = new Map(); // sequence -> packet data
34
+ this.expectedSequence = 0;
35
+
36
+ this.logger = new Logger({ prefix: 'MultiPath' });
37
+ }
38
+
39
+ /**
40
+ * Select number of paths based on security level
41
+ */
42
+ _selectPathCount(level) {
43
+ switch (level) {
44
+ case 'low':
45
+ return 2;
46
+ case 'medium':
47
+ return 4;
48
+ case 'high':
49
+ return 5;
50
+ default:
51
+ return 3;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Initialize multiple paths to server
57
+ */
58
+ async initializePaths(serverHosts, serverPort, connectionOptions = {}) {
59
+ if (!this.enabled) {
60
+ // Single path mode
61
+ const connection = new Connection({
62
+ host: serverHosts[0] || serverHosts,
63
+ port: serverPort,
64
+ ...connectionOptions
65
+ });
66
+
67
+ await connection.connect();
68
+ this.paths.push(connection);
69
+ this.pathStats.push(this._createPathStats(0));
70
+ return;
71
+ }
72
+
73
+ // Create multiple connections
74
+ const hosts = Array.isArray(serverHosts) ? serverHosts : [serverHosts];
75
+ const promises = [];
76
+
77
+ for (let i = 0; i < this.pathCount; i++) {
78
+ // Distribute across multiple hosts or use same host with different connections
79
+ const host = hosts[i % hosts.length];
80
+
81
+ const connection = new Connection({
82
+ host,
83
+ port: serverPort,
84
+ ...connectionOptions
85
+ });
86
+
87
+ // Set up event handlers
88
+ connection.on('tcp:data', (data) => {
89
+ this.pathStats[i].bytesReceived += data.length;
90
+ this.pathStats[i].packetsReceived++;
91
+ this.emit('data', data, i);
92
+ });
93
+
94
+ connection.on('udp:data', (data, rinfo) => {
95
+ this.pathStats[i].bytesReceived += data.length;
96
+ this.pathStats[i].packetsReceived++;
97
+ this.emit('data', data, i);
98
+ });
99
+
100
+ connection.on('error', (error) => {
101
+ this.pathStats[i].errors++;
102
+ this.pathStats[i].available = false;
103
+ this.logger.warn(`Path ${i} error:`, error.message);
104
+ this.emit('path-error', i, error);
105
+ });
106
+
107
+ connection.on('disconnected', () => {
108
+ this.pathStats[i].available = false;
109
+ this.logger.info(`Path ${i} disconnected`);
110
+ this.emit('path-disconnected', i);
111
+ });
112
+
113
+ promises.push(connection.connect().then(() => {
114
+ this.paths.push(connection);
115
+ this.pathStats.push(this._createPathStats(i));
116
+ this.logger.info(`Path ${i} established to ${host}:${serverPort}`);
117
+ }).catch(error => {
118
+ this.logger.error(`Failed to establish path ${i}:`, error.message);
119
+ throw error;
120
+ }));
121
+ }
122
+
123
+ // Wait for all connections
124
+ await Promise.all(promises);
125
+
126
+ this.logger.info(`Multi-path routing initialized with ${this.paths.length} paths`);
127
+ }
128
+
129
+ /**
130
+ * Create path statistics object
131
+ */
132
+ _createPathStats(index) {
133
+ return {
134
+ index,
135
+ available: true,
136
+ latency: 0,
137
+ packetsSent: 0,
138
+ packetsReceived: 0,
139
+ bytesSent: 0,
140
+ bytesReceived: 0,
141
+ errors: 0,
142
+ lastUsed: Date.now(),
143
+ score: 100
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Measure path latency
149
+ */
150
+ async measureLatency(pathIndex) {
151
+ const path = this.paths[pathIndex];
152
+ if (!path || !path.isConnected()) {
153
+ return Infinity;
154
+ }
155
+
156
+ const start = Date.now();
157
+
158
+ try {
159
+ // Send a keepalive packet and measure response time
160
+ await new Promise((resolve, reject) => {
161
+ const timeout = setTimeout(() => reject(new Error('Timeout')), 5000);
162
+
163
+ path.once('tcp:data', () => {
164
+ clearTimeout(timeout);
165
+ resolve();
166
+ });
167
+
168
+ // In production, send actual keepalive packet
169
+ resolve(); // Simulate immediate response
170
+ });
171
+
172
+ const latency = Date.now() - start;
173
+ this.pathStats[pathIndex].latency = latency;
174
+ return latency;
175
+ } catch (error) {
176
+ this.pathStats[pathIndex].latency = Infinity;
177
+ return Infinity;
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Calculate path score for load balancing
183
+ */
184
+ _calculatePathScore(pathIndex) {
185
+ const stats = this.pathStats[pathIndex];
186
+
187
+ if (!stats.available) {
188
+ return 0;
189
+ }
190
+
191
+ // Score based on latency, packet loss, and load
192
+ const latencyScore = stats.latency > 0 ? 1000 / stats.latency : 100;
193
+ const lossRate = stats.packetsSent > 0
194
+ ? 1 - (stats.errors / stats.packetsSent)
195
+ : 1;
196
+ const loadScore = 100 - (stats.packetsSent % 100);
197
+
198
+ const score = (latencyScore * 0.5) + (lossRate * 30) + (loadScore * 0.2);
199
+ stats.score = Math.max(0, Math.min(100, score));
200
+
201
+ return stats.score;
202
+ }
203
+
204
+ /**
205
+ * Select next path based on strategy
206
+ */
207
+ _selectPath() {
208
+ // Filter available paths
209
+ const availablePaths = this.pathStats
210
+ .map((stat, index) => ({ ...stat, index }))
211
+ .filter(stat => stat.available);
212
+
213
+ if (availablePaths.length === 0) {
214
+ throw new Error('No available paths');
215
+ }
216
+
217
+ if (availablePaths.length === 1) {
218
+ return availablePaths[0].index;
219
+ }
220
+
221
+ let selectedIndex;
222
+
223
+ switch (this.strategy) {
224
+ case STRATEGIES.ROUND_ROBIN:
225
+ selectedIndex = this.currentPathIndex % availablePaths.length;
226
+ this.currentPathIndex = (this.currentPathIndex + 1) % availablePaths.length;
227
+ return availablePaths[selectedIndex].index;
228
+
229
+ case STRATEGIES.LEAST_LATENCY:
230
+ return availablePaths.reduce((min, path) =>
231
+ path.latency < min.latency ? path : min
232
+ ).index;
233
+
234
+ case STRATEGIES.LOAD_BALANCED:
235
+ // Calculate scores for all paths
236
+ availablePaths.forEach(path => {
237
+ this._calculatePathScore(path.index);
238
+ });
239
+
240
+ // Select path with highest score
241
+ return availablePaths.reduce((best, path) =>
242
+ path.score > best.score ? path : best
243
+ ).index;
244
+
245
+ case STRATEGIES.WEIGHTED:
246
+ // Weighted random selection based on scores
247
+ const totalScore = availablePaths.reduce((sum, path) =>
248
+ sum + this._calculatePathScore(path.index), 0
249
+ );
250
+
251
+ let random = Math.random() * totalScore;
252
+ for (const path of availablePaths) {
253
+ random -= path.score;
254
+ if (random <= 0) {
255
+ return path.index;
256
+ }
257
+ }
258
+ return availablePaths[0].index;
259
+
260
+ default:
261
+ return availablePaths[0].index;
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Send data across multiple paths
267
+ */
268
+ async send(data, protocol = 'tcp') {
269
+ if (!this.enabled || this.paths.length === 1) {
270
+ // Single path mode
271
+ const path = this.paths[0];
272
+ if (protocol === 'tcp') {
273
+ return await path.sendTCP(data);
274
+ } else {
275
+ return await path.sendUDP(data);
276
+ }
277
+ }
278
+
279
+ // Select path
280
+ const pathIndex = this._selectPath();
281
+ const path = this.paths[pathIndex];
282
+ const stats = this.pathStats[pathIndex];
283
+
284
+ try {
285
+ // Add sequence number for packet reordering
286
+ const sequence = this.sequenceNumber++;
287
+ const packetWithSeq = Buffer.allocUnsafe(4 + data.length);
288
+ packetWithSeq.writeUInt32BE(sequence, 0);
289
+ data.copy(packetWithSeq, 4);
290
+
291
+ // Send via selected path
292
+ if (protocol === 'tcp') {
293
+ await path.sendTCP(packetWithSeq);
294
+ } else {
295
+ await path.sendUDP(packetWithSeq);
296
+ }
297
+
298
+ stats.packetsSent++;
299
+ stats.bytesSent += data.length;
300
+ stats.lastUsed = Date.now();
301
+
302
+ return pathIndex;
303
+ } catch (error) {
304
+ stats.errors++;
305
+ this.logger.error(`Failed to send on path ${pathIndex}:`, error.message);
306
+
307
+ // Try failover to another path
308
+ if (this.paths.length > 1) {
309
+ this.logger.info('Attempting failover to another path');
310
+ stats.available = false;
311
+ return await this.send(data, protocol);
312
+ }
313
+
314
+ throw error;
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Receive and reorder packets
320
+ */
321
+ receive(data, pathIndex) {
322
+ // Extract sequence number
323
+ if (data.length < 4) {
324
+ return null;
325
+ }
326
+
327
+ const sequence = data.readUInt32BE(0);
328
+ const payload = data.slice(4);
329
+
330
+ // Store packet
331
+ this.receivedPackets.set(sequence, payload);
332
+
333
+ // Try to deliver in-order packets
334
+ const deliveredPackets = [];
335
+
336
+ while (this.receivedPackets.has(this.expectedSequence)) {
337
+ const packet = this.receivedPackets.get(this.expectedSequence);
338
+ deliveredPackets.push(packet);
339
+ this.receivedPackets.delete(this.expectedSequence);
340
+ this.expectedSequence++;
341
+ }
342
+
343
+ return deliveredPackets;
344
+ }
345
+
346
+ /**
347
+ * Get path statistics
348
+ */
349
+ getPathStats() {
350
+ return this.pathStats.map(stat => ({ ...stat }));
351
+ }
352
+
353
+ /**
354
+ * Get overall statistics
355
+ */
356
+ getStats() {
357
+ const totalStats = {
358
+ pathCount: this.paths.length,
359
+ activePaths: this.pathStats.filter(s => s.available).length,
360
+ strategy: this.strategy,
361
+ totalBytesSent: 0,
362
+ totalBytesReceived: 0,
363
+ totalPacketsSent: 0,
364
+ totalPacketsReceived: 0,
365
+ totalErrors: 0
366
+ };
367
+
368
+ for (const stat of this.pathStats) {
369
+ totalStats.totalBytesSent += stat.bytesSent;
370
+ totalStats.totalBytesReceived += stat.bytesReceived;
371
+ totalStats.totalPacketsSent += stat.packetsSent;
372
+ totalStats.totalPacketsReceived += stat.packetsReceived;
373
+ totalStats.totalErrors += stat.errors;
374
+ }
375
+
376
+ return totalStats;
377
+ }
378
+
379
+ /**
380
+ * Close all paths
381
+ */
382
+ closeAll() {
383
+ for (const path of this.paths) {
384
+ path.disconnect();
385
+ }
386
+
387
+ this.paths = [];
388
+ this.pathStats = [];
389
+ this.logger.info('All paths closed');
390
+ }
391
+ }
392
+
393
+ module.exports = MultiPath;
394
+ module.exports.STRATEGIES = STRATEGIES;