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.
- package/dist/__tests__/auth.test.d.ts +1 -0
- package/dist/__tests__/auth.test.js +111 -0
- package/dist/__tests__/exports.test.d.ts +1 -0
- package/dist/__tests__/exports.test.js +19 -0
- package/dist/__tests__/fake-user.d.ts +10 -0
- package/dist/__tests__/fake-user.js +31 -0
- package/dist/__tests__/faker.d.ts +19 -0
- package/dist/__tests__/faker.js +56 -0
- package/dist/__tests__/group.test.d.ts +1 -0
- package/dist/__tests__/group.test.js +106 -0
- package/dist/__tests__/matchers.d.ts +60 -0
- package/dist/__tests__/matchers.js +177 -0
- package/dist/__tests__/mock.d.ts +84 -0
- package/dist/__tests__/mock.js +181 -0
- package/dist/__tests__/pool.test.d.ts +1 -0
- package/dist/__tests__/pool.test.js +96 -0
- package/dist/__tests__/relay.test.d.ts +1 -0
- package/dist/__tests__/relay.test.js +654 -0
- package/dist/__tests__/spy.d.ts +7 -0
- package/dist/__tests__/spy.js +65 -0
- package/dist/__tests__/utils.d.ts +3 -0
- package/dist/__tests__/utils.js +8 -0
- package/dist/operators/__tests__/exports.test.d.ts +1 -0
- package/dist/operators/__tests__/exports.test.js +15 -0
- package/dist/operators/__tests__/to-event-store.test.d.ts +1 -0
- package/dist/operators/__tests__/to-event-store.test.js +18 -0
- package/dist/relay.d.ts +16 -2
- package/dist/relay.js +80 -34
- package/dist/server/connection.d.ts +1 -0
- package/dist/server/connection.js +9 -0
- package/dist/types.d.ts +35 -40
- package/package.json +3 -3
|
@@ -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 {};
|