applesauce-relay 0.0.0-next-20250430170741 → 0.0.0-next-20250430195017

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.
@@ -47,17 +47,33 @@ describe("req", () => {
47
47
  await firstValueFrom(relay.connected$.pipe(filter(Boolean)));
48
48
  expect(relay.connected).toBe(true);
49
49
  });
50
- it("should send REQ and CLOSE messages", async () => {
50
+ it("should send expected messages to relay", async () => {
51
+ subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
52
+ // Wait for all message to be sent
53
+ await new Promise((resolve) => setTimeout(resolve, 10));
54
+ expect(server.messages).toEqual([["REQ", "sub1", { kinds: [1] }]]);
55
+ });
56
+ it("should not close the REQ when EOSE is received", async () => {
51
57
  // Create subscription that completes after first EOSE
52
- const sub = relay.req([{ kinds: [1] }], "sub1").subscribe();
58
+ const sub = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
53
59
  // Verify REQ was sent
54
- expect(await server.nextMessage).toEqual(["REQ", "sub1", { kinds: [1] }]);
60
+ await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
55
61
  // Send EOSE to complete subscription
62
+ server.send(["EVENT", "sub1", mockEvent]);
56
63
  server.send(["EOSE", "sub1"]);
64
+ // Verify the subscription did not complete
65
+ expect(sub.receivedComplete()).toBe(false);
66
+ expect(sub.getValues()).toEqual([expect.objectContaining(mockEvent), "EOSE"]);
67
+ });
68
+ it("should send CLOSE when unsubscribed", async () => {
69
+ // Create subscription that completes after first EOSE
70
+ const sub = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
71
+ // Verify REQ was sent
72
+ await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
57
73
  // Complete the subscription
58
74
  sub.unsubscribe();
59
75
  // Verify CLOSE was sent
60
- expect(await server.nextMessage).toEqual(["CLOSE", "sub1"]);
76
+ await expect(server).toReceiveMessage(["CLOSE", "sub1"]);
61
77
  });
62
78
  it("should emit nostr event and EOSE", async () => {
63
79
  const spy = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
@@ -332,9 +348,59 @@ describe("notices$", () => {
332
348
  expect(relay.notices$.value).toEqual(["Important notice"]);
333
349
  });
334
350
  });
351
+ describe("notice$", () => {
352
+ it("should not trigger connection to relay", async () => {
353
+ subscribeSpyTo(relay.notice$);
354
+ await new Promise((resolve) => setTimeout(resolve, 10));
355
+ expect(relay.connected).toBe(false);
356
+ });
357
+ it("should emit NOTICE messages when they are received", async () => {
358
+ const spy = subscribeSpyTo(relay.notice$);
359
+ // Start connection
360
+ subscribeSpyTo(relay.req({ kinds: [1] }));
361
+ // Send multiple NOTICE messages
362
+ server.send(["NOTICE", "Notice 1"]);
363
+ server.send(["NOTICE", "Notice 2"]);
364
+ server.send(["NOTICE", "Notice 3"]);
365
+ // Verify the notices state contains all messages
366
+ expect(spy.getValues()).toEqual(["Notice 1", "Notice 2", "Notice 3"]);
367
+ });
368
+ it("should ignore non-NOTICE messages", async () => {
369
+ const spy = subscribeSpyTo(relay.notice$);
370
+ // Start connection
371
+ subscribeSpyTo(relay.req({ kinds: [1] }));
372
+ server.send(["NOTICE", "Important notice"]);
373
+ server.send(["OTHER", "other message"]);
374
+ // Verify only NOTICE messages are in the state
375
+ expect(spy.getValues()).toEqual(["Important notice"]);
376
+ });
377
+ });
378
+ describe("message$", () => {
379
+ it("should not trigger connection to relay", async () => {
380
+ subscribeSpyTo(relay.message$);
381
+ await new Promise((resolve) => setTimeout(resolve, 10));
382
+ expect(relay.connected).toBe(false);
383
+ });
384
+ it("should emit all messages when they are received", async () => {
385
+ const spy = subscribeSpyTo(relay.message$);
386
+ // Start connection
387
+ subscribeSpyTo(relay.req({ kinds: [1] }));
388
+ // Send multiple NOTICE messages
389
+ server.send(["NOTICE", "Notice 1"]);
390
+ server.send(["EVENT", "sub1", mockEvent]);
391
+ server.send(["EOSE", "sub1"]);
392
+ // Verify the notices state contains all messages
393
+ expect(spy.getValues()).toEqual([
394
+ ["NOTICE", "Notice 1"],
395
+ ["EVENT", "sub1", mockEvent],
396
+ ["EOSE", "sub1"],
397
+ ]);
398
+ });
399
+ });
335
400
  describe("challenge$", () => {
336
401
  it("should not trigger connection to relay", async () => {
337
402
  subscribeSpyTo(relay.challenge$);
403
+ await new Promise((resolve) => setTimeout(resolve, 10));
338
404
  expect(relay.connected).toBe(false);
339
405
  });
340
406
  it("should set challenge$ when AUTH message received", async () => {
package/dist/relay.d.ts CHANGED
@@ -30,21 +30,21 @@ export declare class Relay implements IRelay {
30
30
  notices$: BehaviorSubject<string[]>;
31
31
  /** The last connection error */
32
32
  error$: BehaviorSubject<Error | null>;
33
- /** An observable that emits the NIP-11 information document for the relay */
34
- information$: Observable<RelayInformation | null>;
35
- protected _nip11: RelayInformation | null;
36
- /** An observable that emits the limitations for the relay */
37
- limitations$: Observable<RelayInformation["limitation"] | null>;
38
33
  /**
39
- * An observable of all messages from the relay
40
- * @note Subscribing to this will cause the relay to connect
34
+ * A passive observable of all messages from the relay
35
+ * @note Subscribing to this will not connect to the relay
41
36
  */
42
37
  message$: Observable<any>;
43
38
  /**
44
- * An observable of NOTICE messages from the relay
45
- * @note Subscribing to this will cause the relay to connect
39
+ * A passive observable of NOTICE messages from the relay
40
+ * @note Subscribing to this will not connect to the relay
46
41
  */
47
42
  notice$: Observable<string>;
43
+ /** An observable that emits the NIP-11 information document for the relay */
44
+ information$: Observable<RelayInformation | null>;
45
+ protected _nip11: RelayInformation | null;
46
+ /** An observable that emits the limitations for the relay */
47
+ limitations$: Observable<RelayInformation["limitation"] | null>;
48
48
  get connected(): boolean;
49
49
  get challenge(): string | null;
50
50
  get notices(): string[];
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, switchMap, take, tap, throwError, timeout, timer, } from "rxjs";
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";
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";
@@ -29,21 +29,21 @@ export class Relay {
29
29
  notices$ = new BehaviorSubject([]);
30
30
  /** The last connection error */
31
31
  error$ = new BehaviorSubject(null);
32
- /** An observable that emits the NIP-11 information document for the relay */
33
- information$;
34
- _nip11 = null;
35
- /** An observable that emits the limitations for the relay */
36
- limitations$;
37
32
  /**
38
- * An observable of all messages from the relay
39
- * @note Subscribing to this will cause the relay to connect
33
+ * A passive observable of all messages from the relay
34
+ * @note Subscribing to this will not connect to the relay
40
35
  */
41
36
  message$;
42
37
  /**
43
- * An observable of NOTICE messages from the relay
44
- * @note Subscribing to this will cause the relay to connect
38
+ * A passive observable of NOTICE messages from the relay
39
+ * @note Subscribing to this will not connect to the relay
45
40
  */
46
41
  notice$;
42
+ /** An observable that emits the NIP-11 information document for the relay */
43
+ information$;
44
+ _nip11 = null;
45
+ /** An observable that emits the limitations for the relay */
46
+ limitations$;
47
47
  // sync state
48
48
  get connected() {
49
49
  return this.connected$.value;
@@ -116,7 +116,6 @@ export class Relay {
116
116
  },
117
117
  WebSocketCtor: opts?.WebSocket,
118
118
  });
119
- this.message$ = this.socket.asObservable();
120
119
  // Create an observable to fetch the NIP-11 information document
121
120
  this.information$ = defer(() => {
122
121
  this.log("Fetching NIP-11 information document");
@@ -132,19 +131,18 @@ export class Relay {
132
131
  // Create observables that track if auth is required for REQ or EVENT
133
132
  this.authRequiredForReq = combineLatest([this.receivedAuthRequiredForReq, this.limitations$]).pipe(map(([received, limitations]) => received || limitations?.auth_required === true), tap((required) => required && this.log("Auth required for REQ")), shareReplay(1));
134
133
  this.authRequiredForEvent = combineLatest([this.receivedAuthRequiredForEvent, this.limitations$]).pipe(map(([received, limitations]) => received || limitations?.auth_required === true), tap((required) => required && this.log("Auth required for EVENT")), shareReplay(1));
135
- this.notice$ = this.message$.pipe(
134
+ // Update the notices state
135
+ const listenForNotice = this.socket.pipe(
136
136
  // listen for NOTICE messages
137
- filter((m) => m[0] === "NOTICE"),
137
+ filter((m) => Array.isArray(m) && m[0] === "NOTICE"),
138
138
  // pick the string out of the message
139
- map((m) => m[1]));
140
- // Update the notices state
141
- const notice = this.notice$.pipe(
139
+ map((m) => m[1]),
142
140
  // Track all notices
143
141
  scan((acc, notice) => [...acc, notice], []),
144
142
  // Update the notices state
145
143
  tap((notices) => this.notices$.next(notices)));
146
144
  // Update the challenge state
147
- const challenge = this.message$.pipe(
145
+ const ListenForChallenge = this.socket.pipe(
148
146
  // listen for AUTH messages
149
147
  filter((message) => message[0] === "AUTH"),
150
148
  // pick the challenge string out
@@ -154,12 +152,21 @@ export class Relay {
154
152
  this.log("Received AUTH challenge", challenge);
155
153
  this.challenge$.next(challenge);
156
154
  }));
155
+ const allMessagesSubject = new Subject();
156
+ const listenForAllMessages = this.socket.pipe(tap((message) => allMessagesSubject.next(message)));
157
+ // Create passive observables for messages and notices
158
+ this.message$ = allMessagesSubject.asObservable();
159
+ this.notice$ = this.message$.pipe(
160
+ // listen for NOTICE messages
161
+ filter((m) => Array.isArray(m) && m[0] === "NOTICE"),
162
+ // pick the string out of the message
163
+ map((m) => m[1]));
157
164
  // Merge all watchers
158
165
  this.watchTower = this.ready$.pipe(switchMap((ready) => {
159
166
  if (!ready)
160
167
  return NEVER;
161
168
  // Only start the watch tower if the relay is ready
162
- return merge(notice, challenge, this.information$).pipe(
169
+ return merge(listenForAllMessages, listenForNotice, ListenForChallenge, this.information$).pipe(
163
170
  // Never emit any values
164
171
  ignoreElements(),
165
172
  // Start the reconnect timer if the connection has an error
@@ -184,9 +191,7 @@ export class Relay {
184
191
  .subscribe(() => this.ready$.next(true));
185
192
  }
186
193
  /** Wait for ready and authenticated */
187
- waitForAuth(
188
- // NOTE: require BehaviorSubject so it always has a value
189
- requireAuth, observable) {
194
+ waitForAuth(requireAuth, observable) {
190
195
  return combineLatest([requireAuth, this.authenticated$]).pipe(
191
196
  // wait for auth not required or authenticated
192
197
  filter(([required, authenticated]) => !required || authenticated),
@@ -197,13 +202,17 @@ export class Relay {
197
202
  }
198
203
  /** Wait for the relay to be ready to accept connections */
199
204
  waitForReady(observable) {
200
- return this.ready$.pipe(
201
- // wait for ready to be true
202
- filter((ready) => ready),
203
- // complete after the first value so this does not repeat
204
- take(1),
205
- // switch to the observable
206
- switchMap(() => observable));
205
+ // If the relay is ready, don't wait
206
+ if (this.ready$.value)
207
+ return observable;
208
+ else
209
+ return this.ready$.pipe(
210
+ // wait for ready to be true
211
+ filter((ready) => ready),
212
+ // complete after the first value so this does not repeat
213
+ take(1),
214
+ // switch to the observable
215
+ switchMap(() => observable));
207
216
  }
208
217
  multiplex(open, close, filter) {
209
218
  return this.socket.multiplex(open, close, filter);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-relay",
3
- "version": "0.0.0-next-20250430170741",
3
+ "version": "0.0.0-next-20250430195017",
4
4
  "description": "nostr relay communication framework built on rxjs",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",