220 lines
7.4 KiB
JavaScript
220 lines
7.4 KiB
JavaScript
/**
|
|
* Framed stream adapter for BridgeSwarm connections.
|
|
* Presents a stream interface compatible with Protomux: message boundaries are preserved
|
|
* (each chunk from the native host is one decrypted frame from NoiseSecretStream).
|
|
* Buffers and parses 3-byte little-endian length-prefixed frames in case the bridge
|
|
* ever delivers coalesced or split frames.
|
|
*/
|
|
(function (global) {
|
|
var FRAME_HEADER = 3; // 3-byte LE length prefix (same as @hyperswarm/secret-stream)
|
|
|
|
function readUint24LE(buf, offset) {
|
|
return (buf[offset] | (buf[offset + 1] << 8) | (buf[offset + 2] << 16)) >>> 0;
|
|
}
|
|
|
|
function writeUint24LE(n, buf, offset) {
|
|
buf[offset] = n & 255;
|
|
buf[offset + 1] = (n >>> 8) & 255;
|
|
buf[offset + 2] = (n >>> 16) & 255;
|
|
}
|
|
|
|
function EventEmitter() {
|
|
this._listeners = {};
|
|
}
|
|
EventEmitter.prototype.on = function (ev, fn) {
|
|
if (!this._listeners[ev]) this._listeners[ev] = [];
|
|
this._listeners[ev].push(fn);
|
|
return this;
|
|
};
|
|
EventEmitter.prototype.off = function (ev, fn) {
|
|
if (!this._listeners[ev]) return this;
|
|
this._listeners[ev] = this._listeners[ev].filter(function (f) { return f !== fn; });
|
|
return this;
|
|
};
|
|
EventEmitter.prototype.emit = function (ev) {
|
|
var args = Array.prototype.slice.call(arguments, 1);
|
|
(this._listeners[ev] || []).forEach(function (fn) {
|
|
try { fn.apply(null, args); } catch (_) {}
|
|
});
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Wraps a BridgeSwarmConnection (or any duplex with write/on('data')/on('end')/destroy)
|
|
* and exposes a Protomux-compatible stream: preserves message boundaries, and implements
|
|
* .destroyed, .end(), .pause(), .resume(), 'drain', 'close'.
|
|
* Incoming data is buffered and split by 3-byte LE length prefix; if the underlying
|
|
* connection already delivers one chunk per frame (as with the native host), each chunk
|
|
* is treated as one frame when it's not length-prefixed (length prefix is only used when
|
|
* we have enough bytes to parse it and the chunk doesn't look like a raw frame).
|
|
*/
|
|
function FramedStreamAdapter(conn) {
|
|
EventEmitter.call(this);
|
|
this.conn = conn;
|
|
this.destroyed = false;
|
|
this._paused = false;
|
|
this._buffer = null;
|
|
this._bufferOffset = 0;
|
|
this._frameLength = 0;
|
|
this._readingLength = true;
|
|
this._boundOnData = this._onData.bind(this);
|
|
this._boundOnEnd = this._onEnd.bind(this);
|
|
this._boundOnError = this._onError.bind(this);
|
|
|
|
var self = this;
|
|
this.conn.on('data', this._boundOnData);
|
|
this.conn.on('end', this._boundOnEnd);
|
|
this.conn.on('error', this._boundOnError);
|
|
}
|
|
|
|
FramedStreamAdapter.prototype = Object.create(EventEmitter.prototype);
|
|
|
|
FramedStreamAdapter.prototype._onData = function (chunk) {
|
|
if (this.destroyed || this._paused) return;
|
|
var buf = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
|
|
if (buf.length === 0) return;
|
|
|
|
if (!this._buffer) {
|
|
this._buffer = buf;
|
|
this._bufferOffset = 0;
|
|
} else {
|
|
var total = this._bufferOffset + (this._buffer.length - this._bufferOffset) + buf.length;
|
|
var combined = new Uint8Array(total);
|
|
combined.set(this._buffer.subarray(this._bufferOffset));
|
|
combined.set(buf, this._buffer.length - this._bufferOffset);
|
|
this._buffer = combined;
|
|
this._bufferOffset = 0;
|
|
}
|
|
|
|
while (this._bufferOffset < this._buffer.length && !this.destroyed) {
|
|
if (this._readingLength) {
|
|
if (this._buffer.length - this._bufferOffset < FRAME_HEADER) break;
|
|
this._frameLength = readUint24LE(this._buffer, this._bufferOffset);
|
|
this._bufferOffset += FRAME_HEADER;
|
|
this._readingLength = false;
|
|
}
|
|
if (this._buffer.length - this._bufferOffset < this._frameLength) break;
|
|
var frame = this._buffer.slice(this._bufferOffset, this._bufferOffset + this._frameLength);
|
|
this._bufferOffset += this._frameLength;
|
|
this._readingLength = true;
|
|
this._frameLength = 0;
|
|
this.emit('data', frame);
|
|
}
|
|
|
|
if (this._bufferOffset >= this._buffer.length) {
|
|
this._buffer = null;
|
|
this._bufferOffset = 0;
|
|
} else if (this._bufferOffset > 0) {
|
|
this._buffer = this._buffer.subarray(this._bufferOffset);
|
|
this._bufferOffset = 0;
|
|
}
|
|
};
|
|
|
|
FramedStreamAdapter.prototype._onEnd = function () {
|
|
if (this.destroyed) return;
|
|
this.emit('end');
|
|
};
|
|
|
|
FramedStreamAdapter.prototype._onError = function (err) {
|
|
if (this.destroyed) return;
|
|
this.emit('error', err);
|
|
};
|
|
|
|
FramedStreamAdapter.prototype.write = function (data) {
|
|
if (this.destroyed) return false;
|
|
var buf = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
this.conn.write(buf);
|
|
var self = this;
|
|
if (typeof setImmediate !== 'undefined') {
|
|
setImmediate(function () { self.emit('drain'); });
|
|
} else {
|
|
setTimeout(function () { self.emit('drain'); }, 0);
|
|
}
|
|
return true;
|
|
};
|
|
|
|
FramedStreamAdapter.prototype.end = function () {
|
|
if (this.destroyed) return;
|
|
this.conn.destroy();
|
|
};
|
|
|
|
FramedStreamAdapter.prototype.pause = function () {
|
|
this._paused = true;
|
|
};
|
|
|
|
FramedStreamAdapter.prototype.resume = function () {
|
|
this._paused = false;
|
|
};
|
|
|
|
FramedStreamAdapter.prototype.destroy = function (err) {
|
|
if (this.destroyed) return;
|
|
this.destroyed = true;
|
|
this.conn.off('data', this._boundOnData);
|
|
this.conn.off('end', this._boundOnEnd);
|
|
this.conn.off('error', this._boundOnError);
|
|
this.conn.destroy();
|
|
if (err) this.emit('error', err);
|
|
this.emit('close');
|
|
};
|
|
|
|
/**
|
|
* Wrap a connection that delivers raw frames (one chunk = one frame, no length prefix).
|
|
* Use this when the native host sends one event per NoiseSecretStream message.
|
|
*/
|
|
function wrapRawFrames(conn) {
|
|
var stream = new EventEmitter();
|
|
stream.conn = conn;
|
|
stream.destroyed = false;
|
|
stream._paused = false;
|
|
stream.write = function (data) {
|
|
if (stream.destroyed) return false;
|
|
var buf = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
conn.write(buf);
|
|
var s = stream;
|
|
setTimeout(function () { s.emit('drain'); }, 0);
|
|
return true;
|
|
};
|
|
stream.end = function () {
|
|
if (stream.destroyed) return;
|
|
conn.destroy();
|
|
};
|
|
stream.pause = function () { stream._paused = true; };
|
|
stream.resume = function () { stream._paused = false; };
|
|
stream.destroy = function (err) {
|
|
if (stream.destroyed) return;
|
|
stream.destroyed = true;
|
|
conn.destroy();
|
|
if (err) stream.emit('error', err);
|
|
stream.emit('close');
|
|
};
|
|
conn.on('data', function (chunk) {
|
|
if (!stream._paused && !stream.destroyed) stream.emit('data', chunk);
|
|
});
|
|
conn.on('end', function () {
|
|
if (stream.destroyed) return;
|
|
stream.emit('end');
|
|
stream.emit('close');
|
|
});
|
|
conn.on('error', function (e) { if (!stream.destroyed) stream.emit('error', e); });
|
|
return stream;
|
|
}
|
|
|
|
/**
|
|
* Wrap a connection with length-prefixed framing (3-byte LE).
|
|
* Use when you need to send/receive framed messages; outgoing writes get a length prefix.
|
|
*/
|
|
function wrapLengthPrefixed(conn) {
|
|
return new FramedStreamAdapter(conn);
|
|
}
|
|
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = { FramedStreamAdapter, wrapRawFrames, wrapLengthPrefixed };
|
|
} else {
|
|
global.BridgeSwarmFramedStream = {
|
|
FramedStreamAdapter: FramedStreamAdapter,
|
|
wrapRawFrames: wrapRawFrames,
|
|
wrapLengthPrefixed: wrapLengthPrefixed
|
|
};
|
|
}
|
|
})(typeof window !== 'undefined' ? window : typeof self !== 'undefined' ? self : this);
|