applesauce-relay 6.0.2 → 6.2.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/lib/negentropy.js +1 -1
- package/dist/pool.d.ts +8 -3
- package/dist/pool.js +29 -15
- package/dist/relay.d.ts +14 -2
- package/dist/relay.js +48 -15
- package/package.json +4 -4
package/dist/lib/negentropy.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// (C) 2023 Doug Hoyte. MIT license
|
|
2
2
|
// Modified by hzrd149 to be TypeScript and work without the window.cyrpto.subtle API
|
|
3
|
-
import { sha256 } from "@noble/hashes/sha2";
|
|
3
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
4
4
|
const PROTOCOL_VERSION = 0x61; // Version 1
|
|
5
5
|
const ID_SIZE = 32;
|
|
6
6
|
const FINGERPRINT_SIZE = 16;
|
package/dist/pool.d.ts
CHANGED
|
@@ -4,15 +4,18 @@ import { FilterMap, OutboxMap } from "applesauce-core/helpers/relay-selection";
|
|
|
4
4
|
import { BehaviorSubject, Observable, Subject } from "rxjs";
|
|
5
5
|
import { RelayGroup } from "./group.js";
|
|
6
6
|
import type { NegentropySyncOptions, ReconcileFunction } from "./negentropy.js";
|
|
7
|
-
import { Relay,
|
|
8
|
-
import type {
|
|
7
|
+
import { Relay, type RelayOptions, SyncDirection } from "./relay.js";
|
|
8
|
+
import type { FilterInput, GroupReqMessage, GroupReqOptions, NegentropyReadStore, NegentropySyncStore, PoolRelayInput, PublishResponse, RelayCountResponse, RelayStatus } from "./types.js";
|
|
9
9
|
export declare class RelayPool {
|
|
10
10
|
options?: RelayOptions | undefined;
|
|
11
11
|
relays$: BehaviorSubject<Map<string, Relay>>;
|
|
12
12
|
get relays(): Map<string, Relay>;
|
|
13
13
|
/** Observable of relay status for all relays in the pool */
|
|
14
14
|
status$: Observable<Record<string, RelayStatus>>;
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Whether to ignore relays that are ready=false
|
|
17
|
+
* @deprecated use {@link ignoreUnhealthyRelays} in group() or request() input
|
|
18
|
+
*/
|
|
16
19
|
ignoreOffline: boolean;
|
|
17
20
|
/** A signal when a relay is added */
|
|
18
21
|
add$: Subject<Relay>;
|
|
@@ -25,6 +28,8 @@ export declare class RelayPool {
|
|
|
25
28
|
group(relays: PoolRelayInput, ignoreOffline?: boolean): RelayGroup;
|
|
26
29
|
/** Removes a relay from the pool and defaults to closing the connection */
|
|
27
30
|
remove(relay: string | Relay, close?: boolean): void;
|
|
31
|
+
/** Closes and removes every relay in the pool, tearing down all of their connections and timers */
|
|
32
|
+
close(): void;
|
|
28
33
|
/** Make a REQ to multiple relays that does not deduplicate events */
|
|
29
34
|
req(relays: PoolRelayInput, filters: FilterInput, opts?: GroupReqOptions): Observable<GroupReqMessage>;
|
|
30
35
|
/** Send an EVENT message to multiple relays */
|
package/dist/pool.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isFilterEqual } from "applesauce-core/helpers/filter";
|
|
2
2
|
import { createFilterMap } from "applesauce-core/helpers/relay-selection";
|
|
3
3
|
import { normalizeURL } from "applesauce-core/helpers/url";
|
|
4
|
-
import { BehaviorSubject, distinctUntilChanged, isObservable, map, merge, of, scan, shareReplay, startWith, Subject, switchMap, } from "rxjs";
|
|
4
|
+
import { BehaviorSubject, combineLatest, distinctUntilChanged, filter, isObservable, map, merge, of, scan, shareReplay, startWith, Subject, switchMap, take, } from "rxjs";
|
|
5
5
|
import { RelayGroup } from "./group.js";
|
|
6
6
|
import { Relay } from "./relay.js";
|
|
7
7
|
export class RelayPool {
|
|
@@ -12,8 +12,11 @@ export class RelayPool {
|
|
|
12
12
|
}
|
|
13
13
|
/** Observable of relay status for all relays in the pool */
|
|
14
14
|
status$;
|
|
15
|
-
/**
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Whether to ignore relays that are ready=false
|
|
17
|
+
* @deprecated use {@link ignoreUnhealthyRelays} in group() or request() input
|
|
18
|
+
*/
|
|
19
|
+
ignoreOffline = false;
|
|
17
20
|
/** A signal when a relay is added */
|
|
18
21
|
add$ = new Subject();
|
|
19
22
|
/** A signal when a relay is removed */
|
|
@@ -55,11 +58,17 @@ export class RelayPool {
|
|
|
55
58
|
? relays.map((url) => this.relay(url))
|
|
56
59
|
: relays.pipe(map((urls) => urls.map((url) => this.relay(url))));
|
|
57
60
|
if (ignoreOffline) {
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
// Convert input to an observable so it can react to relays becoming ready.
|
|
62
|
+
// Each relay is included once `ready$` first emits true, and stays included
|
|
63
|
+
// afterwards (subsequent ready=false changes are ignored since the request
|
|
64
|
+
// or subscription will have already started).
|
|
65
|
+
const input$ = Array.isArray(input) ? of(input) : input;
|
|
66
|
+
input = input$.pipe(switchMap((relays) => {
|
|
67
|
+
if (relays.length === 0)
|
|
68
|
+
return of([]);
|
|
69
|
+
const signals = relays.map((relay) => relay.ready$.pipe(filter((ready) => ready), take(1), map(() => relay), startWith(null)));
|
|
70
|
+
return combineLatest(signals).pipe(map((arr) => arr.filter((r) => r !== null)));
|
|
71
|
+
}));
|
|
63
72
|
}
|
|
64
73
|
return new RelayGroup(input);
|
|
65
74
|
}
|
|
@@ -82,19 +91,22 @@ export class RelayPool {
|
|
|
82
91
|
this.relays$.next(this.relays);
|
|
83
92
|
this.remove$.next(instance);
|
|
84
93
|
}
|
|
94
|
+
/** Closes and removes every relay in the pool, tearing down all of their connections and timers */
|
|
95
|
+
close() {
|
|
96
|
+
for (const relay of [...this.relays.values()])
|
|
97
|
+
this.remove(relay, true);
|
|
98
|
+
}
|
|
85
99
|
/** Make a REQ to multiple relays that does not deduplicate events */
|
|
86
100
|
req(relays, filters, opts) {
|
|
87
|
-
|
|
88
|
-
return this.group(relays, false).req(filters, opts);
|
|
101
|
+
return this.group(relays).req(filters, opts);
|
|
89
102
|
}
|
|
90
103
|
/** Send an EVENT message to multiple relays */
|
|
91
104
|
event(relays, event) {
|
|
92
|
-
|
|
93
|
-
return this.group(relays, false).event(event);
|
|
105
|
+
return this.group(relays).event(event);
|
|
94
106
|
}
|
|
95
107
|
/** Negentropy sync event ids with the relays and an event store */
|
|
96
108
|
negentropy(relays, store, filter, reconcile, opts) {
|
|
97
|
-
return this.group(relays
|
|
109
|
+
return this.group(relays).negentropy(store, filter, reconcile, opts);
|
|
98
110
|
}
|
|
99
111
|
/** Publish an event to multiple relays */
|
|
100
112
|
publish(relays, event, opts) {
|
|
@@ -134,10 +146,12 @@ export class RelayPool {
|
|
|
134
146
|
}
|
|
135
147
|
/** Count events on multiple relays */
|
|
136
148
|
count(relays, filters, id) {
|
|
137
|
-
|
|
149
|
+
// Never filter out offline relays in manual methods
|
|
150
|
+
return this.group(relays, false).count(filters, id);
|
|
138
151
|
}
|
|
139
152
|
/** Negentropy sync events with the relays and an event store */
|
|
140
153
|
sync(relays, store, filter, direction) {
|
|
141
|
-
|
|
154
|
+
// Never filter out offline relays in manual methods
|
|
155
|
+
return this.group(relays, false).sync(store, filter, direction);
|
|
142
156
|
}
|
|
143
157
|
}
|
package/dist/relay.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { logger } from "applesauce-core";
|
|
2
2
|
import { KnownEvent, NostrEvent } from "applesauce-core/helpers/event";
|
|
3
3
|
import { Filter } from "applesauce-core/helpers/filter";
|
|
4
|
-
import { BehaviorSubject, MonoTypeOperatorFunction, Observable, RepeatConfig, RetryConfig, Subject } from "rxjs";
|
|
4
|
+
import { BehaviorSubject, MonoTypeOperatorFunction, Observable, RepeatConfig, ReplaySubject, RetryConfig, Subject, Subscription } from "rxjs";
|
|
5
5
|
import { WebSocketSubject, WebSocketSubjectConfig } from "rxjs/webSocket";
|
|
6
6
|
import { type NegentropySyncOptions, type ReconcileFunction } from "./negentropy.js";
|
|
7
7
|
import { AuthSigner, FilterInput, NegentropyReadStore, NegentropySyncStore, PublishOptions, PublishResponse, RelayCountResponse, RelayInformation, RelayReqMessage, RelayReqOptions, RelayRequestCompleteOperator, RelayRequestOptions, RelayRequestResponse, RelayStatus, RelaySubscriptionOptions, RelaySubscriptionResponse } from "./types.js";
|
|
@@ -149,6 +149,15 @@ export declare class Relay {
|
|
|
149
149
|
protected resetState(): void;
|
|
150
150
|
/** An internal observable that is responsible for watching all messages and updating state, subscribing to it will trigger a connection to the relay */
|
|
151
151
|
protected watchTower: Observable<never>;
|
|
152
|
+
/** Long-lived state-watcher subscriptions created in the constructor (open/close/auth) */
|
|
153
|
+
protected internalSubscriptions: Subscription;
|
|
154
|
+
/** The currently armed reconnect timer subscription, if any */
|
|
155
|
+
protected reconnectSubscription: Subscription | null;
|
|
156
|
+
/**
|
|
157
|
+
* Fires when the relay is closed. Used to cancel the watchTower's keepAlive reset timer.
|
|
158
|
+
* A ReplaySubject so a reset timer armed after close() is torn down immediately.
|
|
159
|
+
*/
|
|
160
|
+
protected destroy$: ReplaySubject<void>;
|
|
152
161
|
constructor(url: string, opts?: RelayOptions);
|
|
153
162
|
/** Set ready = false and start the reconnect timer */
|
|
154
163
|
protected startReconnectTimer(error: Error | CloseEvent): void;
|
|
@@ -192,7 +201,10 @@ export declare class Relay {
|
|
|
192
201
|
publish(event: NostrEvent, opts?: PublishOptions): Promise<PublishResponse>;
|
|
193
202
|
/** Negentropy sync events with the relay and an event store */
|
|
194
203
|
sync(store: NegentropySyncStore, filters: Filter, direction?: SyncDirection): Observable<NostrEvent>;
|
|
195
|
-
/**
|
|
204
|
+
/**
|
|
205
|
+
* Force close the connection and tear down all internal subscriptions and timers.
|
|
206
|
+
* @note This is a terminal operation; the relay should be discarded after calling it.
|
|
207
|
+
*/
|
|
196
208
|
close(): void;
|
|
197
209
|
/** An async method that returns the NIP-11 information document for the relay */
|
|
198
210
|
getInformation(): Promise<RelayInformation | null>;
|
package/dist/relay.js
CHANGED
|
@@ -4,7 +4,7 @@ import { ensureHttpURL } from "applesauce-core/helpers/url";
|
|
|
4
4
|
import { mapEventsToStore, simpleTimeout } from "applesauce-core/observable";
|
|
5
5
|
import { nanoid } from "nanoid";
|
|
6
6
|
import { makeAuthEvent } from "nostr-tools/nip42";
|
|
7
|
-
import { BehaviorSubject, catchError, combineLatest, defer, EMPTY, endWith, filter, finalize, firstValueFrom, from, identity, ignoreElements, isObservable, lastValueFrom, map, merge, mergeMap, mergeWith, NEVER, Observable, of, repeat, retry, scan, share, shareReplay, startWith, Subject, switchMap, take, takeUntil, takeWhile, tap, throwError, timeout, timer, } from "rxjs";
|
|
7
|
+
import { BehaviorSubject, catchError, combineLatest, defer, EMPTY, endWith, filter, finalize, firstValueFrom, from, identity, ignoreElements, isObservable, lastValueFrom, map, merge, mergeMap, mergeWith, NEVER, Observable, of, repeat, ReplaySubject, retry, scan, share, shareReplay, startWith, Subject, Subscription, switchMap, take, takeUntil, takeWhile, tap, throwError, timeout, timer, } from "rxjs";
|
|
8
8
|
import { webSocket } from "rxjs/webSocket";
|
|
9
9
|
import { completeWhen } from "./operators/complete-when.js";
|
|
10
10
|
const AUTH_REQUIRED_PREFIX = "auth-required:";
|
|
@@ -210,6 +210,15 @@ export class Relay {
|
|
|
210
210
|
}
|
|
211
211
|
/** An internal observable that is responsible for watching all messages and updating state, subscribing to it will trigger a connection to the relay */
|
|
212
212
|
watchTower;
|
|
213
|
+
/** Long-lived state-watcher subscriptions created in the constructor (open/close/auth) */
|
|
214
|
+
internalSubscriptions = new Subscription();
|
|
215
|
+
/** The currently armed reconnect timer subscription, if any */
|
|
216
|
+
reconnectSubscription = null;
|
|
217
|
+
/**
|
|
218
|
+
* Fires when the relay is closed. Used to cancel the watchTower's keepAlive reset timer.
|
|
219
|
+
* A ReplaySubject so a reset timer armed after close() is torn down immediately.
|
|
220
|
+
*/
|
|
221
|
+
destroy$ = new ReplaySubject(1);
|
|
213
222
|
constructor(url, opts) {
|
|
214
223
|
this.url = url;
|
|
215
224
|
this.log = this.log.extend(url);
|
|
@@ -239,15 +248,15 @@ export class Relay {
|
|
|
239
248
|
/** Use the static method to create a new reconnect method for this relay */
|
|
240
249
|
this.reconnectTimer = Relay.createReconnectTimer(url);
|
|
241
250
|
// Subscribe to open and close events
|
|
242
|
-
this.open$.subscribe(() => {
|
|
251
|
+
this.internalSubscriptions.add(this.open$.subscribe(() => {
|
|
243
252
|
this.log("Connected");
|
|
244
253
|
this.connected$.next(true);
|
|
245
254
|
this.attempts$.next(0);
|
|
246
255
|
this.error$.next(null);
|
|
247
256
|
// Reset to clean state
|
|
248
257
|
this.resetState();
|
|
249
|
-
});
|
|
250
|
-
this.close$.subscribe((event) => {
|
|
258
|
+
}));
|
|
259
|
+
this.internalSubscriptions.add(this.close$.subscribe((event) => {
|
|
251
260
|
if (this.connected$.value)
|
|
252
261
|
this.log("Disconnected");
|
|
253
262
|
else
|
|
@@ -262,7 +271,7 @@ export class Relay {
|
|
|
262
271
|
// Start the reconnect timer if the connection was not closed cleanly
|
|
263
272
|
if (!event.wasClean)
|
|
264
273
|
this.startReconnectTimer(event);
|
|
265
|
-
});
|
|
274
|
+
}));
|
|
266
275
|
this.socket = webSocket({
|
|
267
276
|
url,
|
|
268
277
|
openObserver: this.open$,
|
|
@@ -288,12 +297,12 @@ export class Relay {
|
|
|
288
297
|
this.authRequiredForRead$ = this.receivedAuthRequiredForReq;
|
|
289
298
|
this.authRequiredForPublish$ = this.receivedAuthRequiredForEvent;
|
|
290
299
|
// Log when auth is required
|
|
291
|
-
this.authRequiredForRead$
|
|
300
|
+
this.internalSubscriptions.add(this.authRequiredForRead$
|
|
292
301
|
.pipe(filter((r) => r === true), take(1))
|
|
293
|
-
.subscribe(() => this.log("Auth required for REQ"));
|
|
294
|
-
this.authRequiredForPublish$
|
|
302
|
+
.subscribe(() => this.log("Auth required for REQ")));
|
|
303
|
+
this.internalSubscriptions.add(this.authRequiredForPublish$
|
|
295
304
|
.pipe(filter((r) => r === true), take(1))
|
|
296
|
-
.subscribe(() => this.log("Auth required for EVENT"));
|
|
305
|
+
.subscribe(() => this.log("Auth required for EVENT")));
|
|
297
306
|
// Create status$ observable by combining state observables
|
|
298
307
|
this.status$ = combineLatest({
|
|
299
308
|
url: of(this.url),
|
|
@@ -413,8 +422,8 @@ export class Relay {
|
|
|
413
422
|
this.startReconnectTimer(error instanceof Error ? error : new Error("Connection error"));
|
|
414
423
|
return NEVER;
|
|
415
424
|
}),
|
|
416
|
-
// Add keep alive timer to the connection
|
|
417
|
-
share({ resetOnRefCountZero: () => timer(this.keepAlive) }));
|
|
425
|
+
// Add keep alive timer to the connection, cancelled when the relay is closed
|
|
426
|
+
share({ resetOnRefCountZero: () => timer(this.keepAlive).pipe(takeUntil(this.destroy$)) }));
|
|
418
427
|
}),
|
|
419
428
|
// There should only be a single watch tower
|
|
420
429
|
share());
|
|
@@ -425,9 +434,12 @@ export class Relay {
|
|
|
425
434
|
return;
|
|
426
435
|
this.error$.next(error instanceof Error ? error : new Error("Connection error"));
|
|
427
436
|
this._ready$.next(false);
|
|
428
|
-
|
|
437
|
+
// Cancel any previously armed reconnect timer before arming a new one
|
|
438
|
+
this.reconnectSubscription?.unsubscribe();
|
|
439
|
+
this.reconnectSubscription = this.reconnectTimer(error, this.attempts$.value)
|
|
429
440
|
.pipe(take(1))
|
|
430
441
|
.subscribe(() => {
|
|
442
|
+
this.reconnectSubscription = null;
|
|
431
443
|
this._ready$.next(true);
|
|
432
444
|
});
|
|
433
445
|
}
|
|
@@ -826,8 +838,14 @@ export class Relay {
|
|
|
826
838
|
// Send missing events to the relay
|
|
827
839
|
if (direction & SyncDirection.SEND && have.length > 0) {
|
|
828
840
|
const events = await getEvents(have);
|
|
829
|
-
// Send all events to the relay
|
|
830
|
-
|
|
841
|
+
// Send all events to the relay, marking them as seen on this relay once accepted.
|
|
842
|
+
// The events were not fetched from the relay, but after a successful publish the relay has them.
|
|
843
|
+
await Promise.allSettled(events.map(async (event) => {
|
|
844
|
+
const response = await lastValueFrom(this.event(event));
|
|
845
|
+
if (response.ok)
|
|
846
|
+
addSeenRelay(event, this.url);
|
|
847
|
+
return response;
|
|
848
|
+
}));
|
|
831
849
|
}
|
|
832
850
|
// Fetch missing events from the relay
|
|
833
851
|
if (direction & SyncDirection.RECEIVE && need.length > 0) {
|
|
@@ -862,8 +880,23 @@ export class Relay {
|
|
|
862
880
|
// Only create one upstream subscription
|
|
863
881
|
share());
|
|
864
882
|
}
|
|
865
|
-
/**
|
|
883
|
+
/**
|
|
884
|
+
* Force close the connection and tear down all internal subscriptions and timers.
|
|
885
|
+
* @note This is a terminal operation; the relay should be discarded after calling it.
|
|
886
|
+
*/
|
|
866
887
|
close() {
|
|
888
|
+
// Cancel the watchTower's keepAlive reset timer armed at refcount-zero (and any future one)
|
|
889
|
+
this.destroy$.next();
|
|
890
|
+
this.destroy$.complete();
|
|
891
|
+
// Cancel any pending reconnect timer so it cannot fire (or hold the event loop open) after close
|
|
892
|
+
this.reconnectSubscription?.unsubscribe();
|
|
893
|
+
this.reconnectSubscription = null;
|
|
894
|
+
// Tear down the constructor state watchers (open/close/auth)
|
|
895
|
+
this.internalSubscriptions.unsubscribe();
|
|
896
|
+
// Mark as disconnected since the close$ watcher has been torn down
|
|
897
|
+
if (this.connected$.value)
|
|
898
|
+
this.connected$.next(false);
|
|
899
|
+
// Finally close the underlying socket
|
|
867
900
|
this.socket.unsubscribe();
|
|
868
901
|
}
|
|
869
902
|
/** An async method that returns the NIP-11 information document for the relay */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "applesauce-relay",
|
|
3
|
-
"version": "6.0
|
|
3
|
+
"version": "6.2.0",
|
|
4
4
|
"description": "nostr relay communication framework built on rxjs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -56,15 +56,15 @@
|
|
|
56
56
|
}
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@noble/hashes": "^
|
|
60
|
-
"applesauce-core": "^6.
|
|
59
|
+
"@noble/hashes": "^2.2.0",
|
|
60
|
+
"applesauce-core": "^6.2.0",
|
|
61
61
|
"nanoid": "^5.0.9",
|
|
62
62
|
"nostr-tools": "~2.19",
|
|
63
63
|
"rxjs": "^7.8.1"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@hirez_io/observer-spy": "^2.2.0",
|
|
67
|
-
"applesauce-signers": "^6.
|
|
67
|
+
"applesauce-signers": "^6.2.0",
|
|
68
68
|
"rimraf": "^6.0.1",
|
|
69
69
|
"typescript": "^5.7.3",
|
|
70
70
|
"vitest": "^4.0.15",
|