applesauce-relay 6.0.0 → 6.0.2

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/pool.js CHANGED
@@ -46,6 +46,7 @@ export class RelayPool {
46
46
  relay = new Relay(url, this.options);
47
47
  this.relays.set(url, relay);
48
48
  this.relays$.next(this.relays);
49
+ this.add$.next(relay);
49
50
  return relay;
50
51
  }
51
52
  /** Create a group of relays */
package/dist/relay.d.ts CHANGED
@@ -42,10 +42,10 @@ export type RelayOptions = {
42
42
  now: number;
43
43
  attempts: number;
44
44
  }) => "reconnect" | "close" | "ignore";
45
- /** Default reconnect config for subscription() method */
46
- subscriptionReconnect?: RetryConfig;
47
- /** Default reconnect config for request() method */
48
- requestReconnect?: RetryConfig;
45
+ /** Default retry count or config for subscription() connection errors (default: 3) */
46
+ subscriptionReconnect?: number | RetryConfig;
47
+ /** Default retry count or config for request() connection errors (default: 3) */
48
+ requestReconnect?: number | RetryConfig;
49
49
  /** Default retry config for publish() method */
50
50
  publishRetry?: RetryConfig;
51
51
  };
@@ -134,9 +134,9 @@ export declare class Relay {
134
134
  pingFrequency: number;
135
135
  /** How long to wait for EOSE response in milliseconds (default 20000) */
136
136
  pingTimeout: number;
137
- /** Default reconnect config for subscription() method */
137
+ /** Default retry config for subscription() connection errors */
138
138
  subscriptionReconnect: RetryConfig;
139
- /** Default reconnect config for request() method */
139
+ /** Default retry config for request() connection errors */
140
140
  requestReconnect: RetryConfig;
141
141
  /** Default retry config for publish() method */
142
142
  publishRetry: RetryConfig;
@@ -159,7 +159,12 @@ export declare class Relay {
159
159
  multiplex<T>(open: () => any, close: () => any, filter: (message: any) => boolean): Observable<T>;
160
160
  /** Send a message to the relay */
161
161
  send(message: any): void;
162
- /** Create a REQ observable that emits events or "EOSE" or errors */
162
+ /**
163
+ * Create a REQ observable that emits OPEN, EVENT, EOSE, and CLOSED messages.
164
+ *
165
+ * `resubscribe` only repeats after the relay sends a clean CLOSED message for this REQ.
166
+ * `reconnect` only retries connection errors and does not retry relay CLOSED errors.
167
+ */
163
168
  req(filters: FilterInput, opts?: RelayReqOptions): Observable<RelayReqMessage>;
164
169
  /** Create a COUNT observable that emits a single count response */
165
170
  count(filters: Filter | Filter[], id?: string): Observable<RelayCountResponse>;
@@ -171,15 +176,17 @@ export declare class Relay {
171
176
  negentropy(store: NegentropyReadStore, filter: Filter, reconcile: ReconcileFunction, opts?: NegentropySyncOptions): Promise<boolean>;
172
177
  /** Authenticate with the relay using a signer */
173
178
  authenticate(signer: AuthSigner): Promise<PublishResponse>;
174
- /** Internal operator for creating the retry() operator for reconnecting to the websocket */
179
+ /** Internal operator for creating a retry() operator */
175
180
  protected customRetryOperator<T extends unknown = unknown>(times: undefined | boolean | number | RetryConfig, base?: RetryConfig): MonoTypeOperatorFunction<T>;
176
- /** Internal operator for creating the repeat() operator for resubscribing */
177
- protected customRepeatOperator<T extends unknown = unknown>(times: undefined | boolean | number | RepeatConfig | undefined): MonoTypeOperatorFunction<T>;
181
+ /** Internal operator for retrying connection failures without retrying relay CLOSED errors */
182
+ protected customConnectionRetryOperator<T extends unknown = unknown>(times: undefined | boolean | number | RetryConfig, base?: RetryConfig): MonoTypeOperatorFunction<T>;
183
+ /** Internal operator for creating the repeat() operator, optionally gated by a condition */
184
+ protected customRepeatOperator<T extends unknown = unknown>(times: undefined | boolean | number | RepeatConfig | undefined, condition?: () => boolean): MonoTypeOperatorFunction<T>;
178
185
  /** Internal operator for creating the timeout() operator */
179
186
  protected customTimeoutOperator<T extends unknown = unknown>(timeout: undefined | boolean | number, defaultTimeout: number): MonoTypeOperatorFunction<T>;
180
- /** Creates a REQ that retries when relay errors ( default 3 retries ) */
187
+ /** Creates a persistent REQ that retries connection errors (default 3 retries) */
181
188
  subscription(filters: FilterInput, opts?: RelaySubscriptionOptions): Observable<RelaySubscriptionResponse>;
182
- /** Makes a single request that retires on errors and completes on EOSE */
189
+ /** Makes a single request that retries connection errors and completes on EOSE */
183
190
  request(filters: FilterInput, opts?: RelayRequestOptions): Observable<RelayRequestResponse>;
184
191
  /** Publishes an event to the relay and retries when relay errors or responds with auth-required ( default 3 retries ) */
185
192
  publish(event: NostrEvent, opts?: PublishOptions): Promise<PublishResponse>;
package/dist/relay.js CHANGED
@@ -4,16 +4,21 @@ 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, 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, retry, scan, share, shareReplay, startWith, Subject, 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:";
11
- /** Default retry subscription, request, and publish. linear backoff */
11
+ /** Default reconnect/retry config for request, subscription, and publish. linear backoff */
12
12
  const DEFAULT_RETRY_CONFIG = {
13
13
  count: 3,
14
14
  delay: (_err, count) => timer(count * 1000),
15
15
  resetOnSuccess: true,
16
16
  };
17
+ function normalizeRetryConfig(config) {
18
+ if (typeof config === "number")
19
+ return { ...DEFAULT_RETRY_CONFIG, count: config };
20
+ return { ...DEFAULT_RETRY_CONFIG, ...(config ?? {}) };
21
+ }
17
22
  /** Flags for the negentropy sync type */
18
23
  export var SyncDirection;
19
24
  (function (SyncDirection) {
@@ -174,9 +179,9 @@ export class Relay {
174
179
  pingFrequency = 29_000;
175
180
  /** How long to wait for EOSE response in milliseconds (default 20000) */
176
181
  pingTimeout = 20_000;
177
- /** Default reconnect config for subscription() method */
182
+ /** Default retry config for subscription() connection errors */
178
183
  subscriptionReconnect;
179
- /** Default reconnect config for request() method */
184
+ /** Default retry config for request() connection errors */
180
185
  requestReconnect;
181
186
  /** Default retry config for publish() method */
182
187
  publishRetry;
@@ -224,8 +229,8 @@ export class Relay {
224
229
  if (opts?.onUnresponsive !== undefined)
225
230
  this.onUnresponsive = opts.onUnresponsive;
226
231
  // Set retry configs
227
- this.subscriptionReconnect = { ...DEFAULT_RETRY_CONFIG, ...(opts?.subscriptionReconnect ?? {}) };
228
- this.requestReconnect = { ...DEFAULT_RETRY_CONFIG, ...(opts?.requestReconnect ?? {}) };
232
+ this.subscriptionReconnect = normalizeRetryConfig(opts?.subscriptionReconnect);
233
+ this.requestReconnect = normalizeRetryConfig(opts?.requestReconnect);
229
234
  this.publishRetry = { ...DEFAULT_RETRY_CONFIG, ...(opts?.publishRetry ?? {}) };
230
235
  // Create an observable that tracks boolean authentication state
231
236
  this.authenticated$ = this.authenticationResponse$.pipe(map((response) => response?.ok === true));
@@ -461,7 +466,12 @@ export class Relay {
461
466
  send(message) {
462
467
  this.socket.next(message);
463
468
  }
464
- /** Create a REQ observable that emits events or "EOSE" or errors */
469
+ /**
470
+ * Create a REQ observable that emits OPEN, EVENT, EOSE, and CLOSED messages.
471
+ *
472
+ * `resubscribe` only repeats after the relay sends a clean CLOSED message for this REQ.
473
+ * `reconnect` only retries connection errors and does not retry relay CLOSED errors.
474
+ */
465
475
  req(filters, opts) {
466
476
  const id = opts?.id ?? nanoid();
467
477
  const waitForAuth = opts?.waitForAuth ?? true;
@@ -479,6 +489,7 @@ export class Relay {
479
489
  const filtersComplete = input.pipe(ignoreElements(), endWith(true));
480
490
  // Track whether the relay already sent CLOSED so we skip the redundant client CLOSE
481
491
  let relayClosedSub = false;
492
+ let shouldResubscribe = false;
482
493
  // Create an observable that filters responses from the relay to just the ones for this REQ
483
494
  const messages = this.socket.pipe(filter((m) => Array.isArray(m) && (m[0] === "EVENT" || m[0] === "CLOSED" || m[0] === "EOSE") && m[1] === id),
484
495
  // Map NIP-01 messages to RelayReqMessage
@@ -497,6 +508,7 @@ export class Relay {
497
508
  const error = parseClosedError(m.reason);
498
509
  if (error)
499
510
  throw error;
511
+ shouldResubscribe = true;
500
512
  }
501
513
  }),
502
514
  // Complete the stream on unprefixed CLOSED, emitting the CLOSED message last (inclusive)
@@ -509,6 +521,7 @@ export class Relay {
509
521
  map((filters) => {
510
522
  // Reset closed flag on each new REQ (resubscribe cycles)
511
523
  relayClosedSub = false;
524
+ shouldResubscribe = false;
512
525
  this.socket.next(["REQ", id, ...filters]);
513
526
  // Add to tracking when REQ is sent
514
527
  this.reqs$.next({ ...this.reqs$.value, [id]: filters });
@@ -541,7 +554,7 @@ export class Relay {
541
554
  share());
542
555
  // Wait for auth only when enabled and make sure to start the watch tower
543
556
  const reqWithAuthStrategy = waitForAuth ? this.waitForAuth(this.authRequiredForRead$, observable) : observable;
544
- return this.waitForReady(reqWithAuthStrategy).pipe(
557
+ return defer(() => this.waitForReady(reqWithAuthStrategy)).pipe(
545
558
  // Retry only auth-required errors, optionally waiting for authentication first
546
559
  retry({
547
560
  delay: (error) => {
@@ -558,8 +571,10 @@ export class Relay {
558
571
  return this.authenticated$.pipe(filter((authenticated) => authenticated), take(1));
559
572
  },
560
573
  }),
561
- // Create resubscribe logic (repeat operator)
562
- this.customRepeatOperator(opts?.resubscribe),
574
+ // Retry connection errors independently from relay CLOSED errors
575
+ this.customConnectionRetryOperator(opts?.reconnect),
576
+ // Resubscribe only after the relay cleanly CLOSED this REQ
577
+ this.customRepeatOperator(opts?.resubscribe, () => shouldResubscribe),
563
578
  // Only create one upstream subscription
564
579
  share());
565
580
  }
@@ -679,7 +694,7 @@ export class Relay {
679
694
  const start = p instanceof Promise ? from(p) : of(p);
680
695
  return lastValueFrom(start.pipe(switchMap((event) => this.auth(event))));
681
696
  }
682
- /** Internal operator for creating the retry() operator for reconnecting to the websocket */
697
+ /** Internal operator for creating a retry() operator */
683
698
  customRetryOperator(times, base) {
684
699
  if (times === false || times === undefined)
685
700
  return identity;
@@ -690,16 +705,45 @@ export class Relay {
690
705
  else
691
706
  return retry({ ...base, ...times });
692
707
  }
693
- /** Internal operator for creating the repeat() operator for resubscribing */
694
- customRepeatOperator(times) {
708
+ /** Internal operator for retrying connection failures without retrying relay CLOSED errors */
709
+ customConnectionRetryOperator(times, base) {
695
710
  if (times === false || times === undefined)
696
711
  return identity;
697
- else if (times === true)
698
- return repeat();
712
+ const config = typeof times === "number" ? { ...base, count: times } : times === true ? (base ?? {}) : { ...base, ...times };
713
+ return retry({
714
+ ...config,
715
+ delay: (error, count) => {
716
+ if (error instanceof RelayClosedError)
717
+ return throwError(() => error);
718
+ if (typeof config.delay === "number")
719
+ return timer(config.delay);
720
+ if (typeof config.delay === "function")
721
+ return config.delay(error, count);
722
+ return of(null);
723
+ },
724
+ });
725
+ }
726
+ /** Internal operator for creating the repeat() operator, optionally gated by a condition */
727
+ customRepeatOperator(times, condition) {
728
+ if (times === false || times === undefined)
729
+ return identity;
730
+ const delay = (repeatCount) => {
731
+ if (condition && !condition())
732
+ return EMPTY;
733
+ if (typeof times === "object") {
734
+ if (typeof times.delay === "number")
735
+ return timer(times.delay);
736
+ if (typeof times.delay === "function")
737
+ return times.delay(repeatCount);
738
+ }
739
+ return of(null);
740
+ };
741
+ if (times === true)
742
+ return repeat({ delay });
699
743
  else if (typeof times === "number")
700
- return repeat(times);
744
+ return repeat({ count: times, delay });
701
745
  else
702
- return repeat(times);
746
+ return repeat({ ...times, delay });
703
747
  }
704
748
  /** Internal operator for creating the timeout() operator */
705
749
  customTimeoutOperator(timeout, defaultTimeout) {
@@ -713,7 +757,7 @@ export class Relay {
713
757
  else
714
758
  return simpleTimeout(timeout ?? defaultTimeout);
715
759
  }
716
- /** Creates a REQ that retries when relay errors ( default 3 retries ) */
760
+ /** Creates a persistent REQ that retries connection errors (default 3 retries) */
717
761
  subscription(filters, opts) {
718
762
  return this.req(filters, {
719
763
  ...opts,
@@ -726,7 +770,7 @@ export class Relay {
726
770
  // Single subscription
727
771
  share());
728
772
  }
729
- /** Makes a single request that retires on errors and completes on EOSE */
773
+ /** Makes a single request that retries connection errors and completes on EOSE */
730
774
  request(filters, opts) {
731
775
  const req = this.req(filters, {
732
776
  ...opts,
package/dist/types.d.ts CHANGED
@@ -53,12 +53,12 @@ export type RelayReqOptions = {
53
53
  */
54
54
  waitForAuth?: boolean;
55
55
  /**
56
- * Whether to resubscribe if the subscription is closed by the relay. default is false
56
+ * Whether to resubscribe after a clean CLOSED message from the relay. default is false
57
57
  * @see https://rxjs.dev/api/index/function/repeat
58
58
  */
59
59
  resubscribe?: boolean | number | Parameters<typeof repeat>[0];
60
60
  /**
61
- * Whether to reconnect when socket is closed. default is true (3 retries with 1 second delay)
61
+ * Whether to retry connection errors. default is true (3 retries with linear backoff)
62
62
  * @see https://rxjs.dev/api/index/function/retry
63
63
  */
64
64
  reconnect?: boolean | number | Parameters<typeof retry>[0];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-relay",
3
- "version": "6.0.0",
3
+ "version": "6.0.2",
4
4
  "description": "nostr relay communication framework built on rxjs",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",