aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/transport.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/transport.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transport.js119
1 files changed, 119 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
index da98253..4f6b656 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
@@ -103,8 +103,10 @@ class MeshBayTransport {
set onStreamEnd(fn) { this._onStreamEnd = fn; }
set onStreamError(fn) { this._onStreamError = fn; }
set onIndexSync(fn) { this._onIndexSync = fn; }
+ set onIndexDelta(fn) { this._onIndexDelta = fn; }
set onUploadPolicy(fn) { this._onUploadPolicy = fn; }
set onAppsEnabled(fn) { this._onAppsEnabled = fn; }
+ set onIndexProgress(fn) { this._onIndexProgress = fn; }
get sessionKeys() { return this._sessionKeys; }
@@ -634,6 +636,29 @@ class MeshBayTransport {
return msg;
}
+ /**
+ * How often the node's reconciliation backstop runs, and how long it
+ * waits after a file's last write before hashing it (indexer.py
+ * DirectoryIndexer). Whole seconds only: the node builds the signing
+ * subject with Python's `%g` (drops a trailing ".0"), and the simplest
+ * way to always match it byte-for-byte from JS is to never send a
+ * fractional value in the first place.
+ */
+ async setScanSettings(reconcileIntervalSecs, debounceSecs, signFn) {
+ const reconcile = Math.round(reconcileIntervalSecs);
+ const debounce = Math.round(debounceSecs);
+ const msg = await this._sendAndWait({
+ type: 'set_scan_settings', v: '0.1',
+ reconcile_interval_secs: reconcile, debounce_secs: debounce,
+ });
+ if (msg.type === 'error') throw new Error(msg.detail);
+ if (msg.type === 'admin_challenge') {
+ return this._authorizeAdminOp(
+ msg, 'set_scan_settings', `${reconcile},${debounce}`, signFn);
+ }
+ return msg;
+ }
+
async revokeMember(userId, signFn) {
const msg = await this._sendAndWait({
type: 'member_revoke', v: '0.1', user_id: userId,
@@ -1251,6 +1276,37 @@ class MeshBayTransport {
this._onAppsEnabled(msg.apps || []);
}
+ // The operator's node is scanning — never the entries themselves, just
+ // enough to animate a presence dot. Pushed periodically while it runs,
+ // plus once more on the transition back to idle (daemon.py
+ // _progress_pusher). UNLIKE member_upload_ack/apps_enabled_ack above,
+ // this is never a reply to anything this browser asked for — nobody
+ // calls _sendAndWait for it — so it MUST return here. Falling through
+ // to the "oldest pending" guess below hands it to whatever unrelated
+ // request happens to be waiting (a handshake, a chat history fetch),
+ // which then waits forever for its real answer while this one already
+ // "arrived" — and every message after that is one slot off too. Found
+ // live: a group mid-scan corrupted its own handshake and chat history
+ // this way, arriving roughly every 2s for as long as scanning ran.
+ if (msg.type === 'index_progress') {
+ if (this._onIndexProgress) {
+ this._onIndexProgress({
+ scanning: Boolean(msg.scanning),
+ scanned_bytes: msg.scanned_bytes || 0,
+ total_bytes: msg.total_bytes || 0,
+ });
+ }
+ return;
+ }
+
+ // Same reasoning as index_progress: nobody awaits this one either, it
+ // is purely informational (group-settings.js does not currently act on
+ // it), so it must not be left to fall through to the oldest pending
+ // request.
+ if (msg.type === 'set_scan_settings_ack') {
+ return;
+ }
+
if (msg.type === 'index_sync' && msg.entries) {
if (this._onIndexSync) this._onIndexSync(msg);
const oldest = this._pending.entries().next();
@@ -1260,6 +1316,16 @@ class MeshBayTransport {
return;
}
+ // Incremental update — additions/deletions only, never the whole index.
+ // Only ever arrives after the full index this browser already has (the
+ // node's first push to a newly connected peer is always index_sync, see
+ // daemon.py _broadcast_index_change), so there is always a base to
+ // apply it to.
+ if (msg.type === 'index_delta') {
+ if (this._onIndexDelta) this._onIndexDelta(msg);
+ return;
+ }
+
if (msg.type === 'file_chunk') {
const key = `chunk:${msg.file_id}:${msg.chunk_index}`;
for (const [, handler] of this._pending) {
@@ -1298,6 +1364,29 @@ class MeshBayTransport {
return;
}
+ // chat_hist_resp answers a `chat_hist` request, but under a different
+ // type string — unlike index_sync, which is asked for and answered under
+ // the same name, so the generic fallback below happens to work for it by
+ // accident. Without this check, whenever a chat_hist_resp arrives while
+ // something else this browser asked for (fetchIndex, even the handshake
+ // itself) is still the oldest pending entry, it gets handed to that
+ // instead: the request chat_hist_resp actually belongs to then hangs
+ // until _sendAndWait's own 30s timeout, and whatever it stole from
+ // resolves with the wrong shape entirely — reproduced live as a
+ // consistent ~30s hang immediately after a successful handshake, for one
+ // specific group and not others connected the same way, which is exactly
+ // what depending on response arrival order rather than on request type
+ // predicts: it fires only when the two responses happen to reorder.
+ if (msg.type === 'chat_hist_resp') {
+ const oldest = this._pending.entries().next();
+ if (!oldest.done && oldest.value[1]._reqType === 'chat_hist') {
+ oldest.value[1].resolve(msg);
+ } else {
+ console.warn('[MeshBay] chat_hist_resp with no matching chat_hist pending');
+ }
+ return;
+ }
+
// Everything above is routed by something in the message. What is left is
// matched by arrival order, which is only ever a guess — and a wrong guess
// here hands one request's answer to another, which then waits for a reply
@@ -1349,6 +1438,16 @@ function _encodeValue(val, parts) {
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) {
@@ -1460,6 +1559,26 @@ function _decodeValue(buf, view, offset) {
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];