2020117-agent 0.1.7 → 0.1.8
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/dist/agent.js +93 -0
- package/dist/session.js +218 -2
- package/dist/swarm.d.ts +7 -1
- package/dist/swarm.js +1 -0
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -356,6 +356,8 @@ async function delegateAPI(kind, input, bidSats, provider) {
|
|
|
356
356
|
// --- 4. P2P Swarm Listener ---
|
|
357
357
|
const p2pJobs = new Map();
|
|
358
358
|
const activeSessions = new Map();
|
|
359
|
+
// Backend WebSocket connections for WS tunnel (keyed by ws_id)
|
|
360
|
+
const backendWebSockets = new Map();
|
|
359
361
|
async function startSwarmListener(label) {
|
|
360
362
|
const node = new SwarmNode();
|
|
361
363
|
state.swarmNode = node;
|
|
@@ -509,6 +511,87 @@ async function startSwarmListener(label) {
|
|
|
509
511
|
}
|
|
510
512
|
return;
|
|
511
513
|
}
|
|
514
|
+
// --- WebSocket tunnel ---
|
|
515
|
+
if (msg.type === 'ws_open') {
|
|
516
|
+
const session = findSessionBySocket(socket);
|
|
517
|
+
if (!session) {
|
|
518
|
+
node.send(socket, { type: 'error', id: msg.id, message: 'No active session' });
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const processorUrl = process.env.PROCESSOR;
|
|
522
|
+
if (!processorUrl || (!processorUrl.startsWith('http://') && !processorUrl.startsWith('https://'))) {
|
|
523
|
+
node.send(socket, { type: 'ws_open', id: msg.id, ws_id: msg.ws_id, message: 'No HTTP backend configured' });
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
const wsId = msg.ws_id;
|
|
527
|
+
const wsPath = msg.ws_path || '/';
|
|
528
|
+
const backendWsUrl = processorUrl.replace(/^http:/, 'ws:').replace(/^https:/, 'wss:').replace(/\/$/, '') + wsPath;
|
|
529
|
+
console.log(`[${label}] WS ${wsId}: opening ${backendWsUrl}`);
|
|
530
|
+
try {
|
|
531
|
+
const backendWs = new WebSocket(backendWsUrl, msg.ws_protocols || []);
|
|
532
|
+
backendWebSockets.set(wsId, { ws: backendWs, peerId });
|
|
533
|
+
backendWs.addEventListener('open', () => {
|
|
534
|
+
console.log(`[${label}] WS ${wsId}: backend connected`);
|
|
535
|
+
});
|
|
536
|
+
backendWs.addEventListener('message', (event) => {
|
|
537
|
+
const d = event.data;
|
|
538
|
+
if (typeof d === 'string') {
|
|
539
|
+
node.send(socket, { type: 'ws_message', id: wsId, ws_id: wsId, data: d, ws_frame_type: 'text' });
|
|
540
|
+
}
|
|
541
|
+
else if (d instanceof ArrayBuffer) {
|
|
542
|
+
node.send(socket, { type: 'ws_message', id: wsId, ws_id: wsId, data: Buffer.from(d).toString('base64'), ws_frame_type: 'binary' });
|
|
543
|
+
}
|
|
544
|
+
else if (d instanceof Blob) {
|
|
545
|
+
d.arrayBuffer().then(ab => {
|
|
546
|
+
node.send(socket, { type: 'ws_message', id: wsId, ws_id: wsId, data: Buffer.from(ab).toString('base64'), ws_frame_type: 'binary' });
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
backendWs.addEventListener('close', (event) => {
|
|
551
|
+
console.log(`[${label}] WS ${wsId}: backend closed (code=${event.code})`);
|
|
552
|
+
backendWebSockets.delete(wsId);
|
|
553
|
+
node.send(socket, { type: 'ws_close', id: wsId, ws_id: wsId, ws_code: event.code, ws_reason: event.reason || '' });
|
|
554
|
+
});
|
|
555
|
+
backendWs.addEventListener('error', () => {
|
|
556
|
+
console.error(`[${label}] WS ${wsId}: backend error`);
|
|
557
|
+
backendWebSockets.delete(wsId);
|
|
558
|
+
node.send(socket, { type: 'ws_close', id: wsId, ws_id: wsId, ws_code: 1011, ws_reason: 'Backend WebSocket error' });
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
catch (e) {
|
|
562
|
+
node.send(socket, { type: 'ws_open', id: msg.id, ws_id: wsId, message: e.message });
|
|
563
|
+
}
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (msg.type === 'ws_message') {
|
|
567
|
+
const entry = backendWebSockets.get(msg.ws_id || '');
|
|
568
|
+
if (!entry || entry.ws.readyState !== WebSocket.OPEN)
|
|
569
|
+
return;
|
|
570
|
+
try {
|
|
571
|
+
if (msg.ws_frame_type === 'binary') {
|
|
572
|
+
entry.ws.send(Buffer.from(msg.data || '', 'base64'));
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
entry.ws.send(msg.data || '');
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
catch (e) {
|
|
579
|
+
console.error(`[${label}] WS ${msg.ws_id}: send failed: ${e.message}`);
|
|
580
|
+
}
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (msg.type === 'ws_close') {
|
|
584
|
+
const entry = backendWebSockets.get(msg.ws_id || '');
|
|
585
|
+
if (entry) {
|
|
586
|
+
console.log(`[${label}] WS ${msg.ws_id}: closing backend`);
|
|
587
|
+
try {
|
|
588
|
+
entry.ws.close(msg.ws_code || 1000, msg.ws_reason || '');
|
|
589
|
+
}
|
|
590
|
+
catch { }
|
|
591
|
+
backendWebSockets.delete(msg.ws_id || '');
|
|
592
|
+
}
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
512
595
|
// Session-scoped request (no payment negotiation — session pays per-minute)
|
|
513
596
|
if (msg.type === 'request' && msg.session_id) {
|
|
514
597
|
const session = activeSessions.get(msg.session_id);
|
|
@@ -613,6 +696,16 @@ function endSession(node, session, label) {
|
|
|
613
696
|
clearInterval(session.timeoutTimer);
|
|
614
697
|
session.timeoutTimer = null;
|
|
615
698
|
}
|
|
699
|
+
// Close all backend WebSockets for this peer
|
|
700
|
+
for (const [wsId, entry] of backendWebSockets) {
|
|
701
|
+
if (entry.peerId === session.peerId) {
|
|
702
|
+
try {
|
|
703
|
+
entry.ws.close(1001, 'Session ended');
|
|
704
|
+
}
|
|
705
|
+
catch { }
|
|
706
|
+
backendWebSockets.delete(wsId);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
616
709
|
node.send(session.socket, {
|
|
617
710
|
type: 'session_end',
|
|
618
711
|
id: session.sessionId,
|
package/dist/session.js
CHANGED
|
@@ -42,7 +42,7 @@ for (const arg of process.argv.slice(2)) {
|
|
|
42
42
|
import { SwarmNode, topicFromKind } from './swarm.js';
|
|
43
43
|
import { queryProviderSkill } from './p2p-customer.js';
|
|
44
44
|
import { mintTokens, splitTokens } from './cashu.js';
|
|
45
|
-
import { randomBytes } from 'crypto';
|
|
45
|
+
import { randomBytes, createHash } from 'crypto';
|
|
46
46
|
import { createServer } from 'http';
|
|
47
47
|
import { createInterface } from 'readline';
|
|
48
48
|
import { mkdirSync, writeFileSync } from 'fs';
|
|
@@ -70,6 +70,7 @@ const state = {
|
|
|
70
70
|
shuttingDown: false,
|
|
71
71
|
pendingRequests: new Map(),
|
|
72
72
|
chunkBuffers: new Map(),
|
|
73
|
+
activeWebSockets: new Map(),
|
|
73
74
|
outputCounter: 0,
|
|
74
75
|
};
|
|
75
76
|
// --- Helpers ---
|
|
@@ -172,6 +173,45 @@ function setupMessageHandler() {
|
|
|
172
173
|
warn(`Provider error: ${msg.message}`);
|
|
173
174
|
break;
|
|
174
175
|
}
|
|
176
|
+
case 'ws_message': {
|
|
177
|
+
const browserSocket = state.activeWebSockets.get(msg.ws_id || '');
|
|
178
|
+
if (!browserSocket || browserSocket.destroyed) {
|
|
179
|
+
state.activeWebSockets.delete(msg.ws_id || '');
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
const isText = msg.ws_frame_type !== 'binary';
|
|
183
|
+
const payload = isText
|
|
184
|
+
? Buffer.from(msg.data || '', 'utf-8')
|
|
185
|
+
: Buffer.from(msg.data || '', 'base64');
|
|
186
|
+
try {
|
|
187
|
+
browserSocket.write(buildWsFrame(payload, isText ? 0x01 : 0x02));
|
|
188
|
+
}
|
|
189
|
+
catch { }
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
case 'ws_close': {
|
|
193
|
+
const browserSocket = state.activeWebSockets.get(msg.ws_id || '');
|
|
194
|
+
if (browserSocket && !browserSocket.destroyed) {
|
|
195
|
+
sendWsClose(browserSocket, msg.ws_code || 1000, msg.ws_reason || '');
|
|
196
|
+
browserSocket.end();
|
|
197
|
+
}
|
|
198
|
+
state.activeWebSockets.delete(msg.ws_id || '');
|
|
199
|
+
log(`WS ${msg.ws_id}: closed by provider (code=${msg.ws_code || 1000})`);
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
case 'ws_open': {
|
|
203
|
+
// Provider failed to open backend WS
|
|
204
|
+
if (msg.message) {
|
|
205
|
+
const browserSocket = state.activeWebSockets.get(msg.ws_id || '');
|
|
206
|
+
if (browserSocket && !browserSocket.destroyed) {
|
|
207
|
+
sendWsClose(browserSocket, 1011, msg.message);
|
|
208
|
+
browserSocket.end();
|
|
209
|
+
}
|
|
210
|
+
state.activeWebSockets.delete(msg.ws_id || '');
|
|
211
|
+
warn(`WS ${msg.ws_id}: provider failed: ${msg.message}`);
|
|
212
|
+
}
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
175
215
|
default:
|
|
176
216
|
// Unrecognized unsolicited message — ignore
|
|
177
217
|
break;
|
|
@@ -259,9 +299,11 @@ function startHttpProxy() {
|
|
|
259
299
|
}, HTTP_TIMEOUT_MS);
|
|
260
300
|
// Forward response back to browser
|
|
261
301
|
const respHeaders = { ...(resp.headers || {}) };
|
|
262
|
-
// Remove hop-by-hop headers
|
|
302
|
+
// Remove hop-by-hop and size headers (body may differ after P2P relay)
|
|
263
303
|
delete respHeaders['transfer-encoding'];
|
|
264
304
|
delete respHeaders['connection'];
|
|
305
|
+
delete respHeaders['content-length'];
|
|
306
|
+
delete respHeaders['content-encoding'];
|
|
265
307
|
res.writeHead(resp.status || 200, respHeaders);
|
|
266
308
|
res.end(resp.body || '');
|
|
267
309
|
}
|
|
@@ -270,6 +312,8 @@ function startHttpProxy() {
|
|
|
270
312
|
res.end(JSON.stringify({ error: e.message }));
|
|
271
313
|
}
|
|
272
314
|
});
|
|
315
|
+
// Enable WebSocket tunneling on the same server
|
|
316
|
+
setupWebSocketProxy(server);
|
|
273
317
|
server.on('error', (err) => {
|
|
274
318
|
if (!state.httpServer) {
|
|
275
319
|
reject(err);
|
|
@@ -284,6 +328,172 @@ function startHttpProxy() {
|
|
|
284
328
|
});
|
|
285
329
|
});
|
|
286
330
|
}
|
|
331
|
+
// --- 4b. WebSocket tunnel (RFC 6455 minimal codec) ---
|
|
332
|
+
function wsAcceptKey(clientKey) {
|
|
333
|
+
return createHash('sha1')
|
|
334
|
+
.update(clientKey + '258EAFA5-E914-47DA-95CA-5AB5DB63F35E')
|
|
335
|
+
.digest('base64');
|
|
336
|
+
}
|
|
337
|
+
function buildWsFrame(data, opcode) {
|
|
338
|
+
const len = data.length;
|
|
339
|
+
let header;
|
|
340
|
+
if (len < 126) {
|
|
341
|
+
header = Buffer.alloc(2);
|
|
342
|
+
header[0] = 0x80 | opcode;
|
|
343
|
+
header[1] = len;
|
|
344
|
+
}
|
|
345
|
+
else if (len < 65536) {
|
|
346
|
+
header = Buffer.alloc(4);
|
|
347
|
+
header[0] = 0x80 | opcode;
|
|
348
|
+
header[1] = 126;
|
|
349
|
+
header.writeUInt16BE(len, 2);
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
header = Buffer.alloc(10);
|
|
353
|
+
header[0] = 0x80 | opcode;
|
|
354
|
+
header[1] = 127;
|
|
355
|
+
header.writeUInt32BE(0, 2);
|
|
356
|
+
header.writeUInt32BE(len, 6);
|
|
357
|
+
}
|
|
358
|
+
return Buffer.concat([header, data]);
|
|
359
|
+
}
|
|
360
|
+
function sendWsClose(socket, code, reason) {
|
|
361
|
+
const reasonBuf = Buffer.from(reason, 'utf-8');
|
|
362
|
+
const payload = Buffer.alloc(2 + reasonBuf.length);
|
|
363
|
+
payload.writeUInt16BE(code, 0);
|
|
364
|
+
reasonBuf.copy(payload, 2);
|
|
365
|
+
try {
|
|
366
|
+
socket.write(buildWsFrame(payload, 0x08));
|
|
367
|
+
}
|
|
368
|
+
catch { }
|
|
369
|
+
}
|
|
370
|
+
class WsFrameParser {
|
|
371
|
+
buf = Buffer.alloc(0);
|
|
372
|
+
onFrame = () => { };
|
|
373
|
+
feed(chunk) {
|
|
374
|
+
this.buf = Buffer.concat([this.buf, chunk]);
|
|
375
|
+
while (this.parseOne()) { }
|
|
376
|
+
}
|
|
377
|
+
parseOne() {
|
|
378
|
+
if (this.buf.length < 2)
|
|
379
|
+
return false;
|
|
380
|
+
const byte1 = this.buf[1];
|
|
381
|
+
const masked = (byte1 & 0x80) !== 0;
|
|
382
|
+
let payloadLen = byte1 & 0x7f;
|
|
383
|
+
let offset = 2;
|
|
384
|
+
if (payloadLen === 126) {
|
|
385
|
+
if (this.buf.length < 4)
|
|
386
|
+
return false;
|
|
387
|
+
payloadLen = this.buf.readUInt16BE(2);
|
|
388
|
+
offset = 4;
|
|
389
|
+
}
|
|
390
|
+
else if (payloadLen === 127) {
|
|
391
|
+
if (this.buf.length < 10)
|
|
392
|
+
return false;
|
|
393
|
+
payloadLen = this.buf.readUInt32BE(6);
|
|
394
|
+
offset = 10;
|
|
395
|
+
}
|
|
396
|
+
const maskSize = masked ? 4 : 0;
|
|
397
|
+
const totalLen = offset + maskSize + payloadLen;
|
|
398
|
+
if (this.buf.length < totalLen)
|
|
399
|
+
return false;
|
|
400
|
+
const opcode = this.buf[0] & 0x0f;
|
|
401
|
+
let payload;
|
|
402
|
+
if (masked) {
|
|
403
|
+
const mask = this.buf.subarray(offset, offset + 4);
|
|
404
|
+
payload = Buffer.alloc(payloadLen);
|
|
405
|
+
for (let i = 0; i < payloadLen; i++) {
|
|
406
|
+
payload[i] = this.buf[offset + 4 + i] ^ mask[i & 3];
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
payload = Buffer.from(this.buf.subarray(offset, offset + payloadLen));
|
|
411
|
+
}
|
|
412
|
+
this.buf = Buffer.from(this.buf.subarray(totalLen));
|
|
413
|
+
this.onFrame(opcode, payload);
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
function setupWebSocketProxy(server) {
|
|
418
|
+
server.on('upgrade', (req, socket, head) => {
|
|
419
|
+
if (!state.sessionId || state.shuttingDown) {
|
|
420
|
+
socket.end('HTTP/1.1 503 Service Unavailable\r\n\r\n');
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const wsKey = req.headers['sec-websocket-key'];
|
|
424
|
+
if (!wsKey) {
|
|
425
|
+
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const wsId = randomBytes(4).toString('hex');
|
|
429
|
+
const path = req.url || '/';
|
|
430
|
+
const protocols = req.headers['sec-websocket-protocol']
|
|
431
|
+
? req.headers['sec-websocket-protocol'].split(',').map(s => s.trim())
|
|
432
|
+
: undefined;
|
|
433
|
+
log(`WS ${wsId}: upgrade ${path}`);
|
|
434
|
+
// Complete handshake with browser
|
|
435
|
+
const lines = [
|
|
436
|
+
'HTTP/1.1 101 Switching Protocols',
|
|
437
|
+
'Upgrade: websocket',
|
|
438
|
+
'Connection: Upgrade',
|
|
439
|
+
`Sec-WebSocket-Accept: ${wsAcceptKey(wsKey)}`,
|
|
440
|
+
];
|
|
441
|
+
if (protocols && protocols.length > 0)
|
|
442
|
+
lines.push(`Sec-WebSocket-Protocol: ${protocols[0]}`);
|
|
443
|
+
socket.write(lines.join('\r\n') + '\r\n\r\n');
|
|
444
|
+
state.activeWebSockets.set(wsId, socket);
|
|
445
|
+
// Tell provider to open backend WS
|
|
446
|
+
state.node.send(state.socket, {
|
|
447
|
+
type: 'ws_open',
|
|
448
|
+
id: wsId,
|
|
449
|
+
ws_id: wsId,
|
|
450
|
+
session_id: state.sessionId,
|
|
451
|
+
ws_path: path,
|
|
452
|
+
ws_protocols: protocols,
|
|
453
|
+
});
|
|
454
|
+
// Parse frames from browser → relay to provider
|
|
455
|
+
const parser = new WsFrameParser();
|
|
456
|
+
parser.onFrame = (opcode, payload) => {
|
|
457
|
+
if (opcode === 0x08) { // close
|
|
458
|
+
const code = payload.length >= 2 ? payload.readUInt16BE(0) : 1000;
|
|
459
|
+
state.node.send(state.socket, { type: 'ws_close', id: wsId, ws_id: wsId, ws_code: code, ws_reason: payload.length > 2 ? payload.subarray(2).toString('utf-8') : '' });
|
|
460
|
+
state.activeWebSockets.delete(wsId);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (opcode === 0x09) { // ping → pong
|
|
464
|
+
try {
|
|
465
|
+
socket.write(buildWsFrame(payload, 0x0a));
|
|
466
|
+
}
|
|
467
|
+
catch { }
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (opcode === 0x0a)
|
|
471
|
+
return; // pong — ignore
|
|
472
|
+
// text (0x01) or binary (0x02)
|
|
473
|
+
const isText = opcode === 0x01;
|
|
474
|
+
state.node.send(state.socket, {
|
|
475
|
+
type: 'ws_message',
|
|
476
|
+
id: wsId,
|
|
477
|
+
ws_id: wsId,
|
|
478
|
+
data: isText ? payload.toString('utf-8') : payload.toString('base64'),
|
|
479
|
+
ws_frame_type: isText ? 'text' : 'binary',
|
|
480
|
+
});
|
|
481
|
+
};
|
|
482
|
+
socket.on('data', (chunk) => parser.feed(chunk));
|
|
483
|
+
if (head.length > 0)
|
|
484
|
+
parser.feed(head);
|
|
485
|
+
socket.on('close', () => {
|
|
486
|
+
if (state.activeWebSockets.has(wsId)) {
|
|
487
|
+
state.activeWebSockets.delete(wsId);
|
|
488
|
+
try {
|
|
489
|
+
state.node.send(state.socket, { type: 'ws_close', id: wsId, ws_id: wsId, ws_code: 1001, ws_reason: 'Browser disconnected' });
|
|
490
|
+
}
|
|
491
|
+
catch { }
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
socket.on('error', () => { state.activeWebSockets.delete(wsId); });
|
|
495
|
+
});
|
|
496
|
+
}
|
|
287
497
|
// --- 5. CLI REPL ---
|
|
288
498
|
function startRepl() {
|
|
289
499
|
// Skip REPL when stdin is not a TTY (e.g. background process, piped input)
|
|
@@ -494,6 +704,12 @@ async function endSession() {
|
|
|
494
704
|
state.pendingRequests.delete(id);
|
|
495
705
|
}
|
|
496
706
|
state.chunkBuffers.clear();
|
|
707
|
+
// Close all WebSocket tunnels
|
|
708
|
+
for (const [wsId, socket] of state.activeWebSockets) {
|
|
709
|
+
sendWsClose(socket, 1001, 'Session ending');
|
|
710
|
+
socket.end();
|
|
711
|
+
}
|
|
712
|
+
state.activeWebSockets.clear();
|
|
497
713
|
// Send session_end
|
|
498
714
|
if (state.node && state.socket && state.sessionId) {
|
|
499
715
|
const duration = elapsedSeconds();
|
package/dist/swarm.d.ts
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
import Hyperswarm from 'hyperswarm';
|
|
25
25
|
import { EventEmitter } from 'events';
|
|
26
26
|
export interface SwarmMessage {
|
|
27
|
-
type: 'request' | 'accepted' | 'chunk' | 'result' | 'error' | 'payment' | 'payment_ack' | 'offer' | 'pay_required' | 'stop' | 'skill_request' | 'skill_response' | 'session_start' | 'session_ack' | 'session_tick' | 'session_tick_ack' | 'session_end' | 'http_request' | 'http_response';
|
|
27
|
+
type: 'request' | 'accepted' | 'chunk' | 'result' | 'error' | 'payment' | 'payment_ack' | 'offer' | 'pay_required' | 'stop' | 'skill_request' | 'skill_response' | 'session_start' | 'session_ack' | 'session_tick' | 'session_tick_ack' | 'session_end' | 'http_request' | 'http_response' | 'ws_open' | 'ws_message' | 'ws_close';
|
|
28
28
|
id: string;
|
|
29
29
|
kind?: number;
|
|
30
30
|
input?: string;
|
|
@@ -52,6 +52,12 @@ export interface SwarmMessage {
|
|
|
52
52
|
status?: number;
|
|
53
53
|
chunk_index?: number;
|
|
54
54
|
chunk_total?: number;
|
|
55
|
+
ws_id?: string;
|
|
56
|
+
ws_path?: string;
|
|
57
|
+
ws_protocols?: string[];
|
|
58
|
+
ws_frame_type?: 'text' | 'binary';
|
|
59
|
+
ws_code?: number;
|
|
60
|
+
ws_reason?: string;
|
|
55
61
|
}
|
|
56
62
|
/**
|
|
57
63
|
* Create a deterministic topic hash from a service kind number.
|
package/dist/swarm.js
CHANGED
|
@@ -51,6 +51,7 @@ export class SwarmNode extends EventEmitter {
|
|
|
51
51
|
const combined = existing + buf.toString();
|
|
52
52
|
const lines = combined.split('\n');
|
|
53
53
|
// Last element is incomplete (or empty after trailing newline)
|
|
54
|
+
// Last element is incomplete (or empty after trailing newline)
|
|
54
55
|
this.buffers.set(peerId, lines.pop());
|
|
55
56
|
for (const line of lines) {
|
|
56
57
|
if (!line.trim())
|