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 +5 -5
- package/dist/negentropy.d.ts +2 -2
- package/dist/negentropy.js +1 -1
- package/dist/operators/complete-on-eose.js +2 -5
- package/dist/operators/store-events.d.ts +1 -1
- package/dist/operators/store-events.js +1 -1
- package/dist/operators/to-event-store.d.ts +4 -1
- package/dist/operators/to-event-store.js +8 -12
- package/dist/pool.d.ts +5 -5
- package/dist/pool.js +12 -5
- package/dist/relay.d.ts +4 -4
- package/dist/relay.js +25 -11
- package/dist/types.d.ts +11 -9
- package/package.json +5 -4
- package/dist/__tests__/auth.test.d.ts +0 -1
- package/dist/__tests__/auth.test.js +0 -111
- package/dist/__tests__/group.test.d.ts +0 -1
- package/dist/__tests__/group.test.js +0 -106
- package/dist/__tests__/pool.test.d.ts +0 -1
- package/dist/__tests__/pool.test.js +0 -81
- package/dist/__tests__/relay.test.d.ts +0 -1
- package/dist/__tests__/relay.test.js +0 -611
package/dist/group.d.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import {
|
|
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:
|
|
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:
|
|
16
|
+
request(filters: FilterInput, opts?: RequestOptions): Observable<NostrEvent>;
|
|
17
17
|
/** Open a subscription to all relays with retries ( default 3 retries ) */
|
|
18
|
-
subscription(filters:
|
|
18
|
+
subscription(filters: FilterInput, opts?: SubscriptionOptions): Observable<SubscriptionResponse>;
|
|
19
19
|
}
|
package/dist/negentropy.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
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:
|
|
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;
|
package/dist/negentropy.js
CHANGED
|
@@ -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.
|
|
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(
|
|
3
|
-
|
|
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
|
-
/**
|
|
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 {
|
|
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
|
-
/**
|
|
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
|
-
|
|
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 {
|
|
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:
|
|
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:
|
|
27
|
+
request(relays: string[], filters: FilterInput, opts?: RequestOptions): Observable<NostrEvent>;
|
|
28
28
|
/** Open a subscription to multiple relays */
|
|
29
|
-
subscription(relays: string[], filters:
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const
|
|
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
|
|
274
|
-
const
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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": "
|
|
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": "^
|
|
57
|
+
"applesauce-core": "^2.0.0",
|
|
58
58
|
"nanoid": "^5.0.9",
|
|
59
|
-
"nostr-tools": "^2.
|
|
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.
|
|
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 {};
|