applesauce-relay 0.0.0-next-20250916134023 → 0.0.0-next-20250919114711

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.
@@ -1,177 +0,0 @@
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
- }
@@ -1,84 +0,0 @@
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 {};
@@ -1,181 +0,0 @@
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
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,96 +0,0 @@
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
- });
@@ -1 +0,0 @@
1
- export {};