release(v4.8.2): per-from receive serialization + per-connection bridge dedup
Some checks failed
Test / test (push) Has been cancelled
Some checks failed
Test / test (push) Has been cancelled
Two interlocking robustness fixes for the duplicate-fan-out / first-contact
class of failures Prism reported.
1. `Shade.receive(from, env)` now queues its `manager.decrypt` step
per `from` so concurrent dispatches can't race the SessionManager
ratchet or the StorageProvider (sqlite "database is locked", IDB
transaction conflicts). User message handlers run *outside* the
queue so streams + file-RPC's nested `shade.receive` calls don't
self-deadlock.
2. Bridge WS + SSE handlers now run a per-connection bounded msgId
LRU as defense-in-depth against any flushTo re-entry (event-storm,
future refactor). Pending-flush chains are wrapped in `.catch(() =>
{})` so a transient `ws.send` rejection no longer poisons the
connection's flush loop.
Tests: storming `inbox.blob_stored` 10× per PUT yields exactly one WS/
SSE frame; 8 concurrent `bob.receive('alice', envelope)` calls keep
the ratchet intact and never surface "database is locked".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@shade/transport-bridge",
|
||||
"version": "4.8.1",
|
||||
"version": "4.8.2",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
|
||||
@@ -951,3 +951,80 @@ describe('Sender attribution — bridge push surfaces IncomingMessage.from', ()
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── V4.8.2 — per-connection msgId dedup (Prism FR: duplicate fan-out) ─
|
||||
|
||||
describe('Bridge dedup — single PUT yields exactly one push per connection', () => {
|
||||
test('WS: storming inbox.blob_stored does not duplicate frames for one msgId', async () => {
|
||||
const h = await bootstrap();
|
||||
try {
|
||||
const received: IncomingMessage[] = [];
|
||||
const bridge = new WsBridge({
|
||||
baseUrl: h.baseUrl,
|
||||
auth: bobAuth(h),
|
||||
connectTimeoutMs: 2_000,
|
||||
disableAutoReconnect: true,
|
||||
});
|
||||
await bridge.connect({ onMessage: (m) => received.push(m) });
|
||||
try {
|
||||
// One real PUT + replay the inbox.blob_stored event ten times to
|
||||
// simulate any future code path (or external bug) that double-
|
||||
// fires the trigger. The cursor in flushTo would already cover
|
||||
// the happy case, but the per-connection LRU is the explicit
|
||||
// dedup gate that survives even if cursor logic regresses.
|
||||
const msgId = await putBlob(h, rand(48));
|
||||
for (let i = 0; i < 10; i++) {
|
||||
h.events.emit('inbox.blob_stored', {
|
||||
address: 'bob',
|
||||
msgId,
|
||||
bytes: 48,
|
||||
ttlSeconds: 60,
|
||||
});
|
||||
}
|
||||
await waitFor(() => received.length >= 1, 2_000);
|
||||
// Give any stragglers a chance to arrive and inflate the count.
|
||||
await new Promise((r) => setTimeout(r, 250));
|
||||
expect(received.length).toBe(1);
|
||||
expect(received[0]!.msgId).toBe(msgId);
|
||||
} finally {
|
||||
await bridge.disconnect();
|
||||
}
|
||||
} finally {
|
||||
h.server.stop(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('SSE: same dedup contract', async () => {
|
||||
const h = await bootstrap();
|
||||
try {
|
||||
const received: IncomingMessage[] = [];
|
||||
const bridge = new SseBridge({
|
||||
baseUrl: h.baseUrl,
|
||||
auth: bobAuth(h),
|
||||
initialBackoffMs: 50,
|
||||
maxBackoffMs: 200,
|
||||
disableAutoReconnect: true,
|
||||
});
|
||||
await bridge.connect({ onMessage: (m) => received.push(m) });
|
||||
try {
|
||||
const msgId = await putBlob(h, rand(48));
|
||||
for (let i = 0; i < 10; i++) {
|
||||
h.events.emit('inbox.blob_stored', {
|
||||
address: 'bob',
|
||||
msgId,
|
||||
bytes: 48,
|
||||
ttlSeconds: 60,
|
||||
});
|
||||
}
|
||||
await waitFor(() => received.length >= 1, 2_000);
|
||||
await new Promise((r) => setTimeout(r, 250));
|
||||
expect(received.length).toBe(1);
|
||||
expect(received[0]!.msgId).toBe(msgId);
|
||||
} finally {
|
||||
await bridge.disconnect();
|
||||
}
|
||||
} finally {
|
||||
h.server.stop(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user