applesauce-relay 1.1.0 → 2.0.0

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/group.d.ts CHANGED
@@ -1,19 +1,19 @@
1
- import { Filter, NostrEvent } from "nostr-tools";
1
+ import { type NostrEvent } from "nostr-tools";
2
2
  import { Observable } from "rxjs";
3
- import { IGroup, IRelay, PublishResponse, SubscriptionResponse, PublishOptions, RequestOptions, SubscriptionOptions } from "./types.js";
3
+ import { IGroup, IRelay, PublishResponse, SubscriptionResponse, PublishOptions, RequestOptions, SubscriptionOptions, FilterInput } from "./types.js";
4
4
  export declare class RelayGroup implements IGroup {
5
5
  relays: IRelay[];
6
6
  constructor(relays: IRelay[]);
7
7
  /** Takes an array of observables and only emits EOSE when all observables have emitted EOSE */
8
8
  protected mergeEOSE(...requests: Observable<SubscriptionResponse>[]): Observable<import("nostr-tools").Event | "EOSE">;
9
9
  /** Make a request to all relays */
10
- req(filters: Filter | Filter[], id?: string): Observable<SubscriptionResponse>;
10
+ req(filters: FilterInput, id?: string): Observable<SubscriptionResponse>;
11
11
  /** Send an event to all relays */
12
12
  event(event: NostrEvent): Observable<PublishResponse>;
13
13
  /** Publish an event to all relays with retries ( default 3 retries ) */
14
14
  publish(event: NostrEvent, opts?: PublishOptions): Observable<PublishResponse>;
15
15
  /** Request events from all relays with retries ( default 3 retries ) */
16
- request(filters: Filter | Filter[], opts?: RequestOptions): Observable<NostrEvent>;
16
+ request(filters: FilterInput, opts?: RequestOptions): Observable<NostrEvent>;
17
17
  /** Open a subscription to all relays with retries ( default 3 retries ) */
18
- subscription(filters: Filter | Filter[], opts?: SubscriptionOptions): Observable<SubscriptionResponse>;
18
+ subscription(filters: FilterInput, opts?: SubscriptionOptions): Observable<SubscriptionResponse>;
19
19
  }
@@ -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/pool.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { NostrEvent, type Filter } from "nostr-tools";
1
+ import { type NostrEvent } from "nostr-tools";
2
2
  import { BehaviorSubject, Observable } from "rxjs";
3
3
  import { RelayGroup } from "./group.js";
4
4
  import { Relay, RelayOptions } from "./relay.js";
5
- import { IPool, PublishResponse, PublishOptions, RequestOptions, SubscriptionOptions, SubscriptionResponse } from "./types.js";
5
+ import { IPool, PublishResponse, PublishOptions, RequestOptions, SubscriptionOptions, SubscriptionResponse, FilterInput } from "./types.js";
6
6
  export declare class RelayPool implements IPool {
7
7
  options?: RelayOptions | undefined;
8
8
  groups$: BehaviorSubject<Map<string, RelayGroup>>;
@@ -18,13 +18,13 @@ export declare class RelayPool implements IPool {
18
18
  /** Create a group of relays */
19
19
  group(relays: string[]): RelayGroup;
20
20
  /** Make a REQ to multiple relays that does not deduplicate events */
21
- req(relays: string[], filters: Filter | Filter[], id?: string): Observable<SubscriptionResponse>;
21
+ req(relays: string[], filters: FilterInput, id?: string): Observable<SubscriptionResponse>;
22
22
  /** Send an EVENT message to multiple relays */
23
23
  event(relays: string[], event: NostrEvent): Observable<PublishResponse>;
24
24
  /** Publish an event to multiple relays */
25
25
  publish(relays: string[], event: NostrEvent, opts?: PublishOptions): Observable<PublishResponse>;
26
26
  /** Request events from multiple relays */
27
- request(relays: string[], filters: Filter | Filter[], opts?: RequestOptions): Observable<NostrEvent>;
27
+ request(relays: string[], filters: FilterInput, opts?: RequestOptions): Observable<NostrEvent>;
28
28
  /** Open a subscription to multiple relays */
29
- subscription(relays: string[], filters: Filter | Filter[], opts?: SubscriptionOptions): Observable<SubscriptionResponse>;
29
+ subscription(relays: string[], filters: FilterInput, opts?: SubscriptionOptions): Observable<SubscriptionResponse>;
30
30
  }
package/dist/pool.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { BehaviorSubject } from "rxjs";
2
2
  import { RelayGroup } from "./group.js";
3
3
  import { Relay } from "./relay.js";
4
+ import { normalizeURL } from "applesauce-core/helpers";
4
5
  export class RelayPool {
5
6
  options;
6
7
  groups$ = new BehaviorSubject(new Map());
@@ -21,19 +22,25 @@ export class RelayPool {
21
22
  }
22
23
  /** Get or create a new relay connection */
23
24
  relay(url) {
25
+ // Normalize the url
26
+ url = normalizeURL(url);
27
+ // Check if the url is blacklisted
24
28
  if (this.blacklist.has(url))
25
29
  throw new Error("Relay is on blacklist");
30
+ // Check if the relay already exists
26
31
  let relay = this.relays.get(url);
27
32
  if (relay)
28
33
  return relay;
29
- else {
30
- relay = new Relay(url, this.options);
31
- this.relays$.next(this.relays.set(url, relay));
32
- return relay;
33
- }
34
+ // Create a new relay
35
+ relay = new Relay(url, this.options);
36
+ this.relays$.next(this.relays.set(url, relay));
37
+ return relay;
34
38
  }
35
39
  /** Create a group of relays */
36
40
  group(relays) {
41
+ // Normalize all urls
42
+ relays = relays.map((url) => normalizeURL(url));
43
+ // Filter out any blacklisted relays
37
44
  relays = this.filterBlacklist(relays);
38
45
  const key = relays.sort().join(",");
39
46
  let group = this.groups.get(key);
package/dist/relay.d.ts CHANGED
@@ -3,7 +3,7 @@ import { type Filter, type NostrEvent } from "nostr-tools";
3
3
  import { BehaviorSubject, Observable } from "rxjs";
4
4
  import { WebSocketSubject, WebSocketSubjectConfig } from "rxjs/webSocket";
5
5
  import { RelayInformation } from "nostr-tools/nip11";
6
- import { AuthSigner, IRelay, PublishOptions, PublishResponse, RequestOptions, SubscriptionOptions, SubscriptionResponse } from "./types.js";
6
+ import { AuthSigner, FilterInput, IRelay, PublishOptions, PublishResponse, RequestOptions, SubscriptionOptions, SubscriptionResponse } from "./types.js";
7
7
  /** An error that is thrown when a REQ is closed from the relay side */
8
8
  export declare class ReqCloseError extends Error {
9
9
  }
@@ -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>;
@@ -74,7 +74,7 @@ export declare class Relay implements IRelay {
74
74
  /** Send a message to the relay */
75
75
  next(message: any): void;
76
76
  /** Create a REQ observable that emits events or "EOSE" or errors */
77
- req(filters: Filter | Filter[], id?: string): Observable<SubscriptionResponse>;
77
+ req(filters: FilterInput, id?: string): Observable<SubscriptionResponse>;
78
78
  /** Send an EVENT or AUTH message and return an observable of PublishResponse that completes or errors */
79
79
  event(event: NostrEvent, verb?: "EVENT" | "AUTH"): Observable<PublishResponse>;
80
80
  /** send and AUTH message */
package/dist/relay.js CHANGED
@@ -2,7 +2,7 @@ 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, filter, from, ignoreElements, map, merge, mergeMap, NEVER, of, retry, scan, share, shareReplay, Subject, switchMap, take, 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
7
  import { completeOnEose } from "./operators/complete-on-eose.js";
8
8
  import { markFromRelay } from "./operators/mark-from-relay.js";
@@ -85,7 +85,7 @@ export class Relay {
85
85
  if (this.receivedAuthRequiredForEvent.value)
86
86
  this.receivedAuthRequiredForEvent.next(false);
87
87
  }
88
- /** An internal observable that is responsible for watching all messages and updating state */
88
+ /** An internal observable that is responsible for watching all messages and updating state, subscribing to it will trigger a connection to the relay */
89
89
  watchTower;
90
90
  constructor(url, opts) {
91
91
  this.url = url;
@@ -190,11 +190,13 @@ export class Relay {
190
190
  .pipe(take(1))
191
191
  .subscribe(() => this.ready$.next(true));
192
192
  }
193
- /** Wait for ready and authenticated */
193
+ /** Wait for authentication state, make connection and then wait for authentication if required */
194
194
  waitForAuth(
195
195
  // NOTE: require BehaviorSubject so it always has a value
196
196
  requireAuth, observable) {
197
197
  return combineLatest([requireAuth, this.authenticated$]).pipe(
198
+ // Once the auth state is known, make a connection and watch for auth challenges
199
+ mergeWith(this.watchTower),
198
200
  // wait for auth not required or authenticated
199
201
  filter(([required, authenticated]) => !required || authenticated),
200
202
  // complete after the first value so this does not repeat
@@ -225,10 +227,24 @@ export class Relay {
225
227
  }
226
228
  /** Create a REQ observable that emits events or "EOSE" or errors */
227
229
  req(filters, id = nanoid()) {
228
- const request = this.socket.multiplex(() => (Array.isArray(filters) ? ["REQ", id, ...filters] : ["REQ", id, filters]), () => ["CLOSE", id], (message) => (message[0] === "EVENT" || message[0] === "CLOSED" || message[0] === "EOSE") && message[1] === id);
229
- // Start the watch tower with the observable
230
- const withWatchTower = merge(this.watchTower, request);
231
- const observable = withWatchTower.pipe(
230
+ // Convert filters input into an observable, if its a normal value merge it with NEVER so it never completes
231
+ const input = isObservable(filters) ? filters : merge(of(filters), NEVER);
232
+ // Create an observable that completes when the upstream observable completes
233
+ const complete = input.pipe(ignoreElements(), endWith(null));
234
+ // Create an observable that filters responses from the relay to just the ones for this REQ
235
+ const messages = this.socket.pipe(filter((m) => Array.isArray(m) && (m[0] === "EVENT" || m[0] === "CLOSED" || m[0] === "EOSE") && m[1] === id));
236
+ // Create an observable that controls sending the filters and closing the REQ
237
+ const control = input.pipe(
238
+ // Send the filters when they change
239
+ tap((filters) => this.socket.next(Array.isArray(filters) ? ["REQ", id, ...filters] : ["REQ", id, filters])),
240
+ // Close the req when unsubscribed
241
+ finalize(() => this.socket.next(["CLOSE", id])),
242
+ // Once filters have been sent, switch to listening for messages
243
+ switchMap(() => messages));
244
+ // Start the watch tower with the observables
245
+ const observable = merge(this.watchTower, control).pipe(
246
+ // Complete the subscription when the input is completed
247
+ takeUntil(complete),
232
248
  // Map the messages to events, EOSE, or throw an error
233
249
  map((message) => {
234
250
  if (message[0] === "EOSE")
@@ -270,10 +286,8 @@ export class Relay {
270
286
  // format OK message
271
287
  map((m) => ({ ok: m[2], message: m[3], from: this.url })));
272
288
  });
273
- // Start the watch tower with the observable
274
- const withWatchTower = merge(this.watchTower, base);
275
- // Add complete operators
276
- const observable = withWatchTower.pipe(
289
+ // Start the watch tower and add complete operators
290
+ const observable = merge(this.watchTower, base).pipe(
277
291
  // complete on first value
278
292
  take(1),
279
293
  // listen for OK auth-required
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { EventTemplate, Filter, NostrEvent } from "nostr-tools";
1
+ import { type EventTemplate, type Filter, type NostrEvent } from "nostr-tools";
2
2
  import { Observable } from "rxjs";
3
3
  import { WebSocketSubject } from "rxjs/webSocket";
4
4
  export type SubscriptionResponse = NostrEvent | "EOSE";
@@ -28,11 +28,13 @@ export type SubscriptionOptions = {
28
28
  export type AuthSigner = {
29
29
  signEvent: (event: EventTemplate) => NostrEvent | Promise<NostrEvent>;
30
30
  };
31
+ /** The type of input the REQ method accepts */
32
+ export type FilterInput = Filter | Filter[] | Observable<Filter | Filter[]>;
31
33
  export interface Nip01Actions {
32
34
  /** Send an EVENT message */
33
35
  event(event: NostrEvent): Observable<PublishResponse>;
34
36
  /** Send a REQ message */
35
- req(filters: Filter | Filter[], id?: string): Observable<SubscriptionResponse>;
37
+ req(filters: FilterInput, id?: string): Observable<SubscriptionResponse>;
36
38
  }
37
39
  export interface IRelay extends MultiplexWebSocket, Nip01Actions, IRelayState {
38
40
  url: string;
@@ -52,12 +54,12 @@ export interface IRelay extends MultiplexWebSocket, Nip01Actions, IRelayState {
52
54
  retries?: number;
53
55
  }): Observable<PublishResponse>;
54
56
  /** Send a REQ message with retries */
55
- request(filters: Filter | Filter[], opts?: {
57
+ request(filters: FilterInput, opts?: {
56
58
  id?: string;
57
59
  retries?: number;
58
60
  }): Observable<NostrEvent>;
59
61
  /** Open a subscription with retries */
60
- subscription(filters: Filter | Filter[], opts?: {
62
+ subscription(filters: FilterInput, opts?: {
61
63
  id?: string;
62
64
  retries?: number;
63
65
  }): Observable<SubscriptionResponse>;
@@ -68,12 +70,12 @@ export interface IGroup extends Nip01Actions {
68
70
  retries?: number;
69
71
  }): Observable<PublishResponse>;
70
72
  /** Send a REQ message with retries */
71
- request(filters: Filter | Filter[], opts?: {
73
+ request(filters: FilterInput, opts?: {
72
74
  id?: string;
73
75
  retries?: number;
74
76
  }): Observable<NostrEvent>;
75
77
  /** Open a subscription with retries */
76
- subscription(filters: Filter | Filter[], opts?: {
78
+ subscription(filters: FilterInput, opts?: {
77
79
  id?: string;
78
80
  retries?: number;
79
81
  }): Observable<SubscriptionResponse>;
@@ -82,7 +84,7 @@ export interface IPool {
82
84
  /** Send an EVENT message */
83
85
  event(relays: string[], event: NostrEvent): Observable<PublishResponse>;
84
86
  /** Send a REQ message */
85
- req(relays: string[], filters: Filter | Filter[], id?: string): Observable<SubscriptionResponse>;
87
+ req(relays: string[], filters: FilterInput, id?: string): Observable<SubscriptionResponse>;
86
88
  /** Get or create a relay */
87
89
  relay(url: string): IRelay;
88
90
  /** Create a relay group */
@@ -92,12 +94,12 @@ export interface IPool {
92
94
  retries?: number;
93
95
  }): Observable<PublishResponse>;
94
96
  /** Send a REQ message to relays with retries */
95
- request(relays: string[], filters: Filter | Filter[], opts?: {
97
+ request(relays: string[], filters: FilterInput, opts?: {
96
98
  id?: string;
97
99
  retries?: number;
98
100
  }): Observable<NostrEvent>;
99
101
  /** Open a subscription to relays with retries */
100
- subscription(relays: string[], filters: Filter | Filter[], opts?: {
102
+ subscription(relays: string[], filters: FilterInput, opts?: {
101
103
  id?: string;
102
104
  retries?: number;
103
105
  }): Observable<SubscriptionResponse>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-relay",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
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.0.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,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 {};