applesauce-relay 2.3.0 → 3.1.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/README.md +6 -5
- package/dist/group.d.ts +2 -2
- package/dist/group.js +2 -2
- package/dist/pool.d.ts +4 -2
- package/dist/pool.js +20 -1
- package/dist/relay.d.ts +28 -9
- package/dist/relay.js +102 -42
- package/dist/types.d.ts +67 -62
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -56,9 +56,8 @@ const event = {
|
|
|
56
56
|
// ... other required fields
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
-
relay.
|
|
60
|
-
|
|
61
|
-
});
|
|
59
|
+
const response = await relay.publish(event);
|
|
60
|
+
console.log(`Published:`, response.ok);
|
|
62
61
|
```
|
|
63
62
|
|
|
64
63
|
### Relay Pool
|
|
@@ -85,7 +84,8 @@ pool
|
|
|
85
84
|
});
|
|
86
85
|
|
|
87
86
|
// Publish to multiple relays
|
|
88
|
-
pool.
|
|
87
|
+
const responses = await pool.publish(relays, event);
|
|
88
|
+
responses.forEach((response) => {
|
|
89
89
|
console.log(`Published to ${response.from}:`, response.ok);
|
|
90
90
|
});
|
|
91
91
|
```
|
|
@@ -112,7 +112,8 @@ group
|
|
|
112
112
|
});
|
|
113
113
|
|
|
114
114
|
// Publish to all relays in group
|
|
115
|
-
group.
|
|
115
|
+
const responses = await group.publish(event);
|
|
116
|
+
responses.forEach((response) => {
|
|
116
117
|
console.log(`Published to ${response.from}:`, response.ok);
|
|
117
118
|
});
|
|
118
119
|
```
|
package/dist/group.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type NostrEvent } from "nostr-tools";
|
|
2
2
|
import { Observable } from "rxjs";
|
|
3
|
-
import { IGroup, IRelay,
|
|
3
|
+
import { FilterInput, IGroup, IRelay, PublishOptions, PublishResponse, RequestOptions, SubscriptionOptions, SubscriptionResponse } from "./types.js";
|
|
4
4
|
export declare class RelayGroup implements IGroup {
|
|
5
5
|
relays: IRelay[];
|
|
6
6
|
constructor(relays: IRelay[]);
|
|
@@ -11,7 +11,7 @@ export declare class RelayGroup implements IGroup {
|
|
|
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
|
-
publish(event: NostrEvent, opts?: PublishOptions):
|
|
14
|
+
publish(event: NostrEvent, opts?: PublishOptions): Promise<PublishResponse[]>;
|
|
15
15
|
/** Request events from all relays with retries ( default 3 retries ) */
|
|
16
16
|
request(filters: FilterInput, opts?: RequestOptions): Observable<NostrEvent>;
|
|
17
17
|
/** Open a subscription to all relays with retries ( default 3 retries ) */
|
package/dist/group.js
CHANGED
|
@@ -35,9 +35,9 @@ export class RelayGroup {
|
|
|
35
35
|
}
|
|
36
36
|
/** Publish an event to all relays with retries ( default 3 retries ) */
|
|
37
37
|
publish(event, opts) {
|
|
38
|
-
return
|
|
38
|
+
return Promise.all(this.relays.map((relay) => relay.publish(event, opts).catch(
|
|
39
39
|
// Catch error and return as PublishResponse
|
|
40
|
-
|
|
40
|
+
(err) => ({ ok: false, from: relay.url, message: err?.message || "Unknown error" }))));
|
|
41
41
|
}
|
|
42
42
|
/** Request events from all relays with retries ( default 3 retries ) */
|
|
43
43
|
request(filters, opts) {
|
package/dist/pool.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ 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, FilterInput } from "./types.js";
|
|
5
|
+
import { IPool, PublishResponse, PublishOptions, RequestOptions, SubscriptionOptions, SubscriptionResponse, FilterInput, IRelay } from "./types.js";
|
|
6
6
|
export declare class RelayPool implements IPool {
|
|
7
7
|
options?: RelayOptions | undefined;
|
|
8
8
|
groups$: BehaviorSubject<Map<string, RelayGroup>>;
|
|
@@ -17,12 +17,14 @@ export declare class RelayPool implements IPool {
|
|
|
17
17
|
relay(url: string): Relay;
|
|
18
18
|
/** Create a group of relays */
|
|
19
19
|
group(relays: string[]): RelayGroup;
|
|
20
|
+
/** Removes a relay from the pool and defaults to closing the connection */
|
|
21
|
+
remove(relay: string | IRelay, close?: boolean): void;
|
|
20
22
|
/** Make a REQ to multiple relays that does not deduplicate events */
|
|
21
23
|
req(relays: string[], filters: FilterInput, id?: string): Observable<SubscriptionResponse>;
|
|
22
24
|
/** Send an EVENT message to multiple relays */
|
|
23
25
|
event(relays: string[], event: NostrEvent): Observable<PublishResponse>;
|
|
24
26
|
/** Publish an event to multiple relays */
|
|
25
|
-
publish(relays: string[], event: NostrEvent, opts?: PublishOptions):
|
|
27
|
+
publish(relays: string[], event: NostrEvent, opts?: PublishOptions): Promise<PublishResponse[]>;
|
|
26
28
|
/** Request events from multiple relays */
|
|
27
29
|
request(relays: string[], filters: FilterInput, opts?: RequestOptions): Observable<NostrEvent>;
|
|
28
30
|
/** Open a subscription to multiple relays */
|
package/dist/pool.js
CHANGED
|
@@ -33,7 +33,8 @@ export class RelayPool {
|
|
|
33
33
|
return relay;
|
|
34
34
|
// Create a new relay
|
|
35
35
|
relay = new Relay(url, this.options);
|
|
36
|
-
this.relays
|
|
36
|
+
this.relays.set(url, relay);
|
|
37
|
+
this.relays$.next(this.relays);
|
|
37
38
|
return relay;
|
|
38
39
|
}
|
|
39
40
|
/** Create a group of relays */
|
|
@@ -50,6 +51,24 @@ export class RelayPool {
|
|
|
50
51
|
this.groups$.next(this.groups.set(key, group));
|
|
51
52
|
return group;
|
|
52
53
|
}
|
|
54
|
+
/** Removes a relay from the pool and defaults to closing the connection */
|
|
55
|
+
remove(relay, close = true) {
|
|
56
|
+
let instance;
|
|
57
|
+
if (typeof relay === "string") {
|
|
58
|
+
instance = this.relays.get(relay);
|
|
59
|
+
if (!instance)
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
else if (Array.from(this.relays.values()).some((r) => r === relay)) {
|
|
63
|
+
instance = relay;
|
|
64
|
+
}
|
|
65
|
+
else
|
|
66
|
+
return;
|
|
67
|
+
if (close)
|
|
68
|
+
instance?.close();
|
|
69
|
+
this.relays.delete(instance.url);
|
|
70
|
+
this.relays$.next(this.relays);
|
|
71
|
+
}
|
|
53
72
|
/** Make a REQ to multiple relays that does not deduplicate events */
|
|
54
73
|
req(relays, filters, id) {
|
|
55
74
|
return this.group(relays).req(filters, id);
|
package/dist/relay.d.ts
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import { logger } from "applesauce-core";
|
|
2
2
|
import { type Filter, type NostrEvent } from "nostr-tools";
|
|
3
|
-
import { BehaviorSubject, Observable } from "rxjs";
|
|
4
|
-
import { WebSocketSubject, WebSocketSubjectConfig } from "rxjs/webSocket";
|
|
5
3
|
import { RelayInformation } from "nostr-tools/nip11";
|
|
4
|
+
import { BehaviorSubject, MonoTypeOperatorFunction, Observable, RepeatConfig, RetryConfig, Subject } from "rxjs";
|
|
5
|
+
import { WebSocketSubject, WebSocketSubjectConfig } from "rxjs/webSocket";
|
|
6
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
|
}
|
|
10
10
|
export type RelayOptions = {
|
|
11
|
+
/** Custom WebSocket implementation */
|
|
11
12
|
WebSocket?: WebSocketSubjectConfig<any>["WebSocketCtor"];
|
|
13
|
+
/** How long to wait for an EOSE message (default 10s) */
|
|
14
|
+
eoseTimeout?: number;
|
|
15
|
+
/** How long to wait for an OK message from the relay (default 10s) */
|
|
16
|
+
eventTimeout?: number;
|
|
17
|
+
/** How long to keep the connection alive after nothing is subscribed (default 30s) */
|
|
18
|
+
keepAlive?: number;
|
|
12
19
|
};
|
|
13
20
|
export declare class Relay implements IRelay {
|
|
14
21
|
url: string;
|
|
@@ -47,17 +54,23 @@ export declare class Relay implements IRelay {
|
|
|
47
54
|
protected _nip11: RelayInformation | null;
|
|
48
55
|
/** An observable that emits the limitations for the relay */
|
|
49
56
|
limitations$: Observable<RelayInformation["limitation"] | null>;
|
|
57
|
+
/** An observable that emits when underlying websocket is opened */
|
|
58
|
+
open$: Subject<Event>;
|
|
59
|
+
/** An observable that emits when underlying websocket is closed */
|
|
60
|
+
close$: Subject<CloseEvent>;
|
|
61
|
+
/** An observable that emits when underlying websocket is closing due to unsubscription */
|
|
62
|
+
closing$: Subject<void>;
|
|
50
63
|
get connected(): boolean;
|
|
51
64
|
get challenge(): string | null;
|
|
52
65
|
get notices(): string[];
|
|
53
66
|
get authenticated(): boolean;
|
|
54
67
|
get authenticationResponse(): PublishResponse | null;
|
|
55
68
|
get information(): RelayInformation | null;
|
|
56
|
-
/** If an EOSE message is not seen in this time, emit one locally */
|
|
69
|
+
/** If an EOSE message is not seen in this time, emit one locally (default 10s) */
|
|
57
70
|
eoseTimeout: number;
|
|
58
|
-
/** How long to wait for an OK message from the relay */
|
|
71
|
+
/** How long to wait for an OK message from the relay (default 10s) */
|
|
59
72
|
eventTimeout: number;
|
|
60
|
-
/** How long to keep the connection alive after nothing is subscribed */
|
|
73
|
+
/** How long to keep the connection alive after nothing is subscribed (default 30s) */
|
|
61
74
|
keepAlive: number;
|
|
62
75
|
protected receivedAuthRequiredForReq: BehaviorSubject<boolean>;
|
|
63
76
|
protected receivedAuthRequiredForEvent: BehaviorSubject<boolean>;
|
|
@@ -75,21 +88,27 @@ export declare class Relay implements IRelay {
|
|
|
75
88
|
protected waitForReady<T extends unknown = unknown>(observable: Observable<T>): Observable<T>;
|
|
76
89
|
multiplex<T>(open: () => any, close: () => any, filter: (message: any) => boolean): Observable<T>;
|
|
77
90
|
/** Send a message to the relay */
|
|
78
|
-
|
|
91
|
+
send(message: any): void;
|
|
79
92
|
/** Create a REQ observable that emits events or "EOSE" or errors */
|
|
80
93
|
req(filters: FilterInput, id?: string): Observable<SubscriptionResponse>;
|
|
81
94
|
/** Send an EVENT or AUTH message and return an observable of PublishResponse that completes or errors */
|
|
82
95
|
event(event: NostrEvent, verb?: "EVENT" | "AUTH"): Observable<PublishResponse>;
|
|
83
96
|
/** send and AUTH message */
|
|
84
|
-
auth(event: NostrEvent):
|
|
97
|
+
auth(event: NostrEvent): Promise<PublishResponse>;
|
|
85
98
|
/** Authenticate with the relay using a signer */
|
|
86
|
-
authenticate(signer: AuthSigner):
|
|
99
|
+
authenticate(signer: AuthSigner): Promise<PublishResponse>;
|
|
100
|
+
/** Internal operator for creating the retry() operator */
|
|
101
|
+
protected customRetryOperator<T extends unknown = unknown>(times: undefined | boolean | number | RetryConfig, base?: RetryConfig): MonoTypeOperatorFunction<T>;
|
|
102
|
+
/** Internal operator for creating the repeat() operator */
|
|
103
|
+
protected customRepeatOperator<T extends unknown = unknown>(times: undefined | boolean | number | RepeatConfig | undefined): MonoTypeOperatorFunction<T>;
|
|
87
104
|
/** Creates a REQ that retries when relay errors ( default 3 retries ) */
|
|
88
105
|
subscription(filters: Filter | Filter[], opts?: SubscriptionOptions): Observable<SubscriptionResponse>;
|
|
89
106
|
/** Makes a single request that retires on errors and completes on EOSE */
|
|
90
107
|
request(filters: Filter | Filter[], opts?: RequestOptions): Observable<NostrEvent>;
|
|
91
108
|
/** Publishes an event to the relay and retries when relay errors or responds with auth-required ( default 3 retries ) */
|
|
92
|
-
publish(event: NostrEvent, opts?: PublishOptions):
|
|
109
|
+
publish(event: NostrEvent, opts?: PublishOptions): Promise<PublishResponse>;
|
|
110
|
+
/** Force close the connection */
|
|
111
|
+
close(): void;
|
|
93
112
|
/** Static method to fetch the NIP-11 information document for a relay */
|
|
94
113
|
static fetchInformationDocument(url: string): Observable<RelayInformation | null>;
|
|
95
114
|
/** Static method to create a reconnection method for each relay */
|
package/dist/relay.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { logger } from "applesauce-core";
|
|
2
|
+
import { ensureHttpURL } from "applesauce-core/helpers";
|
|
2
3
|
import { simpleTimeout } from "applesauce-core/observable";
|
|
3
4
|
import { nanoid } from "nanoid";
|
|
4
5
|
import { nip42 } from "nostr-tools";
|
|
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
|
+
import { BehaviorSubject, catchError, combineLatest, defer, endWith, filter, finalize, from, identity, ignoreElements, isObservable, lastValueFrom, map, merge, mergeMap, mergeWith, NEVER, of, repeat, retry, scan, share, shareReplay, Subject, switchMap, take, takeUntil, tap, throwError, timeout, timer, } from "rxjs";
|
|
6
7
|
import { webSocket } from "rxjs/webSocket";
|
|
7
|
-
import { ensureHttpURL } from "applesauce-core/helpers";
|
|
8
8
|
import { completeOnEose } from "./operators/complete-on-eose.js";
|
|
9
9
|
import { markFromRelay } from "./operators/mark-from-relay.js";
|
|
10
|
+
const DEFAULT_RETRY_CONFIG = { count: 10, delay: 1000, resetOnSuccess: true };
|
|
10
11
|
/** An error that is thrown when a REQ is closed from the relay side */
|
|
11
12
|
export class ReqCloseError extends Error {
|
|
12
13
|
}
|
|
@@ -47,6 +48,12 @@ export class Relay {
|
|
|
47
48
|
_nip11 = null;
|
|
48
49
|
/** An observable that emits the limitations for the relay */
|
|
49
50
|
limitations$;
|
|
51
|
+
/** An observable that emits when underlying websocket is opened */
|
|
52
|
+
open$ = new Subject();
|
|
53
|
+
/** An observable that emits when underlying websocket is closed */
|
|
54
|
+
close$ = new Subject();
|
|
55
|
+
/** An observable that emits when underlying websocket is closing due to unsubscription */
|
|
56
|
+
closing$ = new Subject();
|
|
50
57
|
// sync state
|
|
51
58
|
get connected() {
|
|
52
59
|
return this.connected$.value;
|
|
@@ -66,11 +73,11 @@ export class Relay {
|
|
|
66
73
|
get information() {
|
|
67
74
|
return this._nip11;
|
|
68
75
|
}
|
|
69
|
-
/** If an EOSE message is not seen in this time, emit one locally */
|
|
76
|
+
/** If an EOSE message is not seen in this time, emit one locally (default 10s) */
|
|
70
77
|
eoseTimeout = 10_000;
|
|
71
|
-
/** How long to wait for an OK message from the relay */
|
|
78
|
+
/** How long to wait for an OK message from the relay (default 10s) */
|
|
72
79
|
eventTimeout = 10_000;
|
|
73
|
-
/** How long to keep the connection alive after nothing is subscribed */
|
|
80
|
+
/** How long to keep the connection alive after nothing is subscribed (default 30s) */
|
|
74
81
|
keepAlive = 30_000;
|
|
75
82
|
// Subjects that track if an "auth-required" message has been received for REQ or EVENT
|
|
76
83
|
receivedAuthRequiredForReq = new BehaviorSubject(false);
|
|
@@ -96,32 +103,39 @@ export class Relay {
|
|
|
96
103
|
constructor(url, opts) {
|
|
97
104
|
this.url = url;
|
|
98
105
|
this.log = this.log.extend(url);
|
|
106
|
+
// Set common options
|
|
107
|
+
if (opts?.eoseTimeout !== undefined)
|
|
108
|
+
this.eoseTimeout = opts.eoseTimeout;
|
|
109
|
+
if (opts?.eventTimeout !== undefined)
|
|
110
|
+
this.eventTimeout = opts.eventTimeout;
|
|
111
|
+
if (opts?.keepAlive !== undefined)
|
|
112
|
+
this.keepAlive = opts.keepAlive;
|
|
99
113
|
// Create an observable that tracks boolean authentication state
|
|
100
114
|
this.authenticated$ = this.authenticationResponse$.pipe(map((response) => response?.ok === true));
|
|
101
115
|
/** Use the static method to create a new reconnect method for this relay */
|
|
102
116
|
this.reconnectTimer = Relay.createReconnectTimer(url);
|
|
117
|
+
// Subscribe to open and close events
|
|
118
|
+
this.open$.subscribe(() => {
|
|
119
|
+
this.log("Connected");
|
|
120
|
+
this.connected$.next(true);
|
|
121
|
+
this.attempts$.next(0);
|
|
122
|
+
this.error$.next(null);
|
|
123
|
+
this.resetState();
|
|
124
|
+
});
|
|
125
|
+
this.close$.subscribe((event) => {
|
|
126
|
+
this.log("Disconnected");
|
|
127
|
+
this.connected$.next(false);
|
|
128
|
+
this.attempts$.next(this.attempts$.value + 1);
|
|
129
|
+
this.resetState();
|
|
130
|
+
// Start the reconnect timer if the connection was not closed cleanly
|
|
131
|
+
if (!event.wasClean)
|
|
132
|
+
this.startReconnectTimer(event);
|
|
133
|
+
});
|
|
103
134
|
this.socket = webSocket({
|
|
104
135
|
url,
|
|
105
|
-
openObserver:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
this.connected$.next(true);
|
|
109
|
-
this.attempts$.next(0);
|
|
110
|
-
this.error$.next(null);
|
|
111
|
-
this.resetState();
|
|
112
|
-
},
|
|
113
|
-
},
|
|
114
|
-
closeObserver: {
|
|
115
|
-
next: (event) => {
|
|
116
|
-
this.log("Disconnected");
|
|
117
|
-
this.connected$.next(false);
|
|
118
|
-
this.attempts$.next(this.attempts$.value + 1);
|
|
119
|
-
this.resetState();
|
|
120
|
-
// Start the reconnect timer if the connection was not closed cleanly
|
|
121
|
-
if (!event.wasClean)
|
|
122
|
-
this.startReconnectTimer(event);
|
|
123
|
-
},
|
|
124
|
-
},
|
|
136
|
+
openObserver: this.open$,
|
|
137
|
+
closeObserver: this.close$,
|
|
138
|
+
closingObserver: this.closing$,
|
|
125
139
|
WebSocketCtor: opts?.WebSocket,
|
|
126
140
|
});
|
|
127
141
|
// Create an observable to fetch the NIP-11 information document
|
|
@@ -200,7 +214,7 @@ export class Relay {
|
|
|
200
214
|
}
|
|
201
215
|
/** Wait for authentication state, make connection and then wait for authentication if required */
|
|
202
216
|
waitForAuth(
|
|
203
|
-
// NOTE: require BehaviorSubject so it always has a value
|
|
217
|
+
// NOTE: require BehaviorSubject or shareReplay so it always has a value
|
|
204
218
|
requireAuth, observable) {
|
|
205
219
|
return combineLatest([requireAuth, this.authenticated$]).pipe(
|
|
206
220
|
// Once the auth state is known, make a connection and watch for auth challenges
|
|
@@ -230,7 +244,7 @@ export class Relay {
|
|
|
230
244
|
return this.socket.multiplex(open, close, filter);
|
|
231
245
|
}
|
|
232
246
|
/** Send a message to the relay */
|
|
233
|
-
|
|
247
|
+
send(message) {
|
|
234
248
|
this.socket.next(message);
|
|
235
249
|
}
|
|
236
250
|
/** Create a REQ observable that emits events or "EOSE" or errors */
|
|
@@ -238,21 +252,26 @@ export class Relay {
|
|
|
238
252
|
// Convert filters input into an observable, if its a normal value merge it with NEVER so it never completes
|
|
239
253
|
const input = isObservable(filters) ? filters : merge(of(filters), NEVER);
|
|
240
254
|
// Create an observable that completes when the upstream observable completes
|
|
241
|
-
const
|
|
255
|
+
const filtersComplete = input.pipe(ignoreElements(), endWith(null));
|
|
242
256
|
// Create an observable that filters responses from the relay to just the ones for this REQ
|
|
243
|
-
const messages = this.socket.pipe(filter((m) => Array.isArray(m) && (m[0] === "EVENT" || m[0] === "CLOSED" || m[0] === "EOSE") && m[1] === id)
|
|
257
|
+
const messages = this.socket.pipe(filter((m) => Array.isArray(m) && (m[0] === "EVENT" || m[0] === "CLOSED" || m[0] === "EOSE") && m[1] === id),
|
|
258
|
+
// Singleton (prevents the .pipe() operator later from sending two REQ messages )
|
|
259
|
+
share());
|
|
244
260
|
// Create an observable that controls sending the filters and closing the REQ
|
|
245
261
|
const control = input.pipe(
|
|
246
262
|
// Send the filters when they change
|
|
247
263
|
tap((filters) => this.socket.next(Array.isArray(filters) ? ["REQ", id, ...filters] : ["REQ", id, filters])),
|
|
248
|
-
//
|
|
264
|
+
// Send the CLOSE message when unsubscribed or input completes
|
|
249
265
|
finalize(() => this.socket.next(["CLOSE", id])),
|
|
250
266
|
// Once filters have been sent, switch to listening for messages
|
|
251
267
|
switchMap(() => messages));
|
|
252
268
|
// Start the watch tower with the observables
|
|
253
269
|
const observable = merge(this.watchTower, control).pipe(
|
|
270
|
+
// Complete the subscription when the control observable completes
|
|
271
|
+
// This is to work around the fact that merge() waits for both observables to complete
|
|
272
|
+
takeUntil(messages.pipe(ignoreElements(), endWith(true))),
|
|
254
273
|
// Complete the subscription when the input is completed
|
|
255
|
-
takeUntil(
|
|
274
|
+
takeUntil(filtersComplete),
|
|
256
275
|
// Map the messages to events, EOSE, or throw an error
|
|
257
276
|
map((message) => {
|
|
258
277
|
if (message[0] === "EOSE")
|
|
@@ -287,15 +306,20 @@ export class Relay {
|
|
|
287
306
|
}
|
|
288
307
|
/** Send an EVENT or AUTH message and return an observable of PublishResponse that completes or errors */
|
|
289
308
|
event(event, verb = "EVENT") {
|
|
290
|
-
const
|
|
309
|
+
const messages = defer(() => {
|
|
291
310
|
// Send event when subscription starts
|
|
292
311
|
this.socket.next([verb, event]);
|
|
293
312
|
return this.socket.pipe(filter((m) => m[0] === "OK" && m[1] === event.id),
|
|
294
313
|
// format OK message
|
|
295
314
|
map((m) => ({ ok: m[2], message: m[3], from: this.url })));
|
|
296
|
-
})
|
|
315
|
+
}).pipe(
|
|
316
|
+
// Singleton (prevents the .pipe() operator later from sending two EVENT messages )
|
|
317
|
+
share());
|
|
297
318
|
// Start the watch tower and add complete operators
|
|
298
|
-
const observable = merge(this.watchTower,
|
|
319
|
+
const observable = merge(this.watchTower, messages).pipe(
|
|
320
|
+
// Complete the subscription when the messages observable completes
|
|
321
|
+
// This is to work around the fact that merge() waits for both observables to complete
|
|
322
|
+
takeUntil(messages.pipe(ignoreElements(), endWith(true))),
|
|
299
323
|
// complete on first value
|
|
300
324
|
take(1),
|
|
301
325
|
// listen for OK auth-required
|
|
@@ -320,9 +344,9 @@ export class Relay {
|
|
|
320
344
|
}
|
|
321
345
|
/** send and AUTH message */
|
|
322
346
|
auth(event) {
|
|
323
|
-
return this.event(event, "AUTH").pipe(
|
|
347
|
+
return lastValueFrom(this.event(event, "AUTH").pipe(
|
|
324
348
|
// update authenticated
|
|
325
|
-
tap((result) => this.authenticationResponse$.next(result)));
|
|
349
|
+
tap((result) => this.authenticationResponse$.next(result))));
|
|
326
350
|
}
|
|
327
351
|
/** Authenticate with the relay using a signer */
|
|
328
352
|
authenticate(signer) {
|
|
@@ -330,32 +354,68 @@ export class Relay {
|
|
|
330
354
|
throw new Error("Have not received authentication challenge");
|
|
331
355
|
const p = signer.signEvent(nip42.makeAuthEvent(this.url, this.challenge));
|
|
332
356
|
const start = p instanceof Promise ? from(p) : of(p);
|
|
333
|
-
return start.pipe(switchMap((event) => this.auth(event)));
|
|
357
|
+
return lastValueFrom(start.pipe(switchMap((event) => this.auth(event))));
|
|
358
|
+
}
|
|
359
|
+
/** Internal operator for creating the retry() operator */
|
|
360
|
+
customRetryOperator(times, base) {
|
|
361
|
+
if (times === false)
|
|
362
|
+
return identity;
|
|
363
|
+
else if (typeof times === "number")
|
|
364
|
+
return retry({ ...base, count: times });
|
|
365
|
+
else if (times === true)
|
|
366
|
+
return base ? retry(base) : retry();
|
|
367
|
+
else
|
|
368
|
+
return retry({ ...base, ...times });
|
|
369
|
+
}
|
|
370
|
+
/** Internal operator for creating the repeat() operator */
|
|
371
|
+
customRepeatOperator(times) {
|
|
372
|
+
if (times === false || times === undefined)
|
|
373
|
+
return identity;
|
|
374
|
+
else if (times === true)
|
|
375
|
+
return repeat();
|
|
376
|
+
else if (typeof times === "number")
|
|
377
|
+
return repeat(times);
|
|
378
|
+
else
|
|
379
|
+
return repeat(times);
|
|
334
380
|
}
|
|
335
381
|
/** Creates a REQ that retries when relay errors ( default 3 retries ) */
|
|
336
382
|
subscription(filters, opts) {
|
|
337
383
|
return this.req(filters, opts?.id).pipe(
|
|
338
384
|
// Retry on connection errors
|
|
339
|
-
|
|
385
|
+
this.customRetryOperator(opts?.retries ?? opts?.reconnect ?? true, DEFAULT_RETRY_CONFIG),
|
|
386
|
+
// Create resubscribe logic (repeat operator)
|
|
387
|
+
this.customRepeatOperator(opts?.resubscribe),
|
|
388
|
+
// Single subscription
|
|
389
|
+
share());
|
|
340
390
|
}
|
|
341
391
|
/** Makes a single request that retires on errors and completes on EOSE */
|
|
342
392
|
request(filters, opts) {
|
|
343
393
|
return this.req(filters, opts?.id).pipe(
|
|
344
394
|
// Retry on connection errors
|
|
345
|
-
|
|
395
|
+
this.customRetryOperator(opts?.retries ?? opts?.reconnect ?? true, DEFAULT_RETRY_CONFIG),
|
|
396
|
+
// Create resubscribe logic (repeat operator)
|
|
397
|
+
this.customRepeatOperator(opts?.resubscribe),
|
|
346
398
|
// Complete when EOSE is received
|
|
347
|
-
completeOnEose()
|
|
399
|
+
completeOnEose(),
|
|
400
|
+
// Single subscription
|
|
401
|
+
share());
|
|
348
402
|
}
|
|
349
403
|
/** Publishes an event to the relay and retries when relay errors or responds with auth-required ( default 3 retries ) */
|
|
350
404
|
publish(event, opts) {
|
|
351
|
-
return this.event(event).pipe(mergeMap((result) => {
|
|
405
|
+
return lastValueFrom(this.event(event).pipe(mergeMap((result) => {
|
|
352
406
|
// If the relay responds with auth-required, throw an error for the retry operator to handle
|
|
353
407
|
if (result.ok === false && result.message?.startsWith("auth-required:"))
|
|
354
408
|
return throwError(() => new Error(result.message));
|
|
355
409
|
return of(result);
|
|
356
410
|
}),
|
|
357
411
|
// Retry the publish until it succeeds or the number of retries is reached
|
|
358
|
-
|
|
412
|
+
this.customRetryOperator(opts?.retries ?? opts?.reconnect ?? true, DEFAULT_RETRY_CONFIG),
|
|
413
|
+
// Single subscription
|
|
414
|
+
share()));
|
|
415
|
+
}
|
|
416
|
+
/** Force close the connection */
|
|
417
|
+
close() {
|
|
418
|
+
this.socket.unsubscribe();
|
|
359
419
|
}
|
|
360
420
|
/** Static method to fetch the NIP-11 information document for a relay */
|
|
361
421
|
static fetchInformationDocument(url) {
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type EventTemplate, type Filter, type NostrEvent } from "nostr-tools";
|
|
2
|
-
import { Observable } from "rxjs";
|
|
2
|
+
import { Observable, repeat, retry } from "rxjs";
|
|
3
3
|
import { WebSocketSubject } from "rxjs/webSocket";
|
|
4
4
|
export type SubscriptionResponse = NostrEvent | "EOSE";
|
|
5
5
|
export type PublishResponse = {
|
|
@@ -8,99 +8,104 @@ export type PublishResponse = {
|
|
|
8
8
|
from: string;
|
|
9
9
|
};
|
|
10
10
|
export type MultiplexWebSocket<T = any> = Pick<WebSocketSubject<T>, "multiplex">;
|
|
11
|
-
|
|
12
|
-
connected$: Observable<boolean>;
|
|
13
|
-
challenge$: Observable<string | null>;
|
|
14
|
-
authenticated$: Observable<boolean>;
|
|
15
|
-
notices$: Observable<string[]>;
|
|
16
|
-
}
|
|
11
|
+
/** Options for the publish method on the pool and relay */
|
|
17
12
|
export type PublishOptions = {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Number of times to retry the publish. default is 10
|
|
15
|
+
* @see https://rxjs.dev/api/index/function/retry
|
|
16
|
+
* @deprecated use `reconnect` instead
|
|
17
|
+
*/
|
|
18
|
+
retries?: number | Parameters<typeof retry>[0];
|
|
19
|
+
/**
|
|
20
|
+
* Whether to reconnect when socket fails to connect. default is true (10 retries with 1 second delay)
|
|
21
|
+
* @see https://rxjs.dev/api/index/function/retry
|
|
22
|
+
*/
|
|
23
|
+
reconnect?: boolean | number | Parameters<typeof retry>[0];
|
|
23
24
|
};
|
|
25
|
+
/** Options for the request method on the pool and relay */
|
|
26
|
+
export type RequestOptions = SubscriptionOptions;
|
|
27
|
+
/** Options for the subscription method on the pool and relay */
|
|
24
28
|
export type SubscriptionOptions = {
|
|
29
|
+
/** Custom REQ id for the subscription */
|
|
25
30
|
id?: string;
|
|
26
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Number of times to retry the subscription if the relay fails to connect. default is 10
|
|
33
|
+
* @see https://rxjs.dev/api/index/function/retry
|
|
34
|
+
* @deprecated use `reconnect` instead
|
|
35
|
+
*/
|
|
36
|
+
retries?: number | Parameters<typeof retry>[0];
|
|
37
|
+
/**
|
|
38
|
+
* Whether to resubscribe if the subscription is closed by the relay. default is false
|
|
39
|
+
* @see https://rxjs.dev/api/index/function/repeat
|
|
40
|
+
*/
|
|
41
|
+
resubscribe?: boolean | number | Parameters<typeof repeat>[0];
|
|
42
|
+
/**
|
|
43
|
+
* Whether to reconnect when socket is closed. default is true (10 retries with 1 second delay)
|
|
44
|
+
* @see https://rxjs.dev/api/index/function/retry
|
|
45
|
+
*/
|
|
46
|
+
reconnect?: boolean | number | Parameters<typeof retry>[0];
|
|
27
47
|
};
|
|
28
48
|
export type AuthSigner = {
|
|
29
49
|
signEvent: (event: EventTemplate) => NostrEvent | Promise<NostrEvent>;
|
|
30
50
|
};
|
|
31
51
|
/** The type of input the REQ method accepts */
|
|
32
52
|
export type FilterInput = Filter | Filter[] | Observable<Filter | Filter[]>;
|
|
33
|
-
export interface
|
|
34
|
-
/** Send an EVENT message */
|
|
35
|
-
event(event: NostrEvent): Observable<PublishResponse>;
|
|
36
|
-
/** Send a REQ message */
|
|
37
|
-
req(filters: FilterInput, id?: string): Observable<SubscriptionResponse>;
|
|
38
|
-
}
|
|
39
|
-
export interface IRelay extends MultiplexWebSocket, Nip01Actions, IRelayState {
|
|
53
|
+
export interface IRelay extends MultiplexWebSocket {
|
|
40
54
|
url: string;
|
|
41
55
|
message$: Observable<any>;
|
|
42
56
|
notice$: Observable<string>;
|
|
57
|
+
connected$: Observable<boolean>;
|
|
58
|
+
challenge$: Observable<string | null>;
|
|
59
|
+
authenticated$: Observable<boolean>;
|
|
60
|
+
notices$: Observable<string[]>;
|
|
43
61
|
readonly connected: boolean;
|
|
44
62
|
readonly authenticated: boolean;
|
|
45
63
|
readonly challenge: string | null;
|
|
46
64
|
readonly notices: string[];
|
|
65
|
+
/** Force close the connection */
|
|
66
|
+
close(): void;
|
|
67
|
+
/** Send a REQ message */
|
|
68
|
+
req(filters: FilterInput, id?: string): Observable<SubscriptionResponse>;
|
|
69
|
+
/** Send an EVENT message */
|
|
70
|
+
event(event: NostrEvent): Observable<PublishResponse>;
|
|
47
71
|
/** Send an AUTH message */
|
|
48
|
-
auth(event: NostrEvent):
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}>;
|
|
72
|
+
auth(event: NostrEvent): Promise<PublishResponse>;
|
|
73
|
+
/** Authenticate with the relay using a signer */
|
|
74
|
+
authenticate(signer: AuthSigner): Promise<PublishResponse>;
|
|
52
75
|
/** Send an EVENT message with retries */
|
|
53
|
-
publish(event: NostrEvent, opts?:
|
|
54
|
-
retries?: number;
|
|
55
|
-
}): Observable<PublishResponse>;
|
|
76
|
+
publish(event: NostrEvent, opts?: PublishOptions): Promise<PublishResponse>;
|
|
56
77
|
/** Send a REQ message with retries */
|
|
57
|
-
request(filters: FilterInput, opts?:
|
|
58
|
-
id?: string;
|
|
59
|
-
retries?: number;
|
|
60
|
-
}): Observable<NostrEvent>;
|
|
78
|
+
request(filters: FilterInput, opts?: RequestOptions): Observable<NostrEvent>;
|
|
61
79
|
/** Open a subscription with retries */
|
|
62
|
-
subscription(filters: FilterInput, opts?:
|
|
63
|
-
id?: string;
|
|
64
|
-
retries?: number;
|
|
65
|
-
}): Observable<SubscriptionResponse>;
|
|
80
|
+
subscription(filters: FilterInput, opts?: SubscriptionOptions): Observable<SubscriptionResponse>;
|
|
66
81
|
}
|
|
67
|
-
export interface IGroup
|
|
82
|
+
export interface IGroup {
|
|
83
|
+
/** Send a REQ message */
|
|
84
|
+
req(filters: FilterInput, id?: string): Observable<SubscriptionResponse>;
|
|
85
|
+
/** Send an EVENT message */
|
|
86
|
+
event(event: NostrEvent): Observable<PublishResponse>;
|
|
68
87
|
/** Send an EVENT message with retries */
|
|
69
|
-
publish(event: NostrEvent, opts?:
|
|
70
|
-
retries?: number;
|
|
71
|
-
}): Observable<PublishResponse>;
|
|
88
|
+
publish(event: NostrEvent, opts?: PublishOptions): Promise<PublishResponse[]>;
|
|
72
89
|
/** Send a REQ message with retries */
|
|
73
|
-
request(filters: FilterInput, opts?:
|
|
74
|
-
id?: string;
|
|
75
|
-
retries?: number;
|
|
76
|
-
}): Observable<NostrEvent>;
|
|
90
|
+
request(filters: FilterInput, opts?: RequestOptions): Observable<NostrEvent>;
|
|
77
91
|
/** Open a subscription with retries */
|
|
78
|
-
subscription(filters: FilterInput, opts?:
|
|
79
|
-
id?: string;
|
|
80
|
-
retries?: number;
|
|
81
|
-
}): Observable<SubscriptionResponse>;
|
|
92
|
+
subscription(filters: FilterInput, opts?: SubscriptionOptions): Observable<SubscriptionResponse>;
|
|
82
93
|
}
|
|
83
94
|
export interface IPool {
|
|
84
|
-
/** Send an EVENT message */
|
|
85
|
-
event(relays: string[], event: NostrEvent): Observable<PublishResponse>;
|
|
86
|
-
/** Send a REQ message */
|
|
87
|
-
req(relays: string[], filters: FilterInput, id?: string): Observable<SubscriptionResponse>;
|
|
88
95
|
/** Get or create a relay */
|
|
89
96
|
relay(url: string): IRelay;
|
|
90
97
|
/** Create a relay group */
|
|
91
98
|
group(relays: string[]): IGroup;
|
|
99
|
+
/** Removes a relay from the pool and defaults to closing the connection */
|
|
100
|
+
remove(relay: string | IRelay, close?: boolean): void;
|
|
101
|
+
/** Send a REQ message */
|
|
102
|
+
req(relays: string[], filters: FilterInput, id?: string): Observable<SubscriptionResponse>;
|
|
103
|
+
/** Send an EVENT message */
|
|
104
|
+
event(relays: string[], event: NostrEvent): Observable<PublishResponse>;
|
|
92
105
|
/** Send an EVENT message to relays with retries */
|
|
93
|
-
publish(relays: string[], event: NostrEvent, opts?:
|
|
94
|
-
retries?: number;
|
|
95
|
-
}): Observable<PublishResponse>;
|
|
106
|
+
publish(relays: string[], event: NostrEvent, opts?: PublishOptions): Promise<PublishResponse[]>;
|
|
96
107
|
/** Send a REQ message to relays with retries */
|
|
97
|
-
request(relays: string[], filters: FilterInput, opts?:
|
|
98
|
-
id?: string;
|
|
99
|
-
retries?: number;
|
|
100
|
-
}): Observable<NostrEvent>;
|
|
108
|
+
request(relays: string[], filters: FilterInput, opts?: RequestOptions): Observable<NostrEvent>;
|
|
101
109
|
/** Open a subscription to relays with retries */
|
|
102
|
-
subscription(relays: string[], filters: FilterInput, opts?:
|
|
103
|
-
id?: string;
|
|
104
|
-
retries?: number;
|
|
105
|
-
}): Observable<SubscriptionResponse>;
|
|
110
|
+
subscription(relays: string[], filters: FilterInput, opts?: SubscriptionOptions): Observable<SubscriptionResponse>;
|
|
106
111
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "applesauce-relay",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "nostr relay communication framework built on rxjs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -54,14 +54,14 @@
|
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@noble/hashes": "^1.7.1",
|
|
57
|
-
"applesauce-core": "^
|
|
57
|
+
"applesauce-core": "^3.1.0",
|
|
58
58
|
"nanoid": "^5.0.9",
|
|
59
|
-
"nostr-tools": "
|
|
59
|
+
"nostr-tools": "~2.15",
|
|
60
60
|
"rxjs": "^7.8.1"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@hirez_io/observer-spy": "^2.2.0",
|
|
64
|
-
"applesauce-signers": "^
|
|
64
|
+
"applesauce-signers": "^3.1.0",
|
|
65
65
|
"@vitest/expect": "^3.1.1",
|
|
66
66
|
"typescript": "^5.7.3",
|
|
67
67
|
"vitest": "^3.2.3",
|