release(v4.8.4): server-side cross-channel dedup via BridgeDeliveryLog
Some checks failed
Test / test (push) Has been cancelled

V4.8.3 shipped client-side cross-channel dedup hook
(`Inbox.acceptBridgeFrame`), but recipients that didn't migrate to
the new wiring still observed the same envelope twice — once via
WS bridge push, again ~30 s later via inbox-poll. Prism re-verified
the FR after 4.8.3 and asked for a relay-side enforcement so app
code doesn't have to ack-via-DELETE on every bridge frame.

V4.8.4 adds an in-memory `BridgeDeliveryLog` (default 60 s grace,
8192-per-address cap) that records every successful WS / SSE /
long-poll push of `(address, msgId)`. The `/v1/inbox/:addr/fetch`
route filters out blobs in the log's grace window so a recipient
running both a bridge and the 30 s poll cadence sees exactly one
delivery. Cursor advances over the full fetched window so a poll
that straddles a suppressed blob doesn't stall.

The standalone server auto-wires the log between
`createBridgeRoutes` and `createInboxRoutes`. Custom mounts thread
the same instance through `bridgeDeliveryLog` on both factories.

Tests cover WS-then-poll, SSE-then-poll, and a negative control
(non-bridge-pushed blob still comes through inbox-fetch).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 16:31:42 +02:00
parent d47774ef1c
commit a98ea8a1bd
32 changed files with 467 additions and 41 deletions

View File

@@ -47,7 +47,9 @@ interface Harness {
async function bootstrap(opts: { mountWs?: boolean } = {}): Promise<Harness> {
const store = new MemoryInboxStore();
const events = new InboxServerEvents();
const inboxApp = createInboxRoutes(store, crypto, { events, disableRateLimit: true });
// V4.8.4 — share a BridgeDeliveryLog between bridge + inbox routes so
// the inbox-fetch path filters out blobs the bridge already pushed.
// Mirrors the wiring in `@shade/server/standalone.ts`.
const bridge = createBridgeRoutes({
store,
crypto,
@@ -57,6 +59,11 @@ async function bootstrap(opts: { mountWs?: boolean } = {}): Promise<Harness> {
heartbeatIntervalMs: 200,
fallbackPollIntervalMs: 50,
});
const inboxApp = createInboxRoutes(store, crypto, {
events,
disableRateLimit: true,
bridgeDeliveryLog: bridge.bridgeDeliveryLog,
});
const app = new Hono();
app.route('/', inboxApp);
app.route('/', bridge.app);
@@ -952,6 +959,123 @@ describe('Sender attribution — bridge push surfaces IncomingMessage.from', ()
});
});
// ─── V4.8.4 — cross-channel dedup via shared BridgeDeliveryLog ────────
describe('BridgeDeliveryLog — bridge push suppresses subsequent inbox-poll for the same msgId', () => {
test('WS push then /v1/inbox/:addr/fetch: fetch returns 0 blobs but advances cursor', async () => {
// Reproduces the Prism FR `cross-channel-duplicate-fanout-v4.8.2`
// (re-verified on 4.8.3): a single inbox.send was being delivered
// both via the WS bridge AND via the next inbox-poll cycle, the
// duplicate dispatch tripping on already-consumed prekeys.
// V4.8.4's shared BridgeDeliveryLog records every successful
// bridge push and the inbox-fetch route filters those msgIds out
// for the grace window — so a recipient that runs both a bridge
// and a poll cycle observes exactly one delivery.
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 {
const msgId = await putBlob(h, rand(48));
await waitFor(() => received.length === 1, 2_000);
// Give the bridge handler a tick to record the push in the log
// (it happens after the await on ws.send returns).
await new Promise((r) => setTimeout(r, 50));
// Now do a regular inbox-fetch as if the recipient's
// `Inbox.pollOnce` cycle fired. With V4.8.4 wiring, the
// bridge-pushed msgId is filtered out.
const body = await signPayload(crypto, h.bob.signingPrivateKey, {
address: 'bob',
sinceCursor: 0,
});
const res = await fetch(`${h.baseUrl}/v1/inbox/bob/fetch`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
});
const fetchJson = (await res.json()) as { blobs: unknown[]; cursor: number };
expect(fetchJson.blobs.length).toBe(0);
// Cursor advances past the suppressed blob so the next poll
// doesn't re-fetch the same range and stay stuck.
expect(fetchJson.cursor).toBeGreaterThan(0);
expect(received[0]!.msgId).toBe(msgId);
} finally {
await bridge.disconnect();
}
} finally {
h.server.stop(true);
}
});
test('SSE push also records into the log (parity with WS)', 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 {
await putBlob(h, rand(48));
await waitFor(() => received.length === 1, 2_000);
await new Promise((r) => setTimeout(r, 50));
const body = await signPayload(crypto, h.bob.signingPrivateKey, {
address: 'bob',
sinceCursor: 0,
});
const res = await fetch(`${h.baseUrl}/v1/inbox/bob/fetch`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
});
const fetchJson = (await res.json()) as { blobs: unknown[] };
expect(fetchJson.blobs.length).toBe(0);
} finally {
await bridge.disconnect();
}
} finally {
h.server.stop(true);
}
});
test('a non-bridge-pushed msgId is still returned by inbox-fetch', async () => {
// Negative control: blobs that the bridge never pushed (e.g. the
// bridge wasn't connected when the put landed) must still come
// through the inbox-fetch path. The filter is bridge-delivered-
// specific, not a blanket suppression.
const h = await bootstrap();
try {
// No bridge connected.
const msgId = await putBlob(h, rand(48));
const body = await signPayload(crypto, h.bob.signingPrivateKey, {
address: 'bob',
sinceCursor: 0,
});
const res = await fetch(`${h.baseUrl}/v1/inbox/bob/fetch`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
});
const fetchJson = (await res.json()) as { blobs: Array<{ msgId: string }> };
expect(fetchJson.blobs.length).toBe(1);
expect(fetchJson.blobs[0]!.msgId).toBe(msgId);
} finally {
h.server.stop(true);
}
});
});
// ─── V4.8.2 — per-connection msgId dedup (Prism FR: duplicate fan-out) ─
describe('Bridge dedup — single PUT yields exactly one push per connection', () => {