mirror of
https://github.com/block/buzz.git
synced 2026-11-13 21:01:53 +00:00
Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
193 lines
5.4 KiB
JavaScript
193 lines
5.4 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test, { beforeEach } from "node:test";
|
|
|
|
import {
|
|
chunkChannelContexts,
|
|
publishCommunityReadState,
|
|
} from "./communityMarkRead.ts";
|
|
|
|
const PUBKEY = "a".repeat(64);
|
|
const READ_AT = 1_700_000_000;
|
|
|
|
function makeLocalStorage() {
|
|
const store = new Map();
|
|
return {
|
|
get length() {
|
|
return store.size;
|
|
},
|
|
key: (i) => [...store.keys()][i] ?? null,
|
|
getItem: (key) => store.get(key) ?? null,
|
|
setItem: (key, value) => store.set(key, String(value)),
|
|
removeItem: (key) => store.delete(key),
|
|
};
|
|
}
|
|
|
|
beforeEach(() => {
|
|
globalThis.localStorage = makeLocalStorage();
|
|
globalThis.window = { localStorage: globalThis.localStorage };
|
|
});
|
|
|
|
test("chunkChannelContexts puts all channels in one blob when they fit", () => {
|
|
const chunks = chunkChannelContexts(["chan-1", "chan-2"], READ_AT, "client");
|
|
assert.equal(chunks.length, 1);
|
|
assert.deepEqual(chunks[0], { "chan-1": READ_AT, "chan-2": READ_AT });
|
|
});
|
|
|
|
test("chunkChannelContexts splits when the blob exceeds the byte budget", () => {
|
|
const ids = Array.from({ length: 40 }, (_, i) => `channel-${i}`);
|
|
const chunks = chunkChannelContexts(ids, READ_AT, "client", 512);
|
|
assert.ok(chunks.length > 1);
|
|
const merged = Object.assign({}, ...chunks);
|
|
assert.equal(Object.keys(merged).length, ids.length);
|
|
const encoder = new TextEncoder();
|
|
for (const contexts of chunks) {
|
|
const bytes = encoder.encode(
|
|
JSON.stringify({ v: 1, client_id: "client", contexts }),
|
|
).length;
|
|
assert.ok(bytes <= 512, `chunk exceeds budget: ${bytes}`);
|
|
}
|
|
});
|
|
|
|
test("chunkChannelContexts drops overflow beyond maxSlots instead of flooding", () => {
|
|
const ids = Array.from({ length: 100 }, (_, i) => `channel-${i}`);
|
|
const chunks = chunkChannelContexts(ids, READ_AT, "client", 128, 2);
|
|
assert.equal(chunks.length, 2);
|
|
});
|
|
|
|
function membersEvent(channelIds) {
|
|
return {
|
|
id: "e".repeat(64),
|
|
pubkey: "relay",
|
|
created_at: 1,
|
|
kind: 39002,
|
|
tags: channelIds.map((id) => ["d", id]),
|
|
content: "",
|
|
sig: "sig",
|
|
};
|
|
}
|
|
|
|
function metadataEvent(channelId, extraTags = []) {
|
|
return {
|
|
id: "f".repeat(64),
|
|
pubkey: "relay",
|
|
created_at: 1,
|
|
kind: 39000,
|
|
tags: [["d", channelId], ...extraTags],
|
|
content: "",
|
|
sig: "sig",
|
|
};
|
|
}
|
|
|
|
function makeClient({ channelIds, metadata }) {
|
|
const published = [];
|
|
return {
|
|
published,
|
|
async fetchEvents(filter) {
|
|
if (filter.kinds?.includes(39002)) return [membersEvent(channelIds)];
|
|
if (filter.kinds?.includes(39000)) return metadata;
|
|
return [];
|
|
},
|
|
async publishEvent(event) {
|
|
published.push(event);
|
|
},
|
|
};
|
|
}
|
|
|
|
test("publishCommunityReadState publishes one read-state event covering observed channels", async () => {
|
|
const client = makeClient({
|
|
channelIds: ["chan-1", "chan-2"],
|
|
metadata: [metadataEvent("chan-1"), metadataEvent("chan-2")],
|
|
});
|
|
const encrypted = [];
|
|
|
|
await publishCommunityReadState({
|
|
client,
|
|
pubkey: PUBKEY,
|
|
relayUrl: "wss://relay.example",
|
|
nowSeconds: READ_AT,
|
|
encrypt: async (plaintext) => {
|
|
encrypted.push(plaintext);
|
|
return `cipher:${encrypted.length}`;
|
|
},
|
|
sign: async (input) => ({
|
|
id: `signed-${encrypted.length}`,
|
|
pubkey: PUBKEY,
|
|
created_at: input.createdAt,
|
|
kind: input.kind,
|
|
tags: input.tags,
|
|
content: input.content,
|
|
sig: "sig",
|
|
}),
|
|
});
|
|
|
|
assert.equal(client.published.length, 1);
|
|
const event = client.published[0];
|
|
assert.equal(event.kind, 30078);
|
|
assert.equal(event.created_at, READ_AT);
|
|
assert.ok(
|
|
event.tags.some(
|
|
(tag) => tag[0] === "d" && tag[1].startsWith("read-state:"),
|
|
),
|
|
);
|
|
assert.ok(
|
|
event.tags.some((tag) => tag[0] === "t" && tag[1] === "read-state"),
|
|
);
|
|
|
|
const blob = JSON.parse(encrypted[0]);
|
|
assert.equal(blob.v, 1);
|
|
assert.deepEqual(blob.contexts, { "chan-1": READ_AT, "chan-2": READ_AT });
|
|
});
|
|
|
|
test("publishCommunityReadState skips archived channels and publishes nothing when none remain", async () => {
|
|
const client = makeClient({
|
|
channelIds: ["chan-1"],
|
|
metadata: [metadataEvent("chan-1", [["archived", "true"]])],
|
|
});
|
|
|
|
await publishCommunityReadState({
|
|
client,
|
|
pubkey: PUBKEY,
|
|
relayUrl: "wss://relay.example",
|
|
nowSeconds: READ_AT,
|
|
encrypt: async (plaintext) => plaintext,
|
|
sign: async () => {
|
|
throw new Error("must not sign when there is nothing to publish");
|
|
},
|
|
});
|
|
|
|
assert.equal(client.published.length, 0);
|
|
});
|
|
|
|
test("publishCommunityReadState reuses stable slot ids so blobs are replaceable", async () => {
|
|
const makeArgs = (client) => ({
|
|
client,
|
|
pubkey: PUBKEY,
|
|
relayUrl: "wss://relay.example",
|
|
nowSeconds: READ_AT,
|
|
encrypt: async (plaintext) => plaintext,
|
|
sign: async (input) => ({
|
|
id: Math.random().toString(),
|
|
pubkey: PUBKEY,
|
|
created_at: input.createdAt,
|
|
kind: input.kind,
|
|
tags: input.tags,
|
|
content: input.content,
|
|
sig: "sig",
|
|
}),
|
|
});
|
|
|
|
const clientA = makeClient({
|
|
channelIds: ["chan-1"],
|
|
metadata: [metadataEvent("chan-1")],
|
|
});
|
|
await publishCommunityReadState(makeArgs(clientA));
|
|
const clientB = makeClient({
|
|
channelIds: ["chan-1"],
|
|
metadata: [metadataEvent("chan-1")],
|
|
});
|
|
await publishCommunityReadState(makeArgs(clientB));
|
|
|
|
const dTag = (event) => event.tags.find((tag) => tag[0] === "d")[1];
|
|
assert.equal(dTag(clientA.published[0]), dTag(clientB.published[0]));
|
|
});
|