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.
- package/dist/__tests__/relay.test.js +70 -4
- package/dist/relay.d.ts +9 -9
- package/dist/relay.js +37 -28
- package/package.json +1 -1
|
@@ -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
|
|
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")
|
|
58
|
+
const sub = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
|
|
53
59
|
// Verify REQ was sent
|
|
54
|
-
expect(
|
|
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(
|
|
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
|
-
*
|
|
40
|
-
* @note Subscribing to this will
|
|
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
|
-
*
|
|
45
|
-
* @note Subscribing to this will
|
|
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
|
-
*
|
|
39
|
-
* @note Subscribing to this will
|
|
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
|
-
*
|
|
44
|
-
* @note Subscribing to this will
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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);
|