Files
BridgeSwarm/extension/framed-stream.js
T
2026-02-12 03:27:05 -05:00

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);