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,84 @@
1
+ import { CloseOptions } from "mock-socket";
2
+ import Nostr from "nostr-typedef";
3
+ import { WS } from "vitest-websocket-mock";
4
+ export interface MockServerSocket {
5
+ id: number;
6
+ send(message: unknown): void;
7
+ close(options?: CloseOptions): void;
8
+ }
9
+ export interface MockServerBehavior {
10
+ onOpen?(socket: MockServerSocket): void;
11
+ onMessage?(socket: MockServerSocket, message: string): void;
12
+ onClose?(socket: MockServerSocket): void;
13
+ onError?(socket: MockServerSocket, error: Error): void;
14
+ }
15
+ interface MockServer extends WS {
16
+ getSockets: (count: number, options?: {
17
+ timeout?: number;
18
+ }) => Promise<MockServerSocket[]>;
19
+ getSocket: (index: number) => Promise<MockServerSocket>;
20
+ getSocketsSync: () => MockServerSocket[];
21
+ }
22
+ export declare function createMockServer(url: string, behavior: MockServerBehavior): MockServer;
23
+ export interface MockRelay extends MockServer {
24
+ /**
25
+ * Pop the latest message that has not yet been consumed.
26
+ * If such a message does not yet exist, it waits for the message
27
+ * until it times out.
28
+ *
29
+ * The default timeout is 1000 miliseconds.
30
+ */
31
+ next: (timeout?: number) => Promise<Nostr.ToRelayMessage.Any>;
32
+ /**
33
+ * Pop the latest `count` messages that have not yet been consumed.
34
+ * If such a message does not yet exist, it waits for the message
35
+ * until it times out.
36
+ *
37
+ * The default timeout is 1000 miliseconds.
38
+ */
39
+ nexts: (count: number, timeout?: number) => Promise<Nostr.ToRelayMessage.Any[]>;
40
+ /**
41
+ * Send messages of any format from the given socket mock.
42
+ * If `socket` is omitted, it will be sent from all active socket mocks.
43
+ *
44
+ * If the message is a string, as-is string will be sent,
45
+ * otherwise JSON strigify will be attempted.
46
+ *
47
+ * Note that the messages sent by this method are outside
48
+ * the management of the mock relay.
49
+ * This means that if, for example, you use this method to send an OK message,
50
+ * the mock relay will not know about it.
51
+ * Therefore, subsequent calls to emitOK() may send a duplicate OK message.
52
+ * */
53
+ emit(message: unknown, socket?: MockServerSocket): string;
54
+ /**
55
+ * Send COUNT messages from all socket mocks
56
+ * holding active COUNT subscriptions (in other words,
57
+ * subscriptions that have not yet responded with COUNT)
58
+ * with the given subId.
59
+ */
60
+ emitCOUNT(subId: string, count?: number): Nostr.ToClientMessage.COUNT;
61
+ /**
62
+ * Send EOSE messages from all socket mocks
63
+ * holding active REQ subscriptions (in other words,
64
+ * subscriptions that have not yet been CLOSE'd)
65
+ * with the given subId.
66
+ */
67
+ emitEOSE(subId: string): Nostr.ToClientMessage.EOSE;
68
+ /**
69
+ * Send EVENT messages from all socket mocks
70
+ * holding active REQ subscriptions (in other words,
71
+ * subscriptions that have not yet been CLOSE'd)
72
+ * with the given subId.
73
+ */
74
+ emitEVENT(subId: string, event?: Partial<Nostr.Event>): Nostr.ToClientMessage.EVENT;
75
+ /**
76
+ * Send OK messages from all socket mocks
77
+ * holding active EVENT (in other words, an EVENT
78
+ * that has not yet returned an OK message)
79
+ * with the given eventId.
80
+ */
81
+ emitOK(eventId: string, succeeded: boolean, message?: string): Nostr.ToClientMessage.OK;
82
+ }
83
+ export declare function createMockRelay(url: string): MockRelay;
84
+ export {};
@@ -0,0 +1,181 @@
1
+ import { WS } from "vitest-websocket-mock";
2
+ import { faker } from "./faker.js";
3
+ import { withTimeout } from "./utils.js";
4
+ export function createMockServer(url, behavior) {
5
+ const server = new WS(url, { jsonProtocol: true });
6
+ const sockets = [];
7
+ let resolvers = [];
8
+ let nextId = 1;
9
+ server.on("connection", (_socket) => {
10
+ const socket = {
11
+ id: nextId++,
12
+ send(message) {
13
+ if (_socket.readyState !== WebSocket.OPEN) {
14
+ return;
15
+ }
16
+ _socket.send(typeof message === "string" ? message : `${JSON.stringify(message)}`);
17
+ },
18
+ close(options) {
19
+ _socket.close(options);
20
+ },
21
+ };
22
+ sockets.push(socket);
23
+ resolvers.filter(([count]) => count <= sockets.length).forEach(([, resolve]) => resolve([...sockets]));
24
+ resolvers = resolvers.filter(([count]) => count > sockets.length);
25
+ behavior.onOpen?.(socket);
26
+ _socket.on("message", (message) => {
27
+ if (typeof message !== "string") {
28
+ throw new Error("Unexpected type message");
29
+ }
30
+ behavior.onMessage?.(socket, message);
31
+ });
32
+ _socket.on("close", () => {
33
+ behavior.onClose?.(socket);
34
+ });
35
+ _socket.on("error", (error) => {
36
+ behavior.onError?.(socket, error);
37
+ });
38
+ });
39
+ const getSockets = async (count, options) => {
40
+ const promise = new Promise((resolve) => {
41
+ if (sockets.length >= count) {
42
+ resolve([...sockets]);
43
+ }
44
+ else {
45
+ resolvers.push([count, resolve]);
46
+ }
47
+ });
48
+ return withTimeout(promise, `Mock relay was waiting for ${count} connections to be established, but timed out.`, options?.timeout);
49
+ };
50
+ const getSocket = async (index) => {
51
+ const sockets = await getSockets(index + 1);
52
+ return sockets[index];
53
+ };
54
+ return Object.assign(server, {
55
+ getSockets,
56
+ getSocket,
57
+ getSocketsSync: () => [...sockets],
58
+ });
59
+ }
60
+ function relayBehavior() {
61
+ const reqs = new Map();
62
+ const counts = new Map();
63
+ const events = new Map();
64
+ const sockets = new Set();
65
+ return {
66
+ onOpen(socket) {
67
+ sockets.add(socket);
68
+ },
69
+ onMessage(socket, rawMessage) {
70
+ const message = JSON.parse(rawMessage);
71
+ switch (message[0]) {
72
+ case "CLOSE": {
73
+ const subId = message[1];
74
+ reqs.get(socket)?.delete(subId);
75
+ break;
76
+ }
77
+ case "COUNT": {
78
+ if (!counts.has(socket)) {
79
+ counts.set(socket, new Set());
80
+ }
81
+ const subId = message[1];
82
+ counts.get(socket)?.add(subId);
83
+ break;
84
+ }
85
+ case "EVENT": {
86
+ if (!events.has(socket)) {
87
+ events.set(socket, new Set());
88
+ }
89
+ const eventId = message[1].id;
90
+ events.get(socket)?.add(eventId);
91
+ break;
92
+ }
93
+ case "REQ": {
94
+ if (!reqs.has(socket)) {
95
+ reqs.set(socket, new Set());
96
+ }
97
+ const subId = message[1];
98
+ reqs.get(socket)?.add(subId);
99
+ break;
100
+ }
101
+ }
102
+ },
103
+ onClose(socket) {
104
+ reqs.delete(socket);
105
+ counts.delete(socket);
106
+ events.delete(socket);
107
+ sockets.delete(socket);
108
+ },
109
+ emit(message, socket) {
110
+ const toBeSent = typeof message === "string" ? message : `${JSON.stringify(message)}`;
111
+ if (socket) {
112
+ socket.send(toBeSent);
113
+ }
114
+ else {
115
+ for (const socket of sockets) {
116
+ socket.send(toBeSent);
117
+ }
118
+ }
119
+ return toBeSent;
120
+ },
121
+ emitCOUNT(subId, count) {
122
+ const toBeSent = faker.toClientMessage.COUNT(subId, count);
123
+ for (const [socket, subIds] of counts.entries()) {
124
+ if (subIds.has(subId)) {
125
+ socket.send(toBeSent);
126
+ }
127
+ subIds.delete(subId);
128
+ }
129
+ return toBeSent;
130
+ },
131
+ emitEOSE(subId) {
132
+ const toBeSent = faker.toClientMessage.EOSE(subId);
133
+ for (const [socket, subIds] of reqs.entries()) {
134
+ if (subIds.has(subId)) {
135
+ socket.send(toBeSent);
136
+ }
137
+ }
138
+ return toBeSent;
139
+ },
140
+ emitEVENT(subId, event) {
141
+ const toBeSent = faker.toClientMessage.EVENT(subId, event);
142
+ for (const [socket, subIds] of reqs.entries()) {
143
+ if (subIds.has(subId)) {
144
+ socket.send(toBeSent);
145
+ }
146
+ }
147
+ return toBeSent;
148
+ },
149
+ emitOK(eventId, succeeded, message) {
150
+ const toBeSent = faker.toClientMessage.OK(eventId, succeeded, message);
151
+ for (const [socket, eventIds] of events.entries()) {
152
+ if (eventIds.has(eventId)) {
153
+ socket.send(toBeSent);
154
+ }
155
+ eventIds.delete(eventId);
156
+ }
157
+ return toBeSent;
158
+ },
159
+ };
160
+ }
161
+ export function createMockRelay(url) {
162
+ const behavior = relayBehavior();
163
+ const server = createMockServer(url, behavior);
164
+ const { emit, emitCOUNT, emitEOSE, emitEVENT, emitOK } = behavior;
165
+ const timeoutMessage = "Mock relay was waiting for the next message from the client, but timed out.";
166
+ return Object.assign(server, {
167
+ next: (timeout) => withTimeout(server.nextMessage, timeoutMessage, timeout),
168
+ nexts: (count, timeout) => withTimeout(async () => {
169
+ const messages = [];
170
+ for (let i = 0; i < count; i++) {
171
+ messages.push((await server.nextMessage));
172
+ }
173
+ return messages;
174
+ }, timeoutMessage, timeout),
175
+ emit,
176
+ emitCOUNT,
177
+ emitEOSE,
178
+ emitEVENT,
179
+ emitOK,
180
+ });
181
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,96 @@
1
+ import { expect, beforeEach, afterEach, describe, it } from "vitest";
2
+ import { subscribeSpyTo } from "@hirez_io/observer-spy";
3
+ import { WS } from "vitest-websocket-mock";
4
+ import { RelayPool } from "../pool.js";
5
+ let pool;
6
+ let mockServer1;
7
+ let mockServer2;
8
+ let mockEvent;
9
+ beforeEach(async () => {
10
+ // Create mock WebSocket servers
11
+ mockServer1 = new WS("wss://relay1.example.com");
12
+ mockServer2 = new WS("wss://relay2.example.com");
13
+ pool = new RelayPool();
14
+ mockEvent = {
15
+ kind: 1,
16
+ id: "test-id",
17
+ pubkey: "test-pubkey",
18
+ created_at: 1743712795,
19
+ tags: [],
20
+ content: "test content",
21
+ sig: "test-sig",
22
+ };
23
+ });
24
+ afterEach(async () => {
25
+ mockServer1.close();
26
+ mockServer2.close();
27
+ // Clean up WebSocket mocks
28
+ await WS.clean();
29
+ });
30
+ describe("relay", () => {
31
+ it("should create a new relay", () => {
32
+ const url = "wss://relay1.example.com/";
33
+ const relay = pool.relay(url);
34
+ expect(relay).toBeDefined();
35
+ expect(pool.relays.get(url)).toBe(relay);
36
+ });
37
+ it("should return existing relay connection if already exists", () => {
38
+ const url = "wss://relay1.example.com";
39
+ const relay1 = pool.relay(url);
40
+ const relay2 = pool.relay(url);
41
+ expect(relay1).toBe(relay2);
42
+ expect(pool.relays.size).toBe(1);
43
+ });
44
+ it("should normalize relay urls", () => {
45
+ expect(pool.relay("wss://relay.example.com")).toBe(pool.relay("wss://relay.example.com/"));
46
+ expect(pool.relay("wss://relay.example.com:443")).toBe(pool.relay("wss://relay.example.com/"));
47
+ expect(pool.relay("ws://relay.example.com:80")).toBe(pool.relay("ws://relay.example.com/"));
48
+ });
49
+ });
50
+ describe("group", () => {
51
+ it("should create a relay group", () => {
52
+ const urls = ["wss://relay1.example.com/", "wss://relay2.example.com/"];
53
+ const group = pool.group(urls);
54
+ expect(group).toBeDefined();
55
+ expect(pool.groups.get(urls.sort().join(","))).toBe(group);
56
+ });
57
+ });
58
+ describe("req", () => {
59
+ it("should send subscription to multiple relays", async () => {
60
+ const urls = ["wss://relay1.example.com", "wss://relay2.example.com"];
61
+ const filters = { kinds: [1] };
62
+ const spy = subscribeSpyTo(pool.req(urls, filters));
63
+ // Verify REQ was sent to both relays
64
+ const req1 = await mockServer1.nextMessage;
65
+ const req2 = await mockServer2.nextMessage;
66
+ // Both messages should be REQ messages with the same filter
67
+ expect(JSON.parse(req1)[0]).toBe("REQ");
68
+ expect(JSON.parse(req2)[0]).toBe("REQ");
69
+ expect(JSON.parse(req1)[2]).toEqual(filters);
70
+ expect(JSON.parse(req2)[2]).toEqual(filters);
71
+ // Send EVENT from first relay
72
+ mockServer1.send(JSON.stringify(["EVENT", JSON.parse(req1)[1], mockEvent]));
73
+ // Send EOSE from both relays
74
+ mockServer1.send(JSON.stringify(["EOSE", JSON.parse(req1)[1]]));
75
+ mockServer2.send(JSON.stringify(["EOSE", JSON.parse(req2)[1]]));
76
+ expect(spy.getValues()).toContainEqual(expect.objectContaining(mockEvent));
77
+ });
78
+ });
79
+ describe("event", () => {
80
+ it("should publish to multiple relays", async () => {
81
+ const urls = ["wss://relay1.example.com/", "wss://relay2.example.com/"];
82
+ const spy = subscribeSpyTo(pool.event(urls, mockEvent));
83
+ // Verify EVENT was sent to both relays
84
+ const event1 = await mockServer1.nextMessage;
85
+ const event2 = await mockServer2.nextMessage;
86
+ expect(JSON.parse(event1)).toEqual(["EVENT", mockEvent]);
87
+ expect(JSON.parse(event2)).toEqual(["EVENT", mockEvent]);
88
+ // Send OK responses from both relays
89
+ mockServer1.send(JSON.stringify(["OK", mockEvent.id, true, ""]));
90
+ mockServer2.send(JSON.stringify(["OK", mockEvent.id, true, ""]));
91
+ expect(spy.getValues()).toEqual([
92
+ { ok: true, from: "wss://relay1.example.com/", message: "" },
93
+ { ok: true, from: "wss://relay2.example.com/", message: "" },
94
+ ]);
95
+ });
96
+ });
@@ -0,0 +1 @@
1
+ export {};