mirror of
https://github.com/block/buzz.git
synced 2026-11-06 09:21:45 +00:00
Signed-off-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
322 lines
10 KiB
JavaScript
322 lines
10 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import {
|
|
PAGE_REPLAY_MAX_ATTEMPTS,
|
|
RECONNECT_REPLAY_PAGE_LIMIT,
|
|
replayLiveSubscriptions,
|
|
} from "./relayReconnectReplay.ts";
|
|
import { buildChannelFilter } from "./relayChannelFilters.ts";
|
|
import { shouldDispatchSubscriptionEvent } from "./relayClosedRecovery.ts";
|
|
|
|
function deferred() {
|
|
let resolve;
|
|
let reject;
|
|
const promise = new Promise((yes, no) => {
|
|
resolve = yes;
|
|
reject = no;
|
|
});
|
|
return { promise, resolve, reject };
|
|
}
|
|
|
|
async function flush() {
|
|
// Drain continuations without clock-based assertions or transport sleeps.
|
|
for (let i = 0; i < 60; i++) await Promise.resolve();
|
|
}
|
|
|
|
function event(id) {
|
|
return Object.freeze({
|
|
id: String(id).padStart(64, "0"),
|
|
pubkey: "a".repeat(64),
|
|
created_at: 3_000,
|
|
kind: 9,
|
|
tags: Object.freeze([]),
|
|
content: "",
|
|
sig: "b".repeat(128),
|
|
});
|
|
}
|
|
|
|
function subscriber(channelId = "channel-1", since = 2_000) {
|
|
const received = [];
|
|
return {
|
|
mode: "live",
|
|
filter: { ...buildChannelFilter(channelId, 1_000), since },
|
|
lastSeenCreatedAt: 3_100,
|
|
onEvent: (row) => received.push(row.id),
|
|
received,
|
|
};
|
|
}
|
|
|
|
function fixture(entries = [subscriber(), subscriber()], options = {}) {
|
|
const subscriptions = new Map(entries.map((sub, i) => [String(i), sub]));
|
|
const reads = [];
|
|
const frames = [];
|
|
const requestRepair = (request) => {
|
|
const pending = deferred();
|
|
reads.push({ request, ...pending });
|
|
return pending.promise;
|
|
};
|
|
const replay = (overrides = {}) =>
|
|
replayLiveSubscriptions({
|
|
subscriptions,
|
|
sendRaw: async (frame) => {
|
|
frames.push(frame);
|
|
},
|
|
requestRepair,
|
|
generation: 1,
|
|
...options,
|
|
...overrides,
|
|
});
|
|
return { entries, subscriptions, reads, frames, replay };
|
|
}
|
|
|
|
test("overlapping repairs share each composite-cursor page, not subscriber delivery", async () => {
|
|
const f = fixture();
|
|
const running = f.replay();
|
|
await flush();
|
|
assert.equal(f.frames.length, 2, "both live REQs must precede repair");
|
|
assert.equal(
|
|
f.reads.length,
|
|
1,
|
|
"one outstanding native read for equal pages",
|
|
);
|
|
// A live frame has already reached only one subscriber. Shared history must
|
|
// dedupe for it, but still deliver that row to the other subscriber.
|
|
const first = Object.freeze(
|
|
Array.from({ length: RECONNECT_REPLAY_PAGE_LIMIT }, (_, i) => event(i + 1)),
|
|
);
|
|
const live = first[0];
|
|
assert.equal(shouldDispatchSubscriptionEvent(f.entries[0], live), true);
|
|
f.entries[0].onEvent(live);
|
|
f.reads[0].resolve(first);
|
|
await flush();
|
|
assert.equal(f.reads.length, 2);
|
|
assert.deepEqual(f.reads[1].request, {
|
|
channelId: "channel-1",
|
|
since: 2_000,
|
|
limit: 500,
|
|
until: 3_000,
|
|
beforeId: first.at(-1).id,
|
|
});
|
|
const last = event(501);
|
|
f.reads[1].resolve(Object.freeze([last]));
|
|
await running;
|
|
const expected = [...first, last].map((row) => row.id);
|
|
for (const sub of f.entries) {
|
|
assert.deepEqual(sub.received, expected);
|
|
assert.equal(sub.pendingReplaySince, undefined);
|
|
assert.equal(sub.reconnectReplay.repairDone, true);
|
|
}
|
|
});
|
|
|
|
for (const [label, second] of [
|
|
["channel", () => subscriber("channel-2")],
|
|
["history floor", () => subscriber("channel-1", 2_100)],
|
|
]) {
|
|
test(`different ${label} requests never share`, async () => {
|
|
const f = fixture([subscriber(), second()]);
|
|
const running = f.replay();
|
|
await flush();
|
|
assert.equal(f.reads.length, 2);
|
|
f.reads[0].resolve([event(1)]);
|
|
f.reads[1].resolve([event(2)]);
|
|
await running;
|
|
assert.deepEqual(f.entries[0].received, [event(1).id]);
|
|
assert.deepEqual(f.entries[1].received, [event(2).id]);
|
|
});
|
|
}
|
|
|
|
test("completed pages are not cached for a later subscriber", async () => {
|
|
const f = fixture(undefined, { pageReplayConcurrency: 1 });
|
|
const running = f.replay();
|
|
await flush();
|
|
f.reads[0].resolve([event(1)]);
|
|
await flush();
|
|
assert.equal(f.reads.length, 2);
|
|
assert.deepEqual(f.reads[1].request, f.reads[0].request);
|
|
f.reads[1].resolve([event(2)]);
|
|
await running;
|
|
assert.deepEqual(f.entries[0].received, [event(1).id]);
|
|
assert.deepEqual(f.entries[1].received, [event(2).id]);
|
|
});
|
|
|
|
test("a rejected shared page is evicted and both subscribers can retry", async (t) => {
|
|
t.mock.method(console, "warn", () => {});
|
|
const f = fixture();
|
|
const running = f.replay();
|
|
await flush();
|
|
assert.equal(f.reads.length, 1);
|
|
f.reads[0].reject(new Error("temporary native read failure"));
|
|
await flush();
|
|
assert.equal(f.reads.length, 2, "retry must not reuse a rejected promise");
|
|
assert.deepEqual(f.reads[0].request, f.reads[1].request);
|
|
f.reads[1].resolve([event(1)]);
|
|
await running;
|
|
for (const sub of f.entries) {
|
|
assert.deepEqual(sub.received, [event(1).id]);
|
|
assert.equal(sub.pendingReplaySince, undefined);
|
|
}
|
|
});
|
|
|
|
test("exhausted shared reads preserve each floor without rejecting the replay", async (t) => {
|
|
t.mock.method(console, "warn", () => {});
|
|
const f = fixture();
|
|
const running = f.replay();
|
|
for (let attempt = 0; attempt < PAGE_REPLAY_MAX_ATTEMPTS; attempt++) {
|
|
await flush();
|
|
assert.equal(f.reads.length, attempt + 1);
|
|
f.reads[attempt].reject(new Error("unavailable"));
|
|
}
|
|
await running;
|
|
for (const sub of f.entries) {
|
|
assert.deepEqual(sub.received, []);
|
|
assert.equal(sub.pendingReplaySince, 2_000);
|
|
}
|
|
const retry = f.replay({ generation: 2 });
|
|
await flush();
|
|
assert.equal(f.reads.length, PAGE_REPLAY_MAX_ATTEMPTS + 1);
|
|
f.reads.at(-1).resolve([event(1)]);
|
|
await retry;
|
|
for (const sub of f.entries) assert.equal(sub.pendingReplaySince, undefined);
|
|
});
|
|
|
|
test("synchronous request failures retain the existing bounded retry contract", async (t) => {
|
|
t.mock.method(console, "warn", () => {});
|
|
const f = fixture();
|
|
let attempts = 0;
|
|
await f.replay({
|
|
requestRepair: () => {
|
|
attempts++;
|
|
throw new Error("synchronous boundary failure");
|
|
},
|
|
});
|
|
assert.equal(attempts, 2 * PAGE_REPLAY_MAX_ATTEMPTS);
|
|
for (const sub of f.entries) assert.equal(sub.pendingReplaySince, 2_000);
|
|
});
|
|
|
|
for (const action of ["dispose", "replace"]) {
|
|
test(`${action} during a shared read leaves the other subscriber intact`, async () => {
|
|
const f = fixture();
|
|
const running = f.replay();
|
|
await flush();
|
|
assert.equal(f.reads.length, 1);
|
|
const replacement = subscriber();
|
|
if (action === "dispose") f.subscriptions.delete("0");
|
|
else f.subscriptions.set("0", replacement);
|
|
f.reads[0].resolve([event(1)]);
|
|
await running;
|
|
assert.deepEqual(f.entries[0].received, []);
|
|
assert.deepEqual(replacement.received, []);
|
|
assert.deepEqual(f.entries[1].received, [event(1).id]);
|
|
assert.equal(f.entries[1].pendingReplaySince, undefined);
|
|
});
|
|
}
|
|
|
|
test("superseding reconnect does not share stale reads or release its retry floor", async () => {
|
|
let generation = 1;
|
|
const f = fixture();
|
|
const stale = f.replay({ isActive: () => generation === 1 });
|
|
await flush();
|
|
assert.equal(f.reads.length, 1);
|
|
generation = 2;
|
|
const current = f.replay({ generation, isActive: () => generation === 2 });
|
|
await flush();
|
|
assert.equal(f.reads.length, 2, "new connection needs a new native read");
|
|
f.reads[0].resolve([event(1)]);
|
|
await stale;
|
|
for (const sub of f.entries) {
|
|
assert.deepEqual(sub.received, []);
|
|
assert.equal(sub.pendingReplaySince, 2_000);
|
|
assert.equal(sub.reconnectReplay.generation, 2);
|
|
assert.equal(sub.reconnectReplay.repairDone, false);
|
|
}
|
|
f.reads[1].resolve([event(2)]);
|
|
await current;
|
|
for (const sub of f.entries) {
|
|
assert.deepEqual(sub.received, [event(2).id]);
|
|
assert.equal(sub.pendingReplaySince, undefined);
|
|
}
|
|
});
|
|
|
|
test("concurrent replay owners never share pages across sessions", async () => {
|
|
const left = fixture();
|
|
const right = fixture();
|
|
const leftRun = left.replay();
|
|
const rightRun = right.replay();
|
|
await flush();
|
|
assert.equal(left.reads.length, 1);
|
|
assert.equal(right.reads.length, 1);
|
|
left.reads[0].resolve([event(1)]);
|
|
right.reads[0].resolve([event(2)]);
|
|
await Promise.all([leftRun, rightRun]);
|
|
for (const sub of left.entries) assert.deepEqual(sub.received, [event(1).id]);
|
|
for (const sub of right.entries)
|
|
assert.deepEqual(sub.received, [event(2).id]);
|
|
});
|
|
|
|
for (const boundary of ["timestamp", "event id"]) {
|
|
test(`in-flight pages with a different ${boundary} cursor cannot share`, async () => {
|
|
// The third worker starts after the first reader has paged: its page one
|
|
// may see a newer snapshot, so equal floors do not imply equal cursors.
|
|
const f = fixture([subscriber(), subscriber("blocker"), subscriber()], {
|
|
pageReplayConcurrency: 2,
|
|
});
|
|
const running = f.replay();
|
|
await flush();
|
|
const page = Array.from({ length: 500 }, (_, i) => event(i + 1));
|
|
f.reads[0].resolve(page);
|
|
await flush();
|
|
assert.equal(f.reads.length, 3);
|
|
f.reads[1].resolve([]);
|
|
await flush();
|
|
assert.equal(
|
|
f.reads.length,
|
|
4,
|
|
"initial and cursor pages must be separate",
|
|
);
|
|
const alternate = [...page];
|
|
alternate[499] =
|
|
boundary === "timestamp"
|
|
? { ...page[499], created_at: 2_999 }
|
|
: event(501);
|
|
f.reads[3].resolve(alternate);
|
|
await flush();
|
|
assert.equal(
|
|
f.reads.length,
|
|
5,
|
|
"different composite cursors must not share",
|
|
);
|
|
assert.equal(f.reads[2].request.since, f.reads[4].request.since);
|
|
if (boundary === "timestamp") {
|
|
assert.equal(f.reads[2].request.beforeId, f.reads[4].request.beforeId);
|
|
assert.notEqual(f.reads[2].request.until, f.reads[4].request.until);
|
|
} else {
|
|
assert.equal(f.reads[2].request.until, f.reads[4].request.until);
|
|
assert.notEqual(f.reads[2].request.beforeId, f.reads[4].request.beforeId);
|
|
}
|
|
f.reads[2].resolve([event(600)]);
|
|
f.reads[4].resolve([event(700)]);
|
|
await running;
|
|
assert.equal(f.entries[0].received.at(-1), event(600).id);
|
|
assert.equal(f.entries[2].received.at(-1), event(700).id);
|
|
});
|
|
}
|
|
|
|
test("one subscriber callback failure does not poison another's shared page", async (t) => {
|
|
t.mock.method(console, "warn", () => {});
|
|
const broken = subscriber();
|
|
broken.onEvent = () => {
|
|
throw new Error("consumer failure");
|
|
};
|
|
const f = fixture([broken, subscriber()]);
|
|
const running = f.replay();
|
|
await flush();
|
|
f.reads[0].resolve([event(1)]);
|
|
await flush();
|
|
assert.equal(f.reads.length, 2, "failed consumer owns its own retry");
|
|
assert.deepEqual(f.entries[1].received, [event(1).id]);
|
|
assert.equal(f.entries[1].pendingReplaySince, undefined);
|
|
f.reads[1].resolve([event(1)]);
|
|
await running;
|
|
assert.deepEqual(f.entries[1].received, [event(1).id]);
|
|
});
|