applesauce-relay 6.0.3 → 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.
@@ -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
@@ -28,6 +28,8 @@ export declare class RelayPool {
28
28
  group(relays: PoolRelayInput, ignoreOffline?: boolean): RelayGroup;
29
29
  /** Removes a relay from the pool and defaults to closing the connection */
30
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;
31
33
  /** Make a REQ to multiple relays that does not deduplicate events */
32
34
  req(relays: PoolRelayInput, filters: FilterInput, opts?: GroupReqOptions): Observable<GroupReqMessage>;
33
35
  /** Send an EVENT message to multiple relays */
package/dist/pool.js CHANGED
@@ -91,6 +91,11 @@ export class RelayPool {
91
91
  this.relays$.next(this.relays);
92
92
  this.remove$.next(instance);
93
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
+ }
94
99
  /** Make a REQ to multiple relays that does not deduplicate events */
95
100
  req(relays, filters, opts) {
96
101
  return this.group(relays).req(filters, opts);
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
- /** Force close the connection */
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
- this.reconnectTimer(error, this.attempts$.value)
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
- await Promise.allSettled(events.map((event) => lastValueFrom(this.event(event))));
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
- /** Force close the connection */
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",
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": "^1.7.1",
60
- "applesauce-core": "^6.0.0",
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.0.0",
67
+ "applesauce-signers": "^6.2.0",
68
68
  "rimraf": "^6.0.1",
69
69
  "typescript": "^5.7.3",
70
70
  "vitest": "^4.0.15",