applesauce-relay 0.0.0-next-20250808173123 → 0.0.0-next-20250815164532

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,111 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { subscribeSpyTo } from "@hirez_io/observer-spy";
3
+ import { WS } from "vitest-websocket-mock";
4
+ import { Relay } from "../relay.js";
5
+ let server;
6
+ let relay;
7
+ beforeEach(async () => {
8
+ server = new WS("wss://test", { jsonProtocol: true });
9
+ relay = new Relay("wss://test");
10
+ // Create a persistent subscription to keep the connection open
11
+ // @ts-expect-error
12
+ subscribeSpyTo(relay.socket);
13
+ });
14
+ afterEach(async () => {
15
+ server.close();
16
+ // Wait for server to close to prevent memory leaks
17
+ await WS.clean();
18
+ });
19
+ const mockEvent = {
20
+ kind: 1,
21
+ id: "00007641c9c3e65a71843933a44a18060c7c267a4f9169efa3735ece45c8f621",
22
+ pubkey: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
23
+ created_at: 1743712795,
24
+ tags: [["nonce", "13835058055282167643", "16"]],
25
+ content: "This is just stupid: https://codestr.fiatjaf.com/",
26
+ sig: "5a57b5a12bba4b7cf0121077b1421cf4df402c5c221376c076204fc4f7519e28ce6508f26ddc132c406ccfe6e62cc6db857b96c788565cdca9674fe9a0710ac2",
27
+ };
28
+ describe("event", () => {
29
+ it("should wait for auth before sending EVENT if auth-required received", async () => {
30
+ // Create first event subscription
31
+ const spy1 = subscribeSpyTo(relay.event(mockEvent));
32
+ // Verify EVENT was sent
33
+ const firstEventMessage = await server.nextMessage;
34
+ expect(firstEventMessage).toEqual(["EVENT", mockEvent]);
35
+ // Send auth-required response
36
+ server.send(["OK", mockEvent.id, false, "auth-required: need to authenticate"]);
37
+ // Create second event subscription - this should not send EVENT yet
38
+ const spy2 = subscribeSpyTo(relay.event(mockEvent));
39
+ // Should not have received any messages
40
+ expect(server.messages.length).toBe(1);
41
+ // Send AUTH challenge
42
+ server.send(["AUTH", "challenge-string"]);
43
+ // Send auth event
44
+ const authEvent = { ...mockEvent, id: "auth-id" };
45
+ subscribeSpyTo(relay.auth(authEvent));
46
+ // Verify AUTH was sent
47
+ const authMessage = await server.nextMessage;
48
+ expect(authMessage).toEqual(["AUTH", authEvent]);
49
+ // Send successful auth response
50
+ server.send(["OK", authEvent.id, true, ""]);
51
+ // Now the second EVENT should be sent
52
+ const secondEventMessage = await server.nextMessage;
53
+ expect(secondEventMessage).toEqual(["EVENT", mockEvent]);
54
+ // Send OK response for second event
55
+ server.send(["OK", mockEvent.id, true, ""]);
56
+ expect(spy1.getLastValue()).toEqual({
57
+ ok: false,
58
+ message: "auth-required: need to authenticate",
59
+ from: "wss://test",
60
+ });
61
+ expect(spy2.getLastValue()).toEqual({ ok: true, message: "", from: "wss://test" });
62
+ });
63
+ });
64
+ describe("req", () => {
65
+ it("should wait for auth before sending REQ if auth-required received", async () => {
66
+ // Create first REQ subscription
67
+ const filters = [{ kinds: [1], limit: 10 }];
68
+ subscribeSpyTo(relay.req(filters, "sub1"), { expectErrors: true });
69
+ // Verify REQ was sent
70
+ const firstReqMessage = await server.nextMessage;
71
+ expect(firstReqMessage).toEqual(["REQ", "sub1", ...filters]);
72
+ // Send auth-required response
73
+ server.send(["CLOSED", "sub1", "auth-required: need to authenticate"]);
74
+ // Consume the client CLOSE message for sub1
75
+ await server.nextMessage;
76
+ // Create second REQ subscription - this should not send REQ yet
77
+ subscribeSpyTo(relay.req(filters, "sub2"), { expectErrors: true });
78
+ // Should not have received any messages
79
+ expect(server.messages).not.toContain(["REQ", "sub2", ...filters]);
80
+ // Send AUTH challenge
81
+ server.send(["AUTH", "challenge-string"]);
82
+ // Send auth event
83
+ const authEvent = { ...mockEvent, id: "auth-id" };
84
+ subscribeSpyTo(relay.auth(authEvent));
85
+ // Verify AUTH was sent
86
+ const authMessage = await server.nextMessage;
87
+ expect(authMessage).toEqual(["AUTH", authEvent]);
88
+ // Send successful auth response
89
+ server.send(["OK", authEvent.id, true, ""]);
90
+ // Now the second REQ should be sent
91
+ const secondReqMessage = await server.nextMessage;
92
+ expect(secondReqMessage).toEqual(["REQ", "sub2", ...filters]);
93
+ // Send some events for the second subscription
94
+ server.send(["EVENT", "sub2", mockEvent]);
95
+ server.send(["EOSE", "sub2"]);
96
+ });
97
+ });
98
+ describe("auth", () => {
99
+ it("should set authenticated state after successful AUTH challenge response", async () => {
100
+ // Send AUTH challenge
101
+ server.send(["AUTH", "challenge-string"]);
102
+ // Send auth event response
103
+ const authEvent = { ...mockEvent, id: "auth-id" };
104
+ subscribeSpyTo(relay.auth(authEvent));
105
+ // Verify AUTH was sent
106
+ await expect(server).toReceiveMessage(["AUTH", authEvent]);
107
+ // Send successful auth response
108
+ server.send(["OK", authEvent.id, true, ""]);
109
+ expect(relay.authenticated).toBe(true);
110
+ });
111
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import * as exports from "../index.js";
3
+ describe("exports", () => {
4
+ it("should export the expected functions", () => {
5
+ expect(Object.keys(exports).sort()).toMatchInlineSnapshot(`
6
+ [
7
+ "Relay",
8
+ "RelayGroup",
9
+ "RelayPool",
10
+ "ReqCloseError",
11
+ "completeOnEose",
12
+ "markFromRelay",
13
+ "onlyEvents",
14
+ "storeEvents",
15
+ "toEventStore",
16
+ ]
17
+ `);
18
+ });
19
+ });
@@ -0,0 +1,10 @@
1
+ import { SimpleSigner } from "applesauce-signers/signers/simple-signer";
2
+ import type { NostrEvent } from "nostr-tools";
3
+ export declare class FakeUser extends SimpleSigner {
4
+ pubkey: string;
5
+ event(data?: Partial<NostrEvent>): NostrEvent;
6
+ note(content?: string, extra?: Partial<NostrEvent>): import("nostr-tools").Event;
7
+ profile(profile?: any, extra?: Partial<NostrEvent>): import("nostr-tools").Event;
8
+ contacts(pubkeys?: string[]): import("nostr-tools").Event;
9
+ list(tags?: string[][], extra?: Partial<NostrEvent>): import("nostr-tools").Event;
10
+ }
@@ -0,0 +1,31 @@
1
+ import { unixNow } from "applesauce-core/helpers";
2
+ import { SimpleSigner } from "applesauce-signers/signers/simple-signer";
3
+ import { finalizeEvent, getPublicKey, kinds } from "nostr-tools";
4
+ export class FakeUser extends SimpleSigner {
5
+ pubkey = getPublicKey(this.key);
6
+ event(data) {
7
+ return finalizeEvent({
8
+ kind: data?.kind ?? kinds.ShortTextNote,
9
+ content: data?.content || "",
10
+ created_at: data?.created_at ?? unixNow(),
11
+ tags: data?.tags || [],
12
+ }, this.key);
13
+ }
14
+ note(content = "Hello World", extra) {
15
+ return this.event({ kind: kinds.ShortTextNote, content, ...extra });
16
+ }
17
+ profile(profile = {}, extra) {
18
+ return this.event({ kind: kinds.Metadata, content: JSON.stringify({ ...profile }), ...extra });
19
+ }
20
+ contacts(pubkeys = []) {
21
+ return this.event({ kind: kinds.Contacts, tags: pubkeys.map((p) => ["p", p]) });
22
+ }
23
+ list(tags = [], extra) {
24
+ return this.event({
25
+ kind: kinds.Bookmarksets,
26
+ content: "",
27
+ tags: [["d", String(Math.round(Math.random() * 10000))], ...tags],
28
+ ...extra,
29
+ });
30
+ }
31
+ }
@@ -0,0 +1,19 @@
1
+ import * as Nostr from "nostr-typedef";
2
+ export declare const faker: {
3
+ filter(filter?: Nostr.Filter): Nostr.Filter;
4
+ filters(): Nostr.Filter[];
5
+ event<const K extends number>(event?: Partial<Nostr.Event<K>>): Nostr.Event<K>;
6
+ AUTH(event?: Partial<Nostr.Event<Nostr.Kind.ClientAuthentication>>): Nostr.ToRelayMessage.AUTH;
7
+ CLOSE(subId: string): Nostr.ToRelayMessage.CLOSE;
8
+ COUNT(subId: string, filters?: Nostr.Filter[]): Nostr.ToRelayMessage.COUNT;
9
+ EVENT(event?: Partial<Nostr.Event>): Nostr.ToRelayMessage.EVENT;
10
+ REQ(subId: string, filters?: Nostr.Filter[]): Nostr.ToRelayMessage.REQ;
11
+ toClientMessage: {
12
+ AUTH(message?: string): Nostr.ToClientMessage.AUTH;
13
+ COUNT(subId: string, count?: number): Nostr.ToClientMessage.COUNT;
14
+ EOSE(subId: string): Nostr.ToClientMessage.EOSE;
15
+ EVENT(subId: string, event?: Partial<Nostr.Event>): Nostr.ToClientMessage.EVENT;
16
+ NOTICE(message?: string): Nostr.ToClientMessage.NOTICE;
17
+ OK(eventId: string, succeeded: boolean, message?: string): Nostr.ToClientMessage.OK;
18
+ };
19
+ };
@@ -0,0 +1,56 @@
1
+ import { kinds } from "nostr-tools";
2
+ export const faker = {
3
+ filter(filter) {
4
+ return filter ?? { kinds: [0] };
5
+ },
6
+ filters() {
7
+ return [faker.filter()];
8
+ },
9
+ event(event) {
10
+ return {
11
+ id: "*",
12
+ content: "*",
13
+ created_at: 0,
14
+ kind: 0,
15
+ pubkey: "*",
16
+ sig: "*",
17
+ tags: [],
18
+ ...event,
19
+ };
20
+ },
21
+ AUTH(event) {
22
+ return ["AUTH", { ...faker.event(event), kind: kinds.ClientAuth }];
23
+ },
24
+ CLOSE(subId) {
25
+ return ["CLOSE", subId];
26
+ },
27
+ COUNT(subId, filters) {
28
+ return ["COUNT", subId, ...(filters ?? faker.filters())];
29
+ },
30
+ EVENT(event) {
31
+ return ["EVENT", faker.event(event)];
32
+ },
33
+ REQ(subId, filters) {
34
+ return ["REQ", subId, ...(filters ?? faker.filters())];
35
+ },
36
+ toClientMessage: {
37
+ AUTH(message) {
38
+ return ["AUTH", message ?? "*"];
39
+ },
40
+ COUNT(subId, count) {
41
+ return ["COUNT", subId, { count: count ?? 0 }];
42
+ },
43
+ EOSE(subId) {
44
+ return ["EOSE", subId];
45
+ },
46
+ EVENT(subId, event) {
47
+ return ["EVENT", subId, faker.event(event)];
48
+ },
49
+ NOTICE(message) {
50
+ return ["NOTICE", message ?? "*"];
51
+ },
52
+ OK(eventId, succeeded, message) {
53
+ return ["OK", eventId, succeeded, message ?? "*"];
54
+ },
55
+ },
56
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,106 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import { subscribeSpyTo } from "@hirez_io/observer-spy";
3
+ import { WS } from "vitest-websocket-mock";
4
+ import { Relay } from "../relay.js";
5
+ import { RelayGroup } from "../group.js";
6
+ import { of } from "rxjs";
7
+ let mockRelay1;
8
+ let mockRelay2;
9
+ let relay1;
10
+ let relay2;
11
+ let group;
12
+ let mockEvent;
13
+ beforeEach(async () => {
14
+ // Create mock relays
15
+ mockRelay1 = new WS("wss://relay1.test", { jsonProtocol: true });
16
+ mockRelay2 = new WS("wss://relay2.test", { jsonProtocol: true });
17
+ // Mock empty information document
18
+ vi.spyOn(Relay, "fetchInformationDocument").mockImplementation(() => of(null));
19
+ // Create relays
20
+ relay1 = new Relay("wss://relay1.test");
21
+ relay2 = new Relay("wss://relay2.test");
22
+ // Create group
23
+ group = new RelayGroup([relay1, relay2]);
24
+ mockEvent = {
25
+ kind: 1,
26
+ id: "test-id",
27
+ pubkey: "test-pubkey",
28
+ created_at: 1234567890,
29
+ tags: [],
30
+ content: "test content",
31
+ sig: "test-sig",
32
+ };
33
+ });
34
+ afterEach(async () => {
35
+ mockRelay1.close();
36
+ mockRelay2.close();
37
+ await WS.clean();
38
+ });
39
+ describe("req", () => {
40
+ it("should make requests to multiple relays", async () => {
41
+ group.req([{ kinds: [1] }], "test-sub").subscribe();
42
+ await expect(mockRelay1).toReceiveMessage(["REQ", "test-sub", { kinds: [1] }]);
43
+ await expect(mockRelay2).toReceiveMessage(["REQ", "test-sub", { kinds: [1] }]);
44
+ });
45
+ it("should emit events from all relays", async () => {
46
+ const spy = subscribeSpyTo(group.req([{ kinds: [1] }], "test-sub"));
47
+ await expect(mockRelay1).toReceiveMessage(["REQ", "test-sub", { kinds: [1] }]);
48
+ await expect(mockRelay2).toReceiveMessage(["REQ", "test-sub", { kinds: [1] }]);
49
+ mockRelay1.send(["EVENT", "test-sub", { ...mockEvent, id: "1" }]);
50
+ mockRelay2.send(["EVENT", "test-sub", { ...mockEvent, id: "2" }]);
51
+ expect(spy.getValues()).toEqual([expect.objectContaining({ id: "1" }), expect.objectContaining({ id: "2" })]);
52
+ });
53
+ it("should only emit EOSE once all relays have emitted EOSE", async () => {
54
+ const spy = subscribeSpyTo(group.req([{ kinds: [1] }], "test-sub"));
55
+ mockRelay1.send(["EOSE", "test-sub"]);
56
+ expect(spy.getValues()).not.toContain("EOSE");
57
+ mockRelay2.send(["EOSE", "test-sub"]);
58
+ expect(spy.getValues()).toContain("EOSE");
59
+ });
60
+ it("should ignore relays that have an error", async () => {
61
+ const spy = subscribeSpyTo(group.req([{ kinds: [1] }], "test-sub"));
62
+ mockRelay1.error();
63
+ mockRelay2.send(["EVENT", "test-sub", mockEvent]);
64
+ mockRelay2.send(["EOSE", "test-sub"]);
65
+ expect(spy.getValues()).toEqual([expect.objectContaining(mockEvent), "EOSE"]);
66
+ });
67
+ it("should emit EOSE if all relays error", async () => {
68
+ const spy = subscribeSpyTo(group.req([{ kinds: [1] }], "test-sub"));
69
+ mockRelay1.error();
70
+ mockRelay2.error();
71
+ expect(spy.getValues()).toEqual(["EOSE"]);
72
+ });
73
+ });
74
+ describe("event", () => {
75
+ it("should send EVENT to all relays in the group", async () => {
76
+ group.event(mockEvent).subscribe();
77
+ await expect(mockRelay1).toReceiveMessage(["EVENT", mockEvent]);
78
+ await expect(mockRelay2).toReceiveMessage(["EVENT", mockEvent]);
79
+ });
80
+ it("should emit OK messages from all relays", async () => {
81
+ const spy = subscribeSpyTo(group.event(mockEvent));
82
+ mockRelay1.send(["OK", mockEvent.id, true, ""]);
83
+ mockRelay2.send(["OK", mockEvent.id, true, ""]);
84
+ expect(spy.getValues()).toEqual([
85
+ expect.objectContaining({ ok: true, from: "wss://relay1.test", message: "" }),
86
+ expect.objectContaining({ ok: true, from: "wss://relay2.test", message: "" }),
87
+ ]);
88
+ });
89
+ it("should complete when all relays have sent OK messages", async () => {
90
+ const spy = subscribeSpyTo(group.event(mockEvent));
91
+ mockRelay1.send(["OK", mockEvent.id, true, ""]);
92
+ expect(spy.receivedComplete()).toBe(false);
93
+ mockRelay2.send(["OK", mockEvent.id, true, ""]);
94
+ expect(spy.receivedComplete()).toBe(true);
95
+ });
96
+ it("should handle relay errors and still complete", async () => {
97
+ const spy = subscribeSpyTo(group.event(mockEvent));
98
+ mockRelay1.error();
99
+ mockRelay2.send(["OK", mockEvent.id, true, ""]);
100
+ expect(spy.getValues()).toEqual([
101
+ expect.objectContaining({ ok: false, from: "wss://relay1.test", message: "Unknown error" }),
102
+ expect.objectContaining({ ok: true, from: "wss://relay2.test", message: "" }),
103
+ ]);
104
+ expect(spy.receivedComplete()).toBe(true);
105
+ });
106
+ });
@@ -0,0 +1,60 @@
1
+ import Nostr from "nostr-typedef";
2
+ declare module "vitest" {
3
+ interface Assertion<T = any> extends CustomMatchers {
4
+ }
5
+ interface AsymmetricMatchersContaining extends CustomMatchers {
6
+ }
7
+ }
8
+ interface CustomMatchers {
9
+ beToRelayAUTH(): void;
10
+ beToRelayAUTH(event: Partial<Nostr.Event<Nostr.Kind.ClientAuthentication>>): void;
11
+ beToRelayCLOSE(): void;
12
+ beToRelayCLOSE(subId: string): void;
13
+ beToRelayCOUNT(): void;
14
+ beToRelayCOUNT(subId: string): void;
15
+ beToRelayCOUNT(expected: [subId: string, ...filters: Nostr.Filter[]]): void;
16
+ beToRelayEVENT(): void;
17
+ beToRelayEVENT(event: Partial<Nostr.Event>): void;
18
+ beToRelayREQ(): void;
19
+ beToRelayREQ(subId: string): void;
20
+ beToRelayREQ(expected: [subId: string, ...filters: Nostr.Filter[]]): void;
21
+ beToClientAUTH(): void;
22
+ beToClientAUTH(challengeMessage: string): void;
23
+ beToClientCOUNT(): void;
24
+ beToClientCOUNT(subId: string): void;
25
+ beToClientCOUNT(expected: [subId: string, count: number]): void;
26
+ beToClientEOSE(): void;
27
+ beToClientEOSE(subId: string): void;
28
+ beToClientEVENT(): void;
29
+ beToClientEVENT(subId: string): void;
30
+ beToClientEVENT(expected: [subId: string, event: Partial<Nostr.Event>]): void;
31
+ beToClientNOTICE(): void;
32
+ beToClientNOTICE(message: string): void;
33
+ beToClientOK(expected: [eventId: string, succeeded: boolean, message?: string]): void;
34
+ toReceiveAUTH(): Promise<void>;
35
+ toReceiveAUTH(event: Partial<Nostr.Event<Nostr.Kind.ClientAuthentication>>): Promise<void>;
36
+ toReceiveCLOSE(): Promise<void>;
37
+ toReceiveCLOSE(subId: string): Promise<void>;
38
+ toReceiveCOUNT(): Promise<void>;
39
+ toReceiveCOUNT(subId: string): Promise<void>;
40
+ toReceiveCOUNT(expected: [subId: string, ...filters: Nostr.Filter[]]): Promise<void>;
41
+ toReceiveEVENT(): Promise<void>;
42
+ toReceiveEVENT(event: Partial<Nostr.Event>): Promise<void>;
43
+ toReceiveREQ(): Promise<void>;
44
+ toReceiveREQ(subId: string): Promise<void>;
45
+ toReceiveREQ(expected: [subId: string, ...filters: Nostr.Filter[]]): Promise<void>;
46
+ toSeeAUTH(): Promise<void>;
47
+ toSeeAUTH(challengeMessage: string): Promise<void>;
48
+ toSeeCOUNT(): Promise<void>;
49
+ toSeeCOUNT(subId: string): Promise<void>;
50
+ toSeeCOUNT(expected: [subId: string, count: number]): Promise<void>;
51
+ toSeeEOSE(): Promise<void>;
52
+ toSeeEOSE(subId: string): Promise<void>;
53
+ toSeeEVENT(): Promise<void>;
54
+ toSeeEVENT(subId: string): Promise<void>;
55
+ toSeeEVENT(expected: [subId: string, event: Partial<Nostr.Event>]): Promise<void>;
56
+ toSeeNOTICE(): Promise<void>;
57
+ toSeeNOTICE(message: string): Promise<void>;
58
+ toSeeOK(expected: [eventId: string, succeeded: boolean, message?: string]): Promise<void>;
59
+ }
60
+ export {};
@@ -0,0 +1,177 @@
1
+ import { expect } from "vitest";
2
+ import { deriveToReceiveMessage } from "vitest-websocket-mock";
3
+ import { TimeoutError, withTimeout } from "./utils.js";
4
+ const beToRelayAUTH = function (actual, event) {
5
+ const pass = isNostrMessage("AUTH", actual) && (event === undefined || matchEvent(actual[1], event, this.equals.bind(this)));
6
+ return {
7
+ message: message("a to-relay-AUTH message", ["AUTH", event ?? "*"], this, actual, pass),
8
+ pass,
9
+ };
10
+ };
11
+ const beToRelayCLOSE = function (actual, subId) {
12
+ const pass = isNostrMessage("CLOSE", actual) && (subId === undefined || actual[1] === subId);
13
+ return {
14
+ message: message("a to-relay-CLOSE message", ["CLOSE", subId ?? "*"], this, actual, pass),
15
+ pass,
16
+ };
17
+ };
18
+ const beToRelayCOUNT = function (actual, pred) {
19
+ const subId = typeof pred === "string" ? pred : pred !== undefined ? pred[0] : null;
20
+ const filters = typeof pred === "object" && pred !== undefined ? pred.slice(1) : null;
21
+ const pass = isNostrMessage("COUNT", actual) &&
22
+ (subId === null || actual[1] === subId) &&
23
+ (filters === null || this.equals(actual.slice(2), filters));
24
+ return {
25
+ message: message("a to-relay-COUNT message", ["COUNT", subId ?? "*", ...(filters === null ? ["*"] : filters)], this, actual, pass),
26
+ pass,
27
+ };
28
+ };
29
+ const beToRelayEVENT = function (actual, event) {
30
+ const pass = isNostrMessage("EVENT", actual) && (event === undefined || matchEvent(actual[1], event, this.equals.bind(this)));
31
+ return {
32
+ message: message("a to-relay-EVENT message", ["EVENT", event ?? "*"], this, actual, pass),
33
+ pass,
34
+ };
35
+ };
36
+ const beToRelayREQ = function (actual, pred) {
37
+ const subId = typeof pred === "string" ? pred : pred !== undefined ? pred[0] : null;
38
+ const filters = typeof pred === "object" && pred !== undefined ? pred.slice(1) : null;
39
+ const pass = isNostrMessage("REQ", actual) &&
40
+ (subId === null || actual[1] === subId) &&
41
+ (filters === null || this.equals(actual.slice(2), filters));
42
+ return {
43
+ message: message("a to-relay-REQ message", ["REQ", subId ?? "*", ...(filters === null ? ["*"] : filters)], this, actual, pass),
44
+ pass,
45
+ };
46
+ };
47
+ const beToClientAUTH = function (actual, challengeMessage) {
48
+ const pass = isNostrMessage("AUTH", actual) && (challengeMessage === undefined || actual[1] === challengeMessage);
49
+ return {
50
+ message: message("a to-client-AUTH message", ["AUTH", challengeMessage ?? "*"], this, actual, pass),
51
+ pass,
52
+ };
53
+ };
54
+ const beToClientCOUNT = function (actual, pred) {
55
+ const subId = typeof pred === "string" ? pred : pred !== undefined ? pred[0] : null;
56
+ const count = typeof pred === "object" && pred !== undefined ? pred[1] : null;
57
+ const pass = isNostrMessage("COUNT", actual) &&
58
+ (subId === null || actual[1] === subId) &&
59
+ (count === null || this.equals(actual[2], { count }));
60
+ return {
61
+ message: message("a to-client-COUNT message", ["COUNT", subId ?? "*", count === null ? "*" : { count }], this, actual, pass),
62
+ pass,
63
+ };
64
+ };
65
+ const beToClientEOSE = function (actual, subId) {
66
+ const pass = isNostrMessage("EOSE", actual) && (subId === undefined || actual[1] === subId);
67
+ return {
68
+ message: message("a to-client-EOSE message", ["COUNT", subId ?? "*"], this, actual, pass),
69
+ pass,
70
+ };
71
+ };
72
+ const beToClientEVENT = function (actual, pred) {
73
+ const subId = typeof pred === "string" ? pred : pred !== undefined ? pred[0] : null;
74
+ const event = typeof pred === "object" && pred !== undefined ? pred[1] : null;
75
+ const pass = isNostrMessage("EVENT", actual) &&
76
+ (subId === null || actual[1] === subId) &&
77
+ (event === null || matchEvent(actual[2], event, this.equals.bind(this)));
78
+ return {
79
+ message: message("a to-client-EVENT message", ["EVENT", subId ?? "*", event ?? "*"], this, actual, pass),
80
+ pass,
81
+ };
82
+ };
83
+ const beToClientNOTICE = function (actual, noticeMessage) {
84
+ const pass = isNostrMessage("NOTICE", actual) && (noticeMessage === undefined || actual[1] === noticeMessage);
85
+ return {
86
+ message: message("a to-client-NOTICE message", ["EVENT", noticeMessage ?? "*"], this, actual, pass),
87
+ pass,
88
+ };
89
+ };
90
+ const beToClientOK = function (actual, expected) {
91
+ const pass = isNostrMessage("OK", actual) &&
92
+ (expected === undefined ||
93
+ (actual[1] === expected[0] &&
94
+ actual[2] === expected[1] &&
95
+ (expected[2] === undefined || actual[3] === expected[2])));
96
+ return {
97
+ message: message("a to-client-OK message", ["OK", ...(expected ?? ["*", "*", "*"])], this, actual, pass),
98
+ pass,
99
+ };
100
+ };
101
+ const toReceiveAUTH = deriveToReceiveMessage("toReceiveAUTH", beToRelayAUTH);
102
+ const toReceiveCLOSE = deriveToReceiveMessage("toReceiveCLOSE", beToRelayCLOSE);
103
+ const toReceiveCOUNT = deriveToReceiveMessage("toReceiveCOUNT", beToRelayCOUNT);
104
+ const toReceiveEVENT = deriveToReceiveMessage("toReceiveEVENT", beToRelayEVENT);
105
+ const toReceiveREQ = deriveToReceiveMessage("toReceiveREQ", beToRelayREQ);
106
+ const toSeeAUTH = clientMathcer("toSeeAUTH", beToClientAUTH);
107
+ const toSeeCOUNT = clientMathcer("toSeeCOUNT", beToClientCOUNT);
108
+ const toSeeEOSE = clientMathcer("toSeeEOSE", beToClientEOSE);
109
+ const toSeeEVENT = clientMathcer("toSeeEVENT", beToClientEVENT);
110
+ const toSeeNOTICE = clientMathcer("toSeeNOTICE", beToClientNOTICE);
111
+ const toSeeOK = clientMathcer("toSeeOK", beToClientOK);
112
+ expect.extend({
113
+ beToRelayAUTH,
114
+ beToRelayCLOSE,
115
+ beToRelayCOUNT,
116
+ beToRelayEVENT,
117
+ beToRelayREQ,
118
+ beToClientAUTH,
119
+ beToClientCOUNT,
120
+ beToClientEOSE,
121
+ beToClientEVENT,
122
+ beToClientNOTICE,
123
+ beToClientOK,
124
+ toReceiveAUTH,
125
+ toReceiveCLOSE,
126
+ toReceiveCOUNT,
127
+ toReceiveEVENT,
128
+ toReceiveREQ,
129
+ toSeeAUTH,
130
+ toSeeCOUNT,
131
+ toSeeEOSE,
132
+ toSeeEVENT,
133
+ toSeeNOTICE,
134
+ toSeeOK,
135
+ });
136
+ function clientMathcer(name, fn) {
137
+ return async function (spy, pred, options) {
138
+ if (spy.__createClientSpy) {
139
+ try {
140
+ return await withTimeout(async () => fn.call(this, await spy.next(), pred, options));
141
+ }
142
+ catch (err) {
143
+ if (err instanceof TimeoutError) {
144
+ return {
145
+ pass: this.isNot,
146
+ message: () => this.utils.matcherHint(`${this.isNot ? ".not" : ""}.${name}`, "ClientSpy", "expected") +
147
+ "\n\n" +
148
+ `Client spy was waiting for the next message from the relay, but timed out.`,
149
+ };
150
+ }
151
+ else {
152
+ throw err;
153
+ }
154
+ }
155
+ }
156
+ else {
157
+ return {
158
+ pass: this.isNot,
159
+ message: () => this.utils.matcherHint(`${this.isNot ? ".not" : ""}.${name}`, "ClientSpy", "expected") +
160
+ "\n\n" +
161
+ `Mathcer \`${name}\` must be used for ClientSpy, but now being used for:\n\n` +
162
+ `${this.utils.printReceived(spy)}\n`,
163
+ };
164
+ }
165
+ };
166
+ }
167
+ function isNostrMessage(type, message) {
168
+ return Array.isArray(message) && message[0] === type;
169
+ }
170
+ function matchEvent(received, expected, equal) {
171
+ const eventKeys = ["id", "sig", "kind", "tags", "pubkey", "content", "created_at"];
172
+ const isEvent = (x) => typeof x === "object" && x !== null && eventKeys.every((key) => key in x);
173
+ return (isEvent(received) && eventKeys.every((key) => expected[key] === undefined || equal(received[key], expected[key])));
174
+ }
175
+ function message(entityName, expected, state, actual, pass) {
176
+ return () => `It was expected ${pass ? "not " : ""}to be ${entityName}, like this:\n\n${state.utils.printExpected(expected)}\n\nbut got:\n\n${state.utils.printReceived(actual)}\n`;
177
+ }