clone

nostr relays on demand at <name>.bind.ws: claim one with your key, pay in zaps past the free allowance

dependabot/github_actions/actions-9b61906d8b
bindws / test / object / sync.test.ts
sync.test.ts 4.4K
1// NIP-77 across a wake: the filter of an open sync rides on the socket's2// attachment, so a session whose Negentropy object left with the object's3// memory carries on from the store instead of ending with closed:.4import { env, runInDurableObject } from "cloudflare:test";5import { describe, it, expect } from "vitest";6import { finalizeEvent, generateSecretKey, type Event } from "nostr-tools/pure";7import { sha256 } from "@noble/hashes/sha2.js";8import type { Relay } from "../../src/relay.ts";9import { Negentropy, bytesToHex, hexToBytes, type SyncItem } from "../../src/negentropy.ts";10import { now, rpc } from "../helpers/relay.ts";11import { WS } from "../helpers/ws.ts";1213const note = (sk: Uint8Array, content: string, created_at: number) => finalizeEvent({ kind: 1, content, tags: [], created_at }, sk);14const item = (e: Event): SyncItem => ({ timestamp: e.created_at, id: hexToBytes(e.id) });1516// wake empties the relay's in-memory sync sessions, which is what a17// hibernation does to them; the sockets and their attachments stay.18const wake = (name: string) => runInDurableObject(env.RELAY.getByName(name), async (r: Relay) => (r as unknown as { syncs: Map<unknown, unknown> }).syncs.clear());19// persisted reads the sync ids on the first socket's attachment.20const persisted = (name: string) => runInDurableObject(env.RELAY.getByName(name), async (r: Relay) => Object.keys(((r as unknown as { ctx: DurableObjectState }).ctx.getWebSockets()[0].deserializeAttachment() as { syncs?: Record<string, unknown> }).syncs ?? {}));2122// seed claims a relay and stores three notes; the client holds two of them23// and one the relay never saw.24async function seed(name: string) {25 const host = name + ".bind.ws";26 const owner = generateSecretKey();27 const author = generateSecretKey();28 await rpc(host, owner, "claim");29 const c = await WS.connect(host);30 const t = now() - 100;31 const notes = [note(author, "one", t), note(author, "two", t + 1), note(author, "three", t + 2)];32 for (const n of notes) expect((await c.ok(n)).ok).toBe(true);33 const stray = note(author, "stray", t + 3);34 const mine = [notes[0], notes[1], stray];35 return { host, owner, c, notes, stray, mine };36}3738describe("NIP-77 across a wake", () => {39 it("carries a sync on from the store after the session left memory", async () => {40 const { c, notes, stray, mine } = await seed("syncwake");41 const neg = new Negentropy(mine.map(item), sha256);42 c.send("NEG-OPEN", "s1", { kinds: [1] }, bytesToHex(neg.initiate()));43 const first = await c.expect("NEG-MSG");44 expect(await persisted("syncwake")).toEqual(["s1"]);4546 await wake("syncwake");47 // The same opening message again, now against a rebuilt session, draws48 // the same answer, and reconciling it names exactly what each side lacks.49 const again = new Negentropy(mine.map(item), sha256);50 c.send("NEG-MSG", "s1", bytesToHex(again.initiate()));51 const second = await c.expect("NEG-MSG");52 expect(second[2]).toBe(first[2]);53 const r = again.reconcile(hexToBytes(second[2]));54 expect(r.reply).toBeNull();55 expect(r.need).toEqual([notes[2].id]);56 expect(r.have).toEqual([stray.id]);5758 // NEG-CLOSE takes the filter off the socket; after that a wake ends it.59 c.send("NEG-CLOSE", "s1");60 await new Promise((res) => setTimeout(res, 50));61 expect(await persisted("syncwake")).toEqual([]);62 await wake("syncwake");63 c.send("NEG-MSG", "s1", bytesToHex(again.initiate()));64 const err = await c.expect("NEG-ERR");65 expect(err[2]).toMatch(/^closed: no such sync/);66 });6768 it("counts remembered syncs against the cap and drops them with the read rule", async () => {69 const { host, owner, c, mine } = await seed("synccap");70 await rpc(host, owner, "setpolicy", { maxSubs: 1 });71 const neg = new Negentropy(mine.map(item), sha256);72 c.send("NEG-OPEN", "s1", { kinds: [1] }, bytesToHex(neg.initiate()));73 await c.expect("NEG-MSG");74 await wake("synccap");75 c.send("NEG-OPEN", "s2", { kinds: [1] }, bytesToHex(neg.initiate()));76 expect((await c.expect("NEG-ERR"))[2]).toBe("blocked: too many open syncs");7778 // Reads tightened to members: the socket has proved nothing, so the79 // remembered sync goes with the subscriptions it never had.80 await rpc(host, owner, "setpolicy", { reads: "members" });81 expect(await persisted("synccap")).toEqual([]);82 c.send("NEG-MSG", "s1", bytesToHex(neg.initiate()));83 expect((await c.expect("NEG-ERR"))[2]).toMatch(/^closed: no such sync/);84 });85});