summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/transport-codec.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/transport-codec.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transport-codec.js224
1 files changed, 224 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport-codec.js b/packages/meshbay-hub/src/meshbay_hub/static/transport-codec.js
new file mode 100644
index 0000000..eba0461
--- /dev/null
+++ b/packages/meshbay-hub/src/meshbay_hub/static/transport-codec.js
@@ -0,0 +1,224 @@
+// The wire codec: the subset of msgpack MNP uses (maps, strings, integers,
+// binary, arrays, null), and hex for the ids the transport mints.
+
+// ── Minimal msgpack encode/decode ────────────────────────────────────────────
+// Covers the subset used by MNP: maps, strings, integers, binary, arrays, null.
+
+function msgpack_encode(obj) {
+ const parts = [];
+ _encodeValue(obj, parts);
+ const total = parts.reduce((s, p) => s + p.length, 0);
+ const result = new Uint8Array(total);
+ let off = 0;
+ for (const p of parts) { result.set(p, off); off += p.length; }
+ return result;
+}
+
+function _encodeValue(val, parts) {
+ if (val === null || val === undefined) {
+ parts.push(new Uint8Array([0xc0]));
+ } else if (typeof val === 'boolean') {
+ parts.push(new Uint8Array([val ? 0xc3 : 0xc2]));
+ } else if (typeof val === 'number') {
+ if (Number.isInteger(val)) {
+ if (val >= 0 && val <= 127) {
+ parts.push(new Uint8Array([val]));
+ } else if (val >= 0 && val <= 0xff) {
+ parts.push(new Uint8Array([0xcc, val]));
+ } else if (val >= 0 && val <= 0xffff) {
+ const b = new Uint8Array(3); b[0] = 0xcd;
+ new DataView(b.buffer).setUint16(1, val, false);
+ parts.push(b);
+ } else if (val >= 0 && val <= 0xffffffff) {
+ const b = new Uint8Array(5); b[0] = 0xce;
+ new DataView(b.buffer).setUint32(1, val, false);
+ parts.push(b);
+ } else if (val >= 0 && val <= Number.MAX_SAFE_INTEGER) {
+ // Same split as the 0xcf decoder case above, in reverse — without
+ // this, a value over 0xffffffff fell to the plain int32 branch
+ // below and silently wrapped to a wrong, unrelated number instead
+ // of failing loudly.
+ const b = new Uint8Array(9); b[0] = 0xcf;
+ const dv = new DataView(b.buffer);
+ dv.setUint32(1, Math.floor(val / 4294967296), false);
+ dv.setUint32(5, val % 4294967296, false);
+ parts.push(b);
+ } else if (val >= -32 && val < 0) {
+ parts.push(new Uint8Array([val & 0xff]));
+ } else if (val >= -128 && val < 0) {
+ const b = new Uint8Array(2); b[0] = 0xd0; b[1] = val & 0xff;
+ parts.push(b);
+ } else {
+ const b = new Uint8Array(5); b[0] = 0xd2;
+ new DataView(b.buffer).setInt32(1, val, false);
+ parts.push(b);
+ }
+ } else {
+ const b = new Uint8Array(9); b[0] = 0xcb;
+ new DataView(b.buffer).setFloat64(1, val, false);
+ parts.push(b);
+ }
+ } else if (typeof val === 'string') {
+ const encoded = new TextEncoder().encode(val);
+ if (encoded.length <= 31) {
+ parts.push(new Uint8Array([0xa0 | encoded.length]));
+ } else if (encoded.length <= 0xff) {
+ parts.push(new Uint8Array([0xd9, encoded.length]));
+ } else if (encoded.length <= 0xffff) {
+ const b = new Uint8Array(3); b[0] = 0xda;
+ new DataView(b.buffer).setUint16(1, encoded.length, false);
+ parts.push(b);
+ } else {
+ const b = new Uint8Array(5); b[0] = 0xdb;
+ new DataView(b.buffer).setUint32(1, encoded.length, false);
+ parts.push(b);
+ }
+ parts.push(encoded);
+ } else if (val instanceof Uint8Array) {
+ if (val.length <= 0xff) {
+ parts.push(new Uint8Array([0xc4, val.length]));
+ } else if (val.length <= 0xffff) {
+ const b = new Uint8Array(3); b[0] = 0xc5;
+ new DataView(b.buffer).setUint16(1, val.length, false);
+ parts.push(b);
+ } else {
+ const b = new Uint8Array(5); b[0] = 0xc6;
+ new DataView(b.buffer).setUint32(1, val.length, false);
+ parts.push(b);
+ }
+ parts.push(val);
+ } else if (Array.isArray(val)) {
+ if (val.length <= 15) {
+ parts.push(new Uint8Array([0x90 | val.length]));
+ } else if (val.length <= 0xffff) {
+ const b = new Uint8Array(3); b[0] = 0xdc;
+ new DataView(b.buffer).setUint16(1, val.length, false);
+ parts.push(b);
+ } else {
+ const b = new Uint8Array(5); b[0] = 0xdd;
+ new DataView(b.buffer).setUint32(1, val.length, false);
+ parts.push(b);
+ }
+ for (const item of val) _encodeValue(item, parts);
+ } else if (typeof val === 'object') {
+ const keys = Object.keys(val);
+ if (keys.length <= 15) {
+ parts.push(new Uint8Array([0x80 | keys.length]));
+ } else if (keys.length <= 0xffff) {
+ const b = new Uint8Array(3); b[0] = 0xde;
+ new DataView(b.buffer).setUint16(1, keys.length, false);
+ parts.push(b);
+ } else {
+ const b = new Uint8Array(5); b[0] = 0xdf;
+ new DataView(b.buffer).setUint32(1, keys.length, false);
+ parts.push(b);
+ }
+ for (const k of keys) {
+ _encodeValue(k, parts);
+ _encodeValue(val[k], parts);
+ }
+ }
+}
+
+function msgpack_decode(buf) {
+ const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
+ const [val] = _decodeValue(buf, view, 0);
+ return val;
+}
+
+function _decodeValue(buf, view, offset) {
+ const byte = buf[offset];
+
+ if (byte <= 0x7f) return [byte, offset + 1];
+ if ((byte & 0xe0) === 0xe0) return [byte - 256, offset + 1];
+ if ((byte & 0xa0) === 0xa0) {
+ const len = byte & 0x1f;
+ return [new TextDecoder().decode(buf.slice(offset + 1, offset + 1 + len)), offset + 1 + len];
+ }
+ if ((byte & 0xf0) === 0x90) {
+ const len = byte & 0x0f;
+ return _decodeArray(buf, view, offset + 1, len);
+ }
+ if ((byte & 0xf0) === 0x80) {
+ const len = byte & 0x0f;
+ return _decodeMap(buf, view, offset + 1, len);
+ }
+
+ switch (byte) {
+ case 0xc0: return [null, offset + 1];
+ case 0xc2: return [false, offset + 1];
+ case 0xc3: return [true, offset + 1];
+ case 0xc4: { const len = buf[offset + 1]; return [buf.slice(offset + 2, offset + 2 + len), offset + 2 + len]; }
+ case 0xc5: { const len = view.getUint16(offset + 1, false); return [buf.slice(offset + 3, offset + 3 + len), offset + 3 + len]; }
+ case 0xc6: { const len = view.getUint32(offset + 1, false); return [buf.slice(offset + 5, offset + 5 + len), offset + 5 + len]; }
+ case 0xcc: return [buf[offset + 1], offset + 2];
+ case 0xcd: return [view.getUint16(offset + 1, false), offset + 3];
+ case 0xce: return [view.getUint32(offset + 1, false), offset + 5];
+ // uint64/int64 — never emitted by this file's own encoder (a JS number
+ // above 0xffffffff falls to float64 there), but the node's real msgpack
+ // library sends a plain uint64 for any Python int over ~4.3 billion, and
+ // a raw byte count crosses that easily (found live: IndexProgress.
+ // scanned_bytes/total_bytes in the handshake ack, indexer.py, for a
+ // group whose total library size exceeds ~4 GB). Split into two 32-bit
+ // halves rather than DataView's getBigUint64/getBigInt64 — a BigInt
+ // would silently poison every arithmetic use of these fields elsewhere
+ // (percentage math, comparisons) — and every real byte count fits in a
+ // plain JS number well under Number.MAX_SAFE_INTEGER (2^53).
+ case 0xcf: {
+ const hi = view.getUint32(offset + 1, false);
+ const lo = view.getUint32(offset + 5, false);
+ return [hi * 4294967296 + lo, offset + 9];
+ }
+ case 0xd3: {
+ const hi = view.getInt32(offset + 1, false);
+ const lo = view.getUint32(offset + 5, false);
+ return [hi * 4294967296 + lo, offset + 9];
+ }
+ case 0xcb: return [view.getFloat64(offset + 1, false), offset + 9];
+ case 0xd0: return [view.getInt8(offset + 1), offset + 2];
+ case 0xd1: return [view.getInt16(offset + 1, false), offset + 3];
+ case 0xd2: return [view.getInt32(offset + 1, false), offset + 5];
+ case 0xd9: {
+ const len = buf[offset + 1];
+ return [new TextDecoder().decode(buf.slice(offset + 2, offset + 2 + len)), offset + 2 + len];
+ }
+ case 0xda: {
+ const len = view.getUint16(offset + 1, false);
+ return [new TextDecoder().decode(buf.slice(offset + 3, offset + 3 + len)), offset + 3 + len];
+ }
+ case 0xdb: {
+ const len = view.getUint32(offset + 1, false);
+ return [new TextDecoder().decode(buf.slice(offset + 5, offset + 5 + len)), offset + 5 + len];
+ }
+ case 0xdc: { const len = view.getUint16(offset + 1, false); return _decodeArray(buf, view, offset + 3, len); }
+ case 0xdd: { const len = view.getUint32(offset + 1, false); return _decodeArray(buf, view, offset + 5, len); }
+ case 0xde: { const len = view.getUint16(offset + 1, false); return _decodeMap(buf, view, offset + 3, len); }
+ case 0xdf: { const len = view.getUint32(offset + 1, false); return _decodeMap(buf, view, offset + 5, len); }
+ default: throw new Error(`Unknown msgpack type: 0x${byte.toString(16)}`);
+ }
+}
+
+function _decodeArray(buf, view, offset, count) {
+ const arr = [];
+ for (let i = 0; i < count; i++) {
+ const [val, newOff] = _decodeValue(buf, view, offset);
+ arr.push(val);
+ offset = newOff;
+ }
+ return [arr, offset];
+}
+
+function _decodeMap(buf, view, offset, count) {
+ const obj = {};
+ for (let i = 0; i < count; i++) {
+ const [key, off1] = _decodeValue(buf, view, offset);
+ const [val, off2] = _decodeValue(buf, view, off1);
+ obj[key] = val;
+ offset = off2;
+ }
+ return [obj, offset];
+}
+
+function _hex(bytes) {
+ return [...bytes].map(b => b.toString(16).padStart(2, '0')).join('');
+}