applesauce-relay 1.2.0 → 2.1.1

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,8 +1,8 @@
1
- import { ISyncEventStore } from "applesauce-core";
1
+ import { IEventStoreRead } from "applesauce-core";
2
2
  import { Filter } from "nostr-tools";
3
3
  import { MultiplexWebSocket } from "./types.js";
4
4
  import { NegentropyStorageVector } from "./lib/negentropy.js";
5
- export declare function buildStorageFromFilter(store: ISyncEventStore, filter: Filter): NegentropyStorageVector;
5
+ export declare function buildStorageFromFilter(store: IEventStoreRead, filter: Filter): NegentropyStorageVector;
6
6
  export declare function buildStorageVector(items: {
7
7
  id: string;
8
8
  created_at: number;
@@ -5,7 +5,7 @@ import { Negentropy, NegentropyStorageVector } from "./lib/negentropy.js";
5
5
  const log = logger.extend("negentropy");
6
6
  export function buildStorageFromFilter(store, filter) {
7
7
  const storage = new NegentropyStorageVector();
8
- for (const event of store.getAll(filter))
8
+ for (const event of store.getByFilters(filter))
9
9
  storage.insert(event.created_at, event.id);
10
10
  storage.seal();
11
11
  return storage;
@@ -1,7 +1,4 @@
1
1
  import { takeWhile } from "rxjs";
2
- export function completeOnEose(includeEose) {
3
- if (includeEose)
4
- return takeWhile((m) => m !== "EOSE", true);
5
- else
6
- return takeWhile((m) => m !== "EOSE", false);
2
+ export function completeOnEose(inclusive) {
3
+ return takeWhile((m) => m !== "EOSE", inclusive);
7
4
  }
@@ -1,5 +1,5 @@
1
1
  import { IEventStore } from "applesauce-core";
2
2
  import { MonoTypeOperatorFunction } from "rxjs";
3
3
  import { SubscriptionResponse } from "../types.js";
4
- /** Sends all events to the event store */
4
+ /** Sends all events to the event store but does not remove duplicates */
5
5
  export declare function storeEvents(eventStore: IEventStore): MonoTypeOperatorFunction<SubscriptionResponse>;
@@ -1,5 +1,5 @@
1
1
  import { tap } from "rxjs";
2
- /** Sends all events to the event store */
2
+ /** Sends all events to the event store but does not remove duplicates */
3
3
  export function storeEvents(eventStore) {
4
4
  return (source) => {
5
5
  return source.pipe(tap((event) => typeof event !== "string" && eventStore.add(event)));
@@ -2,5 +2,8 @@ import { OperatorFunction } from "rxjs";
2
2
  import { IEventStore } from "applesauce-core";
3
3
  import { NostrEvent } from "nostr-tools";
4
4
  import { SubscriptionResponse } from "../types.js";
5
- /** Adds all events to event store and returns a deduplicated timeline when EOSE is received */
5
+ /**
6
+ * Adds all events to event store and returns a deduplicated timeline when EOSE is received
7
+ * @deprecated use `mapEventsToStore` and `mapEventsToTimeline` instead
8
+ */
6
9
  export declare function toEventStore(eventStore: IEventStore): OperatorFunction<SubscriptionResponse, NostrEvent[]>;
@@ -1,19 +1,15 @@
1
- import { scan } from "rxjs";
2
- import { insertEventIntoDescendingList } from "nostr-tools/utils";
1
+ import { mapEventsToStore, mapEventsToTimeline } from "applesauce-core/observable";
3
2
  import { completeOnEose } from "./complete-on-eose.js";
4
- /** Adds all events to event store and returns a deduplicated timeline when EOSE is received */
3
+ /**
4
+ * Adds all events to event store and returns a deduplicated timeline when EOSE is received
5
+ * @deprecated use `mapEventsToStore` and `mapEventsToTimeline` instead
6
+ */
5
7
  export function toEventStore(eventStore) {
6
8
  return (source) => source.pipe(
7
9
  // Complete when there are not events
8
10
  completeOnEose(),
11
+ // Save events to store and remove duplicates
12
+ mapEventsToStore(eventStore, true),
9
13
  // Add the events to an array
10
- scan((events, event) => {
11
- // Get the current instance of this event
12
- let e = eventStore.add(event);
13
- // If its not in the timeline, add it
14
- if (events.includes(e))
15
- return events;
16
- else
17
- return insertEventIntoDescendingList(events, e);
18
- }, []));
14
+ mapEventsToTimeline());
19
15
  }
package/dist/relay.d.ts CHANGED
@@ -61,12 +61,12 @@ export declare class Relay implements IRelay {
61
61
  protected authRequiredForReq: Observable<boolean>;
62
62
  protected authRequiredForEvent: Observable<boolean>;
63
63
  protected resetState(): void;
64
- /** An internal observable that is responsible for watching all messages and updating state */
64
+ /** An internal observable that is responsible for watching all messages and updating state, subscribing to it will trigger a connection to the relay */
65
65
  protected watchTower: Observable<never>;
66
66
  constructor(url: string, opts?: RelayOptions);
67
67
  /** Set ready = false and start the reconnect timer */
68
68
  protected startReconnectTimer(error: Error | CloseEvent): void;
69
- /** Wait for ready and authenticated */
69
+ /** Wait for authentication state, make connection and then wait for authentication if required */
70
70
  protected waitForAuth<T extends unknown = unknown>(requireAuth: Observable<boolean>, observable: Observable<T>): Observable<T>;
71
71
  /** Wait for the relay to be ready to accept connections */
72
72
  protected waitForReady<T extends unknown = unknown>(observable: Observable<T>): Observable<T>;
package/dist/relay.js CHANGED
@@ -2,8 +2,9 @@ import { logger } from "applesauce-core";
2
2
  import { simpleTimeout } from "applesauce-core/observable";
3
3
  import { nanoid } from "nanoid";
4
4
  import { nip42 } from "nostr-tools";
5
- import { BehaviorSubject, catchError, combineLatest, defer, endWith, filter, finalize, from, ignoreElements, isObservable, map, merge, mergeMap, NEVER, of, retry, scan, share, shareReplay, Subject, switchMap, take, takeUntil, tap, throwError, timeout, timer, } from "rxjs";
5
+ import { BehaviorSubject, catchError, combineLatest, defer, endWith, filter, finalize, from, ignoreElements, isObservable, map, merge, mergeMap, mergeWith, NEVER, of, retry, scan, share, shareReplay, Subject, switchMap, take, takeUntil, tap, throwError, timeout, timer, } from "rxjs";
6
6
  import { webSocket } from "rxjs/webSocket";
7
+ import { ensureHttpURL } from "applesauce-core/helpers";
7
8
  import { completeOnEose } from "./operators/complete-on-eose.js";
8
9
  import { markFromRelay } from "./operators/mark-from-relay.js";
9
10
  /** An error that is thrown when a REQ is closed from the relay side */
@@ -85,7 +86,7 @@ export class Relay {
85
86
  if (this.receivedAuthRequiredForEvent.value)
86
87
  this.receivedAuthRequiredForEvent.next(false);
87
88
  }
88
- /** An internal observable that is responsible for watching all messages and updating state */
89
+ /** An internal observable that is responsible for watching all messages and updating state, subscribing to it will trigger a connection to the relay */
89
90
  watchTower;
90
91
  constructor(url, opts) {
91
92
  this.url = url;
@@ -190,11 +191,13 @@ export class Relay {
190
191
  .pipe(take(1))
191
192
  .subscribe(() => this.ready$.next(true));
192
193
  }
193
- /** Wait for ready and authenticated */
194
+ /** Wait for authentication state, make connection and then wait for authentication if required */
194
195
  waitForAuth(
195
196
  // NOTE: require BehaviorSubject so it always has a value
196
197
  requireAuth, observable) {
197
198
  return combineLatest([requireAuth, this.authenticated$]).pipe(
199
+ // Once the auth state is known, make a connection and watch for auth challenges
200
+ mergeWith(this.watchTower),
198
201
  // wait for auth not required or authenticated
199
202
  filter(([required, authenticated]) => !required || authenticated),
200
203
  // complete after the first value so this does not repeat
@@ -284,10 +287,8 @@ export class Relay {
284
287
  // format OK message
285
288
  map((m) => ({ ok: m[2], message: m[3], from: this.url })));
286
289
  });
287
- // Start the watch tower with the observable
288
- const withWatchTower = merge(this.watchTower, base);
289
- // Add complete operators
290
- const observable = withWatchTower.pipe(
290
+ // Start the watch tower and add complete operators
291
+ const observable = merge(this.watchTower, base).pipe(
291
292
  // complete on first value
292
293
  take(1),
293
294
  // listen for OK auth-required
@@ -351,7 +352,7 @@ export class Relay {
351
352
  }
352
353
  /** Static method to fetch the NIP-11 information document for a relay */
353
354
  static fetchInformationDocument(url) {
354
- return from(fetch(url, { headers: { Accept: "application/nostr+json" } }).then((res) => res.json())).pipe(
355
+ return from(fetch(ensureHttpURL(url), { headers: { Accept: "application/nostr+json" } }).then((res) => res.json())).pipe(
355
356
  // if the fetch fails, return null
356
357
  catchError(() => of(null)),
357
358
  // timeout after 10s
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-relay",
3
- "version": "1.2.0",
3
+ "version": "2.1.1",
4
4
  "description": "nostr relay communication framework built on rxjs",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -54,16 +54,17 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "@noble/hashes": "^1.7.1",
57
- "applesauce-core": "^1.2.0",
57
+ "applesauce-core": "^2.0.0",
58
58
  "nanoid": "^5.0.9",
59
- "nostr-tools": "^2.10.4",
59
+ "nostr-tools": "^2.13",
60
60
  "rxjs": "^7.8.1"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@hirez_io/observer-spy": "^2.2.0",
64
+ "applesauce-signers": "^2.0.0",
64
65
  "@vitest/expect": "^3.1.1",
65
66
  "typescript": "^5.7.3",
66
- "vitest": "^3.1.1",
67
+ "vitest": "^3.2.3",
67
68
  "vitest-websocket-mock": "^0.5.0"
68
69
  },
69
70
  "funding": {
@@ -1 +0,0 @@
1
- export {};
@@ -1,111 +0,0 @@
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
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,19 +0,0 @@
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
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,106 +0,0 @@
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
- });
@@ -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 {};
@@ -1,644 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
- import { subscribeSpyTo } from "@hirez_io/observer-spy";
3
- import { getSeenRelays } from "applesauce-core/helpers";
4
- import { WS } from "vitest-websocket-mock";
5
- import { Relay } from "../relay.js";
6
- import { filter } from "rxjs/operators";
7
- import { firstValueFrom, of, Subject, throwError, timer } from "rxjs";
8
- const defaultMockInfo = {
9
- name: "Test Relay",
10
- description: "Test Relay Description",
11
- pubkey: "testpubkey",
12
- contact: "test@example.com",
13
- supported_nips: [1, 2, 3],
14
- software: "test-software",
15
- version: "1.0.0",
16
- };
17
- let server;
18
- let relay;
19
- beforeEach(async () => {
20
- // Mock empty information document
21
- vi.spyOn(Relay, "fetchInformationDocument").mockImplementation(() => of(null));
22
- // Create mock relay
23
- server = new WS("wss://test", { jsonProtocol: true });
24
- // Create relay
25
- relay = new Relay("wss://test");
26
- relay.keepAlive = 0;
27
- });
28
- // Wait for server to close to prevent memory leaks
29
- afterEach(async () => {
30
- await WS.clean();
31
- vi.clearAllTimers();
32
- vi.useRealTimers();
33
- });
34
- const mockEvent = {
35
- kind: 1,
36
- id: "00007641c9c3e65a71843933a44a18060c7c267a4f9169efa3735ece45c8f621",
37
- pubkey: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
38
- created_at: 1743712795,
39
- tags: [["nonce", "13835058055282167643", "16"]],
40
- content: "This is just stupid: https://codestr.fiatjaf.com/",
41
- sig: "5a57b5a12bba4b7cf0121077b1421cf4df402c5c221376c076204fc4f7519e28ce6508f26ddc132c406ccfe6e62cc6db857b96c788565cdca9674fe9a0710ac2",
42
- };
43
- describe("req", () => {
44
- it("should trigger connection to relay", async () => {
45
- subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
46
- // Wait for connection
47
- await firstValueFrom(relay.connected$.pipe(filter(Boolean)));
48
- expect(relay.connected).toBe(true);
49
- });
50
- it("should send expected messages to relay", async () => {
51
- subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
52
- await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
53
- });
54
- it("should not close the REQ when EOSE is received", async () => {
55
- // Create subscription that completes after first EOSE
56
- const sub = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
57
- // Verify REQ was sent
58
- await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
59
- // Send EOSE to complete subscription
60
- server.send(["EVENT", "sub1", mockEvent]);
61
- server.send(["EOSE", "sub1"]);
62
- // Verify the subscription did not complete
63
- expect(sub.receivedComplete()).toBe(false);
64
- expect(sub.getValues()).toEqual([expect.objectContaining(mockEvent), "EOSE"]);
65
- });
66
- it("should send CLOSE when unsubscribed", async () => {
67
- // Create subscription that completes after first EOSE
68
- const sub = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
69
- // Verify REQ was sent
70
- await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
71
- // Complete the subscription
72
- sub.unsubscribe();
73
- // Verify CLOSE was sent
74
- await expect(server).toReceiveMessage(["CLOSE", "sub1"]);
75
- });
76
- it("should emit nostr event and EOSE", async () => {
77
- const spy = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
78
- await server.connected;
79
- // Send EVENT message
80
- server.send(["EVENT", "sub1", mockEvent]);
81
- // Send EOSE message
82
- server.send(["EOSE", "sub1"]);
83
- expect(spy.getValues()).toEqual([expect.objectContaining(mockEvent), "EOSE"]);
84
- });
85
- it("should ignore EVENT and EOSE messages that do not match subscription id", async () => {
86
- const spy = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
87
- await server.connected;
88
- // Send EVENT message with wrong subscription id
89
- server.send(["EVENT", "wrong_sub", mockEvent]);
90
- // Send EOSE message with wrong subscription id
91
- server.send(["EOSE", "wrong_sub"]);
92
- // Send EVENT message with correct subscription id
93
- server.send(["EVENT", "sub1", mockEvent]);
94
- // Send EOSE message with correct subscription id
95
- server.send(["EOSE", "sub1"]);
96
- expect(spy.getValues()).toEqual([expect.objectContaining(mockEvent), "EOSE"]);
97
- });
98
- it("should mark events with their source relay", async () => {
99
- const spy = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
100
- await server.connected;
101
- // Send EVENT message
102
- server.send(["EVENT", "sub1", mockEvent]);
103
- // Get the received event
104
- const receivedEvent = spy.getValues()[0];
105
- // Verify the event was marked as seen from this relay
106
- expect(getSeenRelays(receivedEvent)).toContain("wss://test");
107
- });
108
- it("should error subscription when CLOSED message is received", async () => {
109
- const spy = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"), { expectErrors: true });
110
- await server.connected;
111
- // Send CLOSED message for the subscription
112
- server.send(["CLOSED", "sub1", "reason"]);
113
- // Verify the subscription completed
114
- expect(spy.receivedError()).toBe(true);
115
- });
116
- it("should not send multiple REQ messages for multiple subscriptions", async () => {
117
- const sub = relay.req([{ kinds: [1] }], "sub1");
118
- sub.subscribe();
119
- sub.subscribe();
120
- // Wait for all messages to be sent
121
- await new Promise((resolve) => setTimeout(resolve, 10));
122
- expect(server.messages).toEqual([["REQ", "sub1", { kinds: [1] }]]);
123
- });
124
- it("should wait for authentication if relay responds with auth-required", async () => {
125
- // First subscription to trigger auth-required
126
- const firstSub = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"), { expectErrors: true });
127
- await server.nextMessage;
128
- // Send CLOSED message with auth-required reason
129
- server.send(["CLOSED", "sub1", "auth-required: need to authenticate"]);
130
- // wait for complete
131
- await firstSub.onError();
132
- await server.nextMessage;
133
- // Create a second subscription that should wait for auth
134
- const secondSub = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub2"), { expectErrors: true });
135
- // Verify no REQ message was sent yet (waiting for auth)
136
- expect(server).not.toHaveReceivedMessages(["REQ", "sub2", { kinds: [1] }]);
137
- // Simulate successful authentication
138
- relay.authenticated$.next(true);
139
- // Now the REQ should be sent
140
- await expect(server).toReceiveMessage(["REQ", "sub2", { kinds: [1] }]);
141
- // Send EVENT and EOSE to complete the subscription
142
- server.send(["EVENT", "sub2", mockEvent]);
143
- server.send(["EOSE", "sub2"]);
144
- // Verify the second subscription received the event and EOSE
145
- expect(secondSub.getValues()).toEqual([expect.objectContaining(mockEvent), "EOSE"]);
146
- });
147
- it("should wait for authentication if relay info document has limitations.auth_required = true", async () => {
148
- // Mock the fetchInformationDocument method to return a document with auth_required = true
149
- vi.spyOn(Relay, "fetchInformationDocument").mockImplementation(() => of({
150
- name: "Auth Required Relay",
151
- description: "A relay that requires authentication",
152
- pubkey: "",
153
- contact: "",
154
- supported_nips: [1, 2, 4],
155
- software: "",
156
- version: "",
157
- limitation: {
158
- auth_required: true,
159
- },
160
- }));
161
- // Create a subscription that should wait for auth
162
- const sub = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
163
- // Wait 10ms to ensure the information document is fetched
164
- await new Promise((resolve) => setTimeout(resolve, 10));
165
- // Verify no REQ message was sent yet (waiting for auth)
166
- expect(server).not.toHaveReceivedMessages(["REQ", "sub1", { kinds: [1] }]);
167
- // Simulate successful authentication
168
- relay.authenticated$.next(true);
169
- // Now the REQ should be sent
170
- await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
171
- // Send EVENT and EOSE to complete the subscription
172
- server.send(["EVENT", "sub1", mockEvent]);
173
- server.send(["EOSE", "sub1"]);
174
- // Verify the subscription received the event and EOSE
175
- expect(sub.getValues()).toEqual([expect.objectContaining(mockEvent), "EOSE"]);
176
- });
177
- it("should throw error if relay closes connection with error", async () => {
178
- const spy = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"), { expectErrors: true });
179
- await server.connected;
180
- // Send CLOSE message with error
181
- server.error({
182
- reason: "error message",
183
- code: 1000,
184
- wasClean: false,
185
- });
186
- // Verify the subscription completed with an error
187
- expect(spy.receivedError()).toBe(true);
188
- });
189
- it("should not return EOSE while waiting for the relay to be ready", async () => {
190
- vi.useFakeTimers();
191
- // @ts-expect-error
192
- relay.ready$.next(false);
193
- const spy = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"), { expectErrors: true });
194
- // Fast-forward time by 20 seconds
195
- await vi.advanceTimersByTimeAsync(20000);
196
- expect(spy.receivedComplete()).toBe(false);
197
- expect(spy.receivedError()).toBe(false);
198
- expect(spy.receivedNext()).toBe(false);
199
- });
200
- it("should wait when relay isn't ready", async () => {
201
- // @ts-expect-error
202
- relay.ready$.next(false);
203
- subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
204
- // Wait 10ms to ensure the relay didn't receive anything
205
- await new Promise((resolve) => setTimeout(resolve, 10));
206
- expect(server.messages.length).toBe(0);
207
- // @ts-expect-error
208
- relay.ready$.next(true);
209
- await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
210
- });
211
- it("should wait for filters if filters are provided as an observable", async () => {
212
- const filters = new Subject();
213
- subscribeSpyTo(relay.req(filters, "sub1"));
214
- // Wait 10sm and ensure no messages were sent yet
215
- await new Promise((resolve) => setTimeout(resolve, 10));
216
- expect(server.messagesToConsume.pendingItems.length).toBe(0);
217
- // Send REQ message with filters
218
- filters.next([{ kinds: [1] }]);
219
- // Wait for the REQ message to be sent
220
- await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
221
- });
222
- it("should update filters if filters are provided as an observable", async () => {
223
- const filters = new Subject();
224
- subscribeSpyTo(relay.req(filters, "sub1"));
225
- // Send REQ message with filters
226
- filters.next([{ kinds: [1] }]);
227
- // Should send REQ message with new filters
228
- await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
229
- // Send REQ message with filters
230
- filters.next([{ kinds: [2] }]);
231
- // Should send new REQ message with new filters
232
- await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [2] }]);
233
- // It should not send CLOSE message
234
- await expect(server.messages).not.toContain(["CLOSE", "sub1"]);
235
- });
236
- it("should complete if filters are provided as an observable that completes", async () => {
237
- const filters = new Subject();
238
- const sub = subscribeSpyTo(relay.req(filters, "sub1"));
239
- // Send REQ message with filters
240
- filters.next([{ kinds: [1] }]);
241
- // Complete the observable
242
- filters.complete();
243
- await sub.onComplete();
244
- expect(sub.receivedComplete()).toBe(true);
245
- });
246
- });
247
- describe("event", () => {
248
- it("should wait for authentication if relay responds with auth-required", async () => {
249
- // First event to trigger auth-required
250
- const firstSpy = subscribeSpyTo(relay.event(mockEvent));
251
- await expect(server).toReceiveMessage(["EVENT", mockEvent]);
252
- // Send OK with auth-required message
253
- server.send(["OK", mockEvent.id, false, "auth-required: need to authenticate"]);
254
- await firstSpy.onComplete();
255
- // Create a second event that should wait for auth
256
- const secondSpy = subscribeSpyTo(relay.event(mockEvent));
257
- // Verify no EVENT message was sent yet (waiting for auth)
258
- expect(server).not.toHaveReceivedMessages(["EVENT", mockEvent]);
259
- // Simulate successful authentication
260
- relay.authenticated$.next(true);
261
- // Now the EVENT should be sent
262
- await expect(server).toReceiveMessage(["EVENT", mockEvent]);
263
- // Send OK response to complete the event
264
- server.send(["OK", mockEvent.id, true, ""]);
265
- // Verify the second event completed successfully
266
- await secondSpy.onComplete();
267
- expect(secondSpy.receivedComplete()).toBe(true);
268
- });
269
- it("should wait for authentication if relay info document has limitations.auth_required = true", async () => {
270
- // Mock the fetchInformationDocument method to return a document with auth_required = true
271
- vi.spyOn(Relay, "fetchInformationDocument").mockImplementation(() => of({
272
- name: "Auth Required Relay",
273
- description: "A relay that requires authentication",
274
- pubkey: "",
275
- contact: "",
276
- supported_nips: [1, 2, 4],
277
- software: "",
278
- version: "",
279
- limitation: {
280
- auth_required: true,
281
- },
282
- }));
283
- // Create a subscription that should wait for auth
284
- const sub = subscribeSpyTo(relay.event(mockEvent));
285
- // Wait 10ms to ensure the information document is fetched
286
- await new Promise((resolve) => setTimeout(resolve, 10));
287
- // Verify no REQ message was sent yet (waiting for auth)
288
- expect(server).not.toHaveReceivedMessages(["EVENT", mockEvent]);
289
- // Simulate successful authentication
290
- relay.authenticated$.next(true);
291
- // Now the REQ should be sent
292
- await expect(server).toReceiveMessage(["EVENT", mockEvent]);
293
- // Send EVENT and EOSE to complete the subscription
294
- server.send(["OK", mockEvent.id, true, ""]);
295
- // Verify the subscription completed
296
- await sub.onComplete();
297
- expect(sub.receivedComplete()).toBe(true);
298
- });
299
- it("should trigger connection to relay", async () => {
300
- subscribeSpyTo(relay.event(mockEvent));
301
- // Wait for connection
302
- await firstValueFrom(relay.connected$.pipe(filter(Boolean)));
303
- expect(relay.connected).toBe(true);
304
- });
305
- it("observable should complete when matching OK response received", async () => {
306
- const spy = subscribeSpyTo(relay.event(mockEvent));
307
- // Verify EVENT message was sent
308
- expect(await server.nextMessage).toEqual(["EVENT", mockEvent]);
309
- // Send matching OK response
310
- server.send(["OK", mockEvent.id, true, ""]);
311
- await spy.onComplete();
312
- expect(spy.receivedComplete()).toBe(true);
313
- });
314
- it("should ignore OK responses for different events", async () => {
315
- const spy = subscribeSpyTo(relay.event(mockEvent));
316
- await server.connected;
317
- // Send non-matching OK response
318
- server.send(["OK", "different_id", true, ""]);
319
- expect(spy.receivedComplete()).toBe(false);
320
- // Send matching OK response
321
- server.send(["OK", mockEvent.id, true, ""]);
322
- expect(spy.receivedComplete()).toBe(true);
323
- });
324
- it("should send EVENT message to relay", async () => {
325
- relay.event(mockEvent).subscribe();
326
- expect(await server.nextMessage).toEqual(["EVENT", mockEvent]);
327
- });
328
- it("should complete with error if no OK received within 10s", async () => {
329
- vi.useFakeTimers();
330
- const spy = subscribeSpyTo(relay.event(mockEvent));
331
- // Fast-forward time by 10 seconds
332
- await vi.advanceTimersByTimeAsync(10000);
333
- expect(spy.receivedComplete()).toBe(true);
334
- expect(spy.getLastValue()).toEqual({ ok: false, from: "wss://test", message: "Timeout" });
335
- });
336
- it("should throw error if relay closes connection with error", async () => {
337
- const spy = subscribeSpyTo(relay.event(mockEvent), { expectErrors: true });
338
- await server.connected;
339
- // Send CLOSE message with error
340
- server.error({
341
- reason: "error message",
342
- code: 1000,
343
- wasClean: false,
344
- });
345
- // Verify the subscription completed with an error
346
- expect(spy.receivedError()).toBe(true);
347
- });
348
- it("should not throw a timeout error while waiting for the relay to be ready", async () => {
349
- vi.useFakeTimers();
350
- // @ts-expect-error
351
- relay.ready$.next(false);
352
- const spy = subscribeSpyTo(relay.event(mockEvent), { expectErrors: true });
353
- // Fast-forward time by 20 seconds
354
- await vi.advanceTimersByTimeAsync(20000);
355
- expect(spy.receivedComplete()).toBe(false);
356
- expect(spy.receivedError()).toBe(false);
357
- });
358
- it("should wait when relay isn't ready", async () => {
359
- // @ts-expect-error
360
- relay.ready$.next(false);
361
- subscribeSpyTo(relay.event(mockEvent));
362
- // Wait 10ms to ensure the relay didn't receive anything
363
- await new Promise((resolve) => setTimeout(resolve, 10));
364
- expect(server.messages.length).toBe(0);
365
- // @ts-expect-error
366
- relay.ready$.next(true);
367
- await expect(server).toReceiveMessage(["EVENT", mockEvent]);
368
- });
369
- });
370
- describe("notices$", () => {
371
- it("should not trigger connection to relay", async () => {
372
- subscribeSpyTo(relay.notices$);
373
- expect(relay.connected).toBe(false);
374
- });
375
- it("should accumulate notices in notices$ state", async () => {
376
- subscribeSpyTo(relay.req({ kinds: [1] }));
377
- // Send multiple NOTICE messages
378
- server.send(["NOTICE", "Notice 1"]);
379
- server.send(["NOTICE", "Notice 2"]);
380
- server.send(["NOTICE", "Notice 3"]);
381
- // Verify the notices state contains all messages
382
- expect(relay.notices$.value).toEqual(["Notice 1", "Notice 2", "Notice 3"]);
383
- });
384
- it("should ignore non-NOTICE messages", async () => {
385
- subscribeSpyTo(relay.req({ kinds: [1] }));
386
- server.send(["NOTICE", "Important notice"]);
387
- server.send(["OTHER", "other message"]);
388
- // Verify only NOTICE messages are in the state
389
- expect(relay.notices$.value).toEqual(["Important notice"]);
390
- });
391
- });
392
- describe("notice$", () => {
393
- it("should not trigger connection to relay", async () => {
394
- subscribeSpyTo(relay.notice$);
395
- await new Promise((resolve) => setTimeout(resolve, 10));
396
- expect(relay.connected).toBe(false);
397
- });
398
- it("should emit NOTICE messages when they are received", async () => {
399
- const spy = subscribeSpyTo(relay.notice$);
400
- // Start connection
401
- subscribeSpyTo(relay.req({ kinds: [1] }));
402
- // Send multiple NOTICE messages
403
- server.send(["NOTICE", "Notice 1"]);
404
- server.send(["NOTICE", "Notice 2"]);
405
- server.send(["NOTICE", "Notice 3"]);
406
- // Verify the notices state contains all messages
407
- expect(spy.getValues()).toEqual(["Notice 1", "Notice 2", "Notice 3"]);
408
- });
409
- it("should ignore non-NOTICE messages", async () => {
410
- const spy = subscribeSpyTo(relay.notice$);
411
- // Start connection
412
- subscribeSpyTo(relay.req({ kinds: [1] }));
413
- server.send(["NOTICE", "Important notice"]);
414
- server.send(["OTHER", "other message"]);
415
- // Verify only NOTICE messages are in the state
416
- expect(spy.getValues()).toEqual(["Important notice"]);
417
- });
418
- });
419
- describe("message$", () => {
420
- it("should not trigger connection to relay", async () => {
421
- subscribeSpyTo(relay.message$);
422
- await new Promise((resolve) => setTimeout(resolve, 10));
423
- expect(relay.connected).toBe(false);
424
- });
425
- it("should emit all messages when they are received", async () => {
426
- const spy = subscribeSpyTo(relay.message$);
427
- // Start connection
428
- subscribeSpyTo(relay.req({ kinds: [1] }));
429
- // Send multiple NOTICE messages
430
- server.send(["NOTICE", "Notice 1"]);
431
- server.send(["EVENT", "sub1", mockEvent]);
432
- server.send(["EOSE", "sub1"]);
433
- // Verify the notices state contains all messages
434
- expect(spy.getValues()).toEqual([
435
- ["NOTICE", "Notice 1"],
436
- ["EVENT", "sub1", mockEvent],
437
- ["EOSE", "sub1"],
438
- ]);
439
- });
440
- });
441
- describe("challenge$", () => {
442
- it("should not trigger connection to relay", async () => {
443
- subscribeSpyTo(relay.challenge$);
444
- await new Promise((resolve) => setTimeout(resolve, 10));
445
- expect(relay.connected).toBe(false);
446
- });
447
- it("should set challenge$ when AUTH message received", async () => {
448
- subscribeSpyTo(relay.req({ kinds: [1] }));
449
- // Send AUTH message with challenge string
450
- server.send(["AUTH", "challenge-string-123"]);
451
- // Verify challenge$ was set
452
- expect(relay.challenge$.value).toBe("challenge-string-123");
453
- });
454
- it("should ignore non-AUTH messages", async () => {
455
- subscribeSpyTo(relay.req({ kinds: [1] }));
456
- server.send(["NOTICE", "Not a challenge"]);
457
- server.send(["OTHER", "other message"]);
458
- // Verify challenge$ remains null
459
- expect(relay.challenge$.value).toBe(null);
460
- });
461
- });
462
- describe("information$", () => {
463
- it("should fetch information document when information$ is subscribed to", async () => {
464
- // Mock the fetchInformationDocument method
465
- const mockInfo = { ...defaultMockInfo, limitation: { auth_required: false } };
466
- vi.spyOn(Relay, "fetchInformationDocument").mockReturnValue(of(mockInfo));
467
- // Subscribe to information$
468
- const sub = subscribeSpyTo(relay.information$);
469
- // Verify fetchInformationDocument was called with the relay URL
470
- expect(Relay.fetchInformationDocument).toHaveBeenCalledWith(relay.url);
471
- // Verify the information was emitted
472
- expect(sub.getLastValue()).toEqual(mockInfo);
473
- });
474
- it("should return null when fetchInformationDocument fails", async () => {
475
- // Mock the fetchInformationDocument method to throw an error
476
- vi.spyOn(Relay, "fetchInformationDocument").mockReturnValue(throwError(() => new Error("Failed to fetch")));
477
- // Subscribe to information$
478
- const sub = subscribeSpyTo(relay.information$);
479
- // Verify fetchInformationDocument was called
480
- expect(Relay.fetchInformationDocument).toHaveBeenCalled();
481
- // Verify null was emitted
482
- expect(sub.getLastValue()).toBeNull();
483
- });
484
- it("should cache the information document", async () => {
485
- // Mock the fetchInformationDocument method
486
- const mockInfo = { ...defaultMockInfo, limitation: { auth_required: true } };
487
- vi.spyOn(Relay, "fetchInformationDocument").mockReturnValue(of(mockInfo));
488
- // Subscribe to information$ multiple times
489
- const sub1 = subscribeSpyTo(relay.information$);
490
- const sub2 = subscribeSpyTo(relay.information$);
491
- // Verify fetchInformationDocument was called only once
492
- expect(Relay.fetchInformationDocument).toHaveBeenCalledTimes(1);
493
- // Verify both subscriptions received the same information
494
- expect(sub1.getLastValue()).toEqual(mockInfo);
495
- expect(sub2.getLastValue()).toEqual(mockInfo);
496
- // Verify the internal state was updated
497
- expect(relay.information).toEqual(mockInfo);
498
- });
499
- });
500
- describe("createReconnectTimer", () => {
501
- it("should create a reconnect timer when relay closes with error", async () => {
502
- const reconnectTimer = vi.fn().mockReturnValue(timer(1000));
503
- vi.spyOn(Relay, "createReconnectTimer").mockReturnValue(reconnectTimer);
504
- relay = new Relay("wss://test");
505
- const spy = subscribeSpyTo(relay.req([{ kinds: [1] }]), { expectErrors: true });
506
- // Send CLOSE message with error
507
- server.error({
508
- reason: "error message",
509
- code: 1000,
510
- wasClean: false,
511
- });
512
- // Verify the subscription errored
513
- expect(spy.receivedError()).toBe(true);
514
- expect(reconnectTimer).toHaveBeenCalledWith(expect.any(Error), 0);
515
- });
516
- it("should set ready$ to false until the reconnect timer completes", async () => {
517
- vi.useFakeTimers();
518
- const reconnectTimer = vi.fn().mockReturnValue(timer(1000));
519
- vi.spyOn(Relay, "createReconnectTimer").mockReturnValue(reconnectTimer);
520
- relay = new Relay("wss://test");
521
- subscribeSpyTo(relay.req([{ kinds: [1] }]), { expectErrors: true });
522
- // Send CLOSE message with error
523
- server.error({
524
- reason: "error message",
525
- code: 1000,
526
- wasClean: false,
527
- });
528
- // @ts-expect-error
529
- expect(relay.ready$.value).toBe(false);
530
- // Fast-forward time by 10ms
531
- await vi.advanceTimersByTimeAsync(5000);
532
- // @ts-expect-error
533
- expect(relay.ready$.value).toBe(true);
534
- });
535
- });
536
- describe("publish", () => {
537
- it("should retry when auth-required is received and authentication is completed", async () => {
538
- // First attempt to publish
539
- const spy = subscribeSpyTo(relay.publish(mockEvent));
540
- // Verify EVENT was sent
541
- await expect(server).toReceiveMessage(["EVENT", mockEvent]);
542
- // Send auth-required response
543
- server.send(["AUTH", "challenge-string"]);
544
- server.send(["OK", mockEvent.id, false, "auth-required: need to authenticate"]);
545
- // Send auth event
546
- const authEvent = { ...mockEvent, id: "auth-id" };
547
- subscribeSpyTo(relay.auth(authEvent));
548
- // Verify AUTH was sent
549
- await expect(server).toReceiveMessage(["AUTH", authEvent]);
550
- // Send successful auth response
551
- server.send(["OK", authEvent.id, true, ""]);
552
- // Wait for the event to be sent again
553
- await expect(server).toReceiveMessage(["EVENT", mockEvent]);
554
- // Send successful response for the retried event
555
- server.send(["OK", mockEvent.id, true, ""]);
556
- // Verify the final result is successful
557
- expect(spy.getLastValue()).toEqual({ ok: true, message: "", from: "wss://test" });
558
- });
559
- it("should error after max retries", async () => {
560
- const spy = subscribeSpyTo(relay.publish(mockEvent, { retries: 0 }), { expectErrors: true });
561
- // Close with error
562
- server.error({ reason: "error message", code: 1000, wasClean: false });
563
- // Verify the subscription errored
564
- expect(spy.receivedError()).toBe(true);
565
- });
566
- });
567
- describe("request", () => {
568
- it("should retry when auth-required is received and authentication is completed", async () => {
569
- // First attempt to request
570
- const spy = subscribeSpyTo(relay.request({ kinds: [1] }, { id: "sub1" }));
571
- // Verify REQ was sent
572
- await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
573
- // Send auth-required response
574
- server.send(["AUTH", "challenge-string"]);
575
- server.send(["CLOSED", "sub1", "auth-required: need to authenticate"]);
576
- // Wait for subscription to close
577
- await expect(server).toReceiveMessage(["CLOSE", "sub1"]);
578
- // Send auth event
579
- const authEvent = { ...mockEvent, id: "auth-id" };
580
- const authSpy = subscribeSpyTo(relay.auth(authEvent));
581
- // Verify AUTH was sent
582
- await expect(server).toReceiveMessage(["AUTH", authEvent]);
583
- server.send(["OK", authEvent.id, true, ""]);
584
- // Wait for auth to complete
585
- await authSpy.onComplete();
586
- // Wait for retry
587
- await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
588
- // Send response
589
- server.send(["EVENT", "sub1", mockEvent]);
590
- server.send(["EOSE", "sub1"]);
591
- // Verify the final result is successful
592
- expect(spy.getLastValue()).toEqual(expect.objectContaining(mockEvent));
593
- expect(spy.receivedComplete()).toBe(true);
594
- });
595
- });
596
- describe("subscription", () => {
597
- it("should retry when auth-required is received and authentication is completed", async () => {
598
- // First attempt to request
599
- const spy = subscribeSpyTo(relay.subscription({ kinds: [1] }, { id: "sub1" }));
600
- // Verify REQ was sent
601
- await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
602
- // Send auth-required response
603
- server.send(["AUTH", "challenge-string"]);
604
- server.send(["CLOSED", "sub1", "auth-required: need to authenticate"]);
605
- // Wait for subscription to close
606
- await expect(server).toReceiveMessage(["CLOSE", "sub1"]);
607
- // Send auth event
608
- const authEvent = { ...mockEvent, id: "auth-id" };
609
- const authSpy = subscribeSpyTo(relay.auth(authEvent));
610
- // Verify AUTH was sent
611
- await expect(server).toReceiveMessage(["AUTH", authEvent]);
612
- server.send(["OK", authEvent.id, true, ""]);
613
- // Wait for auth to complete
614
- await authSpy.onComplete();
615
- // Wait for retry
616
- await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
617
- // Send response
618
- server.send(["EVENT", "sub1", mockEvent]);
619
- server.send(["EOSE", "sub1"]);
620
- // Verify the final result is successful
621
- expect(spy.getValues()).toEqual([expect.objectContaining(mockEvent), "EOSE"]);
622
- expect(spy.receivedComplete()).toBe(false);
623
- });
624
- });
625
- // describe("keepAlive", () => {
626
- // it("should close the socket connection after keepAlive timeout", async () => {
627
- // vi.useFakeTimers();
628
- // // Set a short keepAlive timeout for testing
629
- // relay.keepAlive = 100; // 100ms for quick testing
630
- // // Subscribe to the relay to ensure it is active
631
- // const sub = subscribeSpyTo(relay.req([{ kinds: [1] }]));
632
- // // Wait for connection
633
- // await server.connected;
634
- // // Close the subscription
635
- // sub.unsubscribe();
636
- // // Fast-forward time by 10ms
637
- // await vi.advanceTimersByTimeAsync(10);
638
- // // should still be connected
639
- // expect(relay.connected).toBe(true);
640
- // // Wait for the keepAlive timeout to elapse
641
- // await vi.advanceTimersByTimeAsync(150);
642
- // expect(relay.connected).toBe(false);
643
- // });
644
- // });
@@ -1 +0,0 @@
1
- export {};
@@ -1,15 +0,0 @@
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
- "completeOnEose",
8
- "markFromRelay",
9
- "onlyEvents",
10
- "storeEvents",
11
- "toEventStore",
12
- ]
13
- `);
14
- });
15
- });