applesauce-relay 0.0.0-next-20251220152312 → 0.0.0-next-20251231055351

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/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from "./group.js";
2
2
  export * from "./liveness.js";
3
+ export * from "./management.js";
3
4
  export * from "./pool.js";
4
5
  export * from "./relay.js";
5
6
  export * from "./types.js";
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from "./group.js";
2
2
  export * from "./liveness.js";
3
+ export * from "./management.js";
3
4
  export * from "./pool.js";
4
5
  export * from "./relay.js";
5
6
  export * from "./types.js";
@@ -0,0 +1,169 @@
1
+ import { logger } from "applesauce-core";
2
+ import { Observable } from "rxjs";
3
+ import { AuthSigner } from "./types.js";
4
+ import { Relay } from "./relay.js";
5
+ /** Base request structure for all NIP-86 requests */
6
+ export interface TRelayRequest<Method extends string, Params extends any[] = any[]> {
7
+ /** The method to call */
8
+ method: Method;
9
+ /** Parameters for the method */
10
+ params: Params;
11
+ }
12
+ /** Base response structure for all NIP-86 responses */
13
+ export type TRelayErrorResponse = {
14
+ /** Error message, non-null in case of error */
15
+ error: string;
16
+ result: null;
17
+ };
18
+ export type TRelaySuccessResponse<Result> = {
19
+ error: null;
20
+ /** Result object, null in case of error */
21
+ result: Result;
22
+ };
23
+ /** Merged relay management method and response types */
24
+ export type TRelayMethod<Method extends string = string, Params extends any[] = any[], Result extends any = any> = {
25
+ /** Method string */
26
+ method: Method;
27
+ /** Request type */
28
+ request: TRelayRequest<Method, Params>;
29
+ /** Response success type */
30
+ response: TRelaySuccessResponse<Result>;
31
+ /** Response error type */
32
+ error: TRelayErrorResponse;
33
+ };
34
+ export type SupportedMethodsMethod = TRelayMethod<"supportedmethods", [], string[]>;
35
+ export type BanPubkeyMethod = TRelayMethod<"banpubkey", [string, string?], true>;
36
+ export type ListBannedPubkeysMethod = TRelayMethod<"listbannedpubkeys", [], Array<{
37
+ pubkey: string;
38
+ reason?: string;
39
+ }>>;
40
+ export type AllowPubkeyMethod = TRelayMethod<"allowpubkey", [string, string?], true>;
41
+ export type ListAllowedPubkeysMethod = TRelayMethod<"listallowedpubkeys", [
42
+ ], Array<{
43
+ pubkey: string;
44
+ reason?: string;
45
+ }>>;
46
+ export type ListEventsNeedingModerationMethod = TRelayMethod<"listeventsneedingmoderation", [
47
+ ], Array<{
48
+ id: string;
49
+ reason?: string;
50
+ }>>;
51
+ export type AllowEventMethod = TRelayMethod<"allowevent", [string, string?], true>;
52
+ export type BanEventMethod = TRelayMethod<"banevent", [string, string?], true>;
53
+ export type ListBannedEventsMethod = TRelayMethod<"listbannedevents", [], Array<{
54
+ id: string;
55
+ reason?: string;
56
+ }>>;
57
+ export type ChangeRelayNameMethod = TRelayMethod<"changerelayname", [string], true>;
58
+ export type ChangeRelayDescriptionMethod = TRelayMethod<"changerelaydescription", [string], true>;
59
+ export type ChangeRelayIconMethod = TRelayMethod<"changerelayicon", [string], true>;
60
+ export type AllowKindMethod = TRelayMethod<"allowkind", [number], true>;
61
+ export type DisallowKindMethod = TRelayMethod<"disallowkind", [number], true>;
62
+ export type ListAllowedKindsMethod = TRelayMethod<"listallowedkinds", [], number[]>;
63
+ export type BlockIpMethod = TRelayMethod<"blockip", [string, string?], true>;
64
+ export type UnblockIpMethod = TRelayMethod<"unblockip", [string], true>;
65
+ export type ListBlockedIpsMethod = TRelayMethod<"listblockedips", [], Array<{
66
+ ip: string;
67
+ reason?: string;
68
+ }>>;
69
+ /** Union type for all relay management method definitions */
70
+ export type RelayManagementMethods = SupportedMethodsMethod | BanPubkeyMethod | ListBannedPubkeysMethod | AllowPubkeyMethod | ListAllowedPubkeysMethod | ListEventsNeedingModerationMethod | AllowEventMethod | BanEventMethod | ListBannedEventsMethod | ChangeRelayNameMethod | ChangeRelayDescriptionMethod | ChangeRelayIconMethod | AllowKindMethod | DisallowKindMethod | ListAllowedKindsMethod | BlockIpMethod | UnblockIpMethod | ListBlockedIpsMethod;
71
+ /** Custom error for relay management operations */
72
+ export declare class RelayManagementError extends Error {
73
+ readonly method?: string | undefined;
74
+ readonly statusCode?: number | undefined;
75
+ constructor(message: string, method?: string | undefined, statusCode?: number | undefined);
76
+ }
77
+ /** RelayManagement class for NIP-86 relay management API */
78
+ export declare class RelayManagement {
79
+ #private;
80
+ readonly relay: Relay;
81
+ readonly signer: AuthSigner;
82
+ protected log: typeof logger;
83
+ protected httpUrl: string;
84
+ constructor(relay: Relay, signer: AuthSigner);
85
+ /**
86
+ * Core request method that handles all RPC calls with NIP-98 authentication
87
+ */
88
+ request<Method extends RelayManagementMethods>(method: Method["method"], params: Method["request"]["params"]): Promise<Method["response"]["result"]>;
89
+ /** Get list of supported methods */
90
+ supportedMethods(): Promise<string[]>;
91
+ /** Ban a pubkey */
92
+ banPubkey(pubkey: string, reason?: string): Promise<true>;
93
+ /** List all banned pubkeys */
94
+ listBannedPubkeys(): Promise<Array<{
95
+ pubkey: string;
96
+ reason?: string;
97
+ }>>;
98
+ /** Allow a pubkey */
99
+ allowPubkey(pubkey: string, reason?: string): Promise<true>;
100
+ /** List all allowed pubkeys */
101
+ listAllowedPubkeys(): Promise<Array<{
102
+ pubkey: string;
103
+ reason?: string;
104
+ }>>;
105
+ /** List events needing moderation */
106
+ listEventsNeedingModeration(): Promise<Array<{
107
+ id: string;
108
+ reason?: string;
109
+ }>>;
110
+ /** Allow an event */
111
+ allowEvent(eventId: string, reason?: string): Promise<true>;
112
+ /** Ban an event */
113
+ banEvent(eventId: string, reason?: string): Promise<true>;
114
+ /** List all banned events */
115
+ listBannedEvents(): Promise<Array<{
116
+ id: string;
117
+ reason?: string;
118
+ }>>;
119
+ /** Change relay name */
120
+ changeRelayName(name: string): Promise<true>;
121
+ /** Change relay description */
122
+ changeRelayDescription(description: string): Promise<true>;
123
+ /** Change relay icon */
124
+ changeRelayIcon(iconUrl: string): Promise<true>;
125
+ /** Allow a kind */
126
+ allowKind(kind: number): Promise<true>;
127
+ /** Disallow a kind */
128
+ disallowKind(kind: number): Promise<true>;
129
+ /** List all allowed kinds */
130
+ listAllowedKinds(): Promise<number[]>;
131
+ /** Block an IP address */
132
+ blockIp(ip: string, reason?: string): Promise<true>;
133
+ /** Unblock an IP address */
134
+ unblockIp(ip: string): Promise<true>;
135
+ /** List all blocked IPs */
136
+ listBlockedIps(): Promise<Array<{
137
+ ip: string;
138
+ reason?: string;
139
+ }>>;
140
+ /** Observable that emits supported methods when subscribed */
141
+ supportMethods$: Observable<string[]>;
142
+ /** Observable that emits banned pubkeys when subscribed */
143
+ bannedPubkeys$: Observable<Array<{
144
+ pubkey: string;
145
+ reason?: string;
146
+ }>>;
147
+ /** Observable that emits allowed pubkeys when subscribed */
148
+ allowedPubkeys$: Observable<Array<{
149
+ pubkey: string;
150
+ reason?: string;
151
+ }>>;
152
+ /** Observable that emits events needing moderation when subscribed */
153
+ eventsNeedingModeration$: Observable<Array<{
154
+ id: string;
155
+ reason?: string;
156
+ }>>;
157
+ /** Observable that emits banned events when subscribed */
158
+ bannedEvents$: Observable<Array<{
159
+ id: string;
160
+ reason?: string;
161
+ }>>;
162
+ /** Observable that emits allowed kinds when subscribed */
163
+ allowedKinds$: Observable<number[]>;
164
+ /** Observable that emits blocked IPs when subscribed */
165
+ blockedIps$: Observable<Array<{
166
+ ip: string;
167
+ reason?: string;
168
+ }>>;
169
+ }
@@ -0,0 +1,202 @@
1
+ import { logger } from "applesauce-core";
2
+ import { ensureHttpURL } from "applesauce-core/helpers/url";
3
+ import { getToken } from "nostr-tools/nip98";
4
+ import { BehaviorSubject, from, shareReplay, throwError } from "rxjs";
5
+ import { catchError, switchMap } from "rxjs/operators";
6
+ /** Custom error for relay management operations */
7
+ export class RelayManagementError extends Error {
8
+ method;
9
+ statusCode;
10
+ constructor(message, method, statusCode) {
11
+ super(message);
12
+ this.method = method;
13
+ this.statusCode = statusCode;
14
+ this.name = "RelayManagementError";
15
+ }
16
+ }
17
+ /** RelayManagement class for NIP-86 relay management API */
18
+ export class RelayManagement {
19
+ relay;
20
+ signer;
21
+ log = logger.extend("RelayManagement");
22
+ httpUrl;
23
+ // Internal refresh triggers for observables
24
+ #refreshSupportMethods$ = new BehaviorSubject(undefined);
25
+ #refreshBannedPubkeys$ = new BehaviorSubject(undefined);
26
+ #refreshAllowedPubkeys$ = new BehaviorSubject(undefined);
27
+ #refreshEventsNeedingModeration$ = new BehaviorSubject(undefined);
28
+ #refreshBannedEvents$ = new BehaviorSubject(undefined);
29
+ #refreshAllowedKinds$ = new BehaviorSubject(undefined);
30
+ #refreshBlockedIps$ = new BehaviorSubject(undefined);
31
+ constructor(relay, signer) {
32
+ this.relay = relay;
33
+ this.signer = signer;
34
+ this.log = this.log.extend(relay.url);
35
+ this.httpUrl = ensureHttpURL(relay.url);
36
+ }
37
+ /**
38
+ * Core request method that handles all RPC calls with NIP-98 authentication
39
+ */
40
+ async request(method, params) {
41
+ const requestBody = {
42
+ method,
43
+ params,
44
+ };
45
+ const requestBodyString = JSON.stringify(requestBody);
46
+ // Generate NIP-98 token using getToken from nostr-tools
47
+ const authHeader = await getToken(this.httpUrl, "POST", (event) => this.signer.signEvent(event), true, // includeAuthorizationScheme - returns "Nostr <token>" format
48
+ requestBody);
49
+ // Make the HTTP request
50
+ const response = await fetch(this.httpUrl, {
51
+ method: "POST",
52
+ headers: {
53
+ "Content-Type": "application/nostr+json+rpc",
54
+ Authorization: authHeader,
55
+ },
56
+ body: requestBodyString,
57
+ });
58
+ // Handle HTTP errors
59
+ if (!response.ok) {
60
+ if (response.status === 401) {
61
+ throw new RelayManagementError("Unauthorized: Invalid or missing NIP-98 authentication", method, response.status);
62
+ }
63
+ const errorText = await response.text().catch(() => "Unknown error");
64
+ throw new RelayManagementError(`HTTP ${response.status}: ${errorText}`, method, response.status);
65
+ }
66
+ // Parse the response
67
+ const data = await response.json();
68
+ // Handle RPC errors
69
+ if (data.error) {
70
+ throw new RelayManagementError(`RPC error: ${data.error}`, method, response.status);
71
+ }
72
+ return data.result;
73
+ }
74
+ // Convenience methods for each RPC call
75
+ /** Get list of supported methods */
76
+ async supportedMethods() {
77
+ return this.request("supportedmethods", []);
78
+ }
79
+ /** Ban a pubkey */
80
+ async banPubkey(pubkey, reason) {
81
+ const result = await this.request("banpubkey", reason ? [pubkey, reason] : [pubkey]);
82
+ this.#refreshBannedPubkeys$.next();
83
+ return result;
84
+ }
85
+ /** List all banned pubkeys */
86
+ async listBannedPubkeys() {
87
+ return this.request("listbannedpubkeys", []);
88
+ }
89
+ /** Allow a pubkey */
90
+ async allowPubkey(pubkey, reason) {
91
+ const result = await this.request("allowpubkey", reason ? [pubkey, reason] : [pubkey]);
92
+ this.#refreshAllowedPubkeys$.next();
93
+ this.#refreshBannedPubkeys$.next(); // Also refresh banned list in case it was unbanned
94
+ return result;
95
+ }
96
+ /** List all allowed pubkeys */
97
+ async listAllowedPubkeys() {
98
+ return this.request("listallowedpubkeys", []);
99
+ }
100
+ /** List events needing moderation */
101
+ async listEventsNeedingModeration() {
102
+ return this.request("listeventsneedingmoderation", []);
103
+ }
104
+ /** Allow an event */
105
+ async allowEvent(eventId, reason) {
106
+ const result = await this.request("allowevent", reason ? [eventId, reason] : [eventId]);
107
+ this.#refreshBannedEvents$.next(); // Also refresh banned list in case it was unbanned
108
+ this.#refreshEventsNeedingModeration$.next();
109
+ return result;
110
+ }
111
+ /** Ban an event */
112
+ async banEvent(eventId, reason) {
113
+ const result = await this.request("banevent", reason ? [eventId, reason] : [eventId]);
114
+ this.#refreshBannedEvents$.next();
115
+ this.#refreshEventsNeedingModeration$.next();
116
+ return result;
117
+ }
118
+ /** List all banned events */
119
+ async listBannedEvents() {
120
+ return this.request("listbannedevents", []);
121
+ }
122
+ /** Change relay name */
123
+ async changeRelayName(name) {
124
+ return this.request("changerelayname", [name]);
125
+ }
126
+ /** Change relay description */
127
+ async changeRelayDescription(description) {
128
+ return this.request("changerelaydescription", [description]);
129
+ }
130
+ /** Change relay icon */
131
+ async changeRelayIcon(iconUrl) {
132
+ return this.request("changerelayicon", [iconUrl]);
133
+ }
134
+ /** Allow a kind */
135
+ async allowKind(kind) {
136
+ const result = await this.request("allowkind", [kind]);
137
+ this.#refreshAllowedKinds$.next();
138
+ return result;
139
+ }
140
+ /** Disallow a kind */
141
+ async disallowKind(kind) {
142
+ const result = await this.request("disallowkind", [kind]);
143
+ this.#refreshAllowedKinds$.next();
144
+ return result;
145
+ }
146
+ /** List all allowed kinds */
147
+ async listAllowedKinds() {
148
+ return this.request("listallowedkinds", []);
149
+ }
150
+ /** Block an IP address */
151
+ async blockIp(ip, reason) {
152
+ const result = await this.request("blockip", reason ? [ip, reason] : [ip]);
153
+ this.#refreshBlockedIps$.next();
154
+ return result;
155
+ }
156
+ /** Unblock an IP address */
157
+ async unblockIp(ip) {
158
+ const result = await this.request("unblockip", [ip]);
159
+ this.#refreshBlockedIps$.next();
160
+ return result;
161
+ }
162
+ /** List all blocked IPs */
163
+ async listBlockedIps() {
164
+ return this.request("listblockedips", []);
165
+ }
166
+ // Reactive observables for list methods
167
+ /** Observable that emits supported methods when subscribed */
168
+ supportMethods$ = this.#refreshSupportMethods$.pipe(switchMap(() => from(this.supportedMethods())), catchError((error) => {
169
+ this.log("Error fetching supported methods:", error);
170
+ return throwError(() => error);
171
+ }), shareReplay(1));
172
+ /** Observable that emits banned pubkeys when subscribed */
173
+ bannedPubkeys$ = this.#refreshBannedPubkeys$.pipe(switchMap(() => from(this.listBannedPubkeys())), catchError((error) => {
174
+ this.log("Error fetching banned pubkeys:", error);
175
+ return throwError(() => error);
176
+ }), shareReplay(1));
177
+ /** Observable that emits allowed pubkeys when subscribed */
178
+ allowedPubkeys$ = this.#refreshAllowedPubkeys$.pipe(switchMap(() => from(this.listAllowedPubkeys())), catchError((error) => {
179
+ this.log("Error fetching allowed pubkeys:", error);
180
+ return throwError(() => error);
181
+ }), shareReplay(1));
182
+ /** Observable that emits events needing moderation when subscribed */
183
+ eventsNeedingModeration$ = this.#refreshEventsNeedingModeration$.pipe(switchMap(() => from(this.listEventsNeedingModeration())), catchError((error) => {
184
+ this.log("Error fetching events needing moderation:", error);
185
+ return throwError(() => error);
186
+ }), shareReplay(1));
187
+ /** Observable that emits banned events when subscribed */
188
+ bannedEvents$ = this.#refreshBannedEvents$.pipe(switchMap(() => from(this.listBannedEvents())), catchError((error) => {
189
+ this.log("Error fetching banned events:", error);
190
+ return throwError(() => error);
191
+ }), shareReplay(1));
192
+ /** Observable that emits allowed kinds when subscribed */
193
+ allowedKinds$ = this.#refreshAllowedKinds$.pipe(switchMap(() => from(this.listAllowedKinds())), catchError((error) => {
194
+ this.log("Error fetching allowed kinds:", error);
195
+ return throwError(() => error);
196
+ }), shareReplay(1));
197
+ /** Observable that emits blocked IPs when subscribed */
198
+ blockedIps$ = this.#refreshBlockedIps$.pipe(switchMap(() => from(this.listBlockedIps())), catchError((error) => {
199
+ this.log("Error fetching blocked IPs:", error);
200
+ return throwError(() => error);
201
+ }), shareReplay(1));
202
+ }
package/dist/pool.d.ts CHANGED
@@ -10,6 +10,8 @@ export declare class RelayPool implements IPool {
10
10
  options?: RelayOptions | undefined;
11
11
  relays$: BehaviorSubject<Map<string, Relay>>;
12
12
  get relays(): Map<string, Relay>;
13
+ /** Whether to ignore relays that are ready=false */
14
+ ignoreOffline: boolean;
13
15
  /** A signal when a relay is added */
14
16
  add$: Subject<IRelay>;
15
17
  /** A signal when a relay is removed */
@@ -18,7 +20,7 @@ export declare class RelayPool implements IPool {
18
20
  /** Get or create a new relay connection */
19
21
  relay(url: string): Relay;
20
22
  /** Create a group of relays */
21
- group(relays: IPoolRelayInput): RelayGroup;
23
+ group(relays: IPoolRelayInput, ignoreOffline?: boolean): RelayGroup;
22
24
  /** Removes a relay from the pool and defaults to closing the connection */
23
25
  remove(relay: string | IRelay, close?: boolean): void;
24
26
  /** Make a REQ to multiple relays that does not deduplicate events */
package/dist/pool.js CHANGED
@@ -10,6 +10,8 @@ export class RelayPool {
10
10
  get relays() {
11
11
  return this.relays$.value;
12
12
  }
13
+ /** Whether to ignore relays that are ready=false */
14
+ ignoreOffline = true;
13
15
  /** A signal when a relay is added */
14
16
  add$ = new Subject();
15
17
  /** A signal when a relay is removed */
@@ -29,14 +31,21 @@ export class RelayPool {
29
31
  relay = new Relay(url, this.options);
30
32
  this.relays.set(url, relay);
31
33
  this.relays$.next(this.relays);
32
- this.add$.next(relay);
33
34
  return relay;
34
35
  }
35
36
  /** Create a group of relays */
36
- group(relays) {
37
- return new RelayGroup(Array.isArray(relays)
37
+ group(relays, ignoreOffline = this.ignoreOffline) {
38
+ let input = Array.isArray(relays)
38
39
  ? relays.map((url) => this.relay(url))
39
- : relays.pipe(map((urls) => urls.map((url) => this.relay(url)))));
40
+ : relays.pipe(map((urls) => urls.map((url) => this.relay(url))));
41
+ if (ignoreOffline) {
42
+ // Filter out the offline relays
43
+ if (Array.isArray(input))
44
+ input = input.filter((relay) => relay.ready);
45
+ else
46
+ input = input.pipe(map((relays) => relays.filter((relay) => relay.ready)));
47
+ }
48
+ return new RelayGroup(input);
40
49
  }
41
50
  /** Removes a relay from the pool and defaults to closing the connection */
42
51
  remove(relay, close = true) {
@@ -59,15 +68,17 @@ export class RelayPool {
59
68
  }
60
69
  /** Make a REQ to multiple relays that does not deduplicate events */
61
70
  req(relays, filters, id) {
62
- return this.group(relays).req(filters, id);
71
+ // Never filter out offline relays in manual methods
72
+ return this.group(relays, false).req(filters, id);
63
73
  }
64
74
  /** Send an EVENT message to multiple relays */
65
75
  event(relays, event) {
66
- return this.group(relays).event(event);
76
+ // Never filter out offline relays in manual methods
77
+ return this.group(relays, false).event(event);
67
78
  }
68
79
  /** Negentropy sync event ids with the relays and an event store */
69
80
  negentropy(relays, store, filter, reconcile, opts) {
70
- return this.group(relays).negentropy(store, filter, reconcile, opts);
81
+ return this.group(relays, false).negentropy(store, filter, reconcile, opts);
71
82
  }
72
83
  /** Publish an event to multiple relays */
73
84
  publish(relays, event, opts) {
package/dist/relay.d.ts CHANGED
@@ -25,13 +25,20 @@ export type RelayOptions = {
25
25
  publishTimeout?: number;
26
26
  /** How long to keep the connection alive after nothing is subscribed (default 30s) */
27
27
  keepAlive?: number;
28
+ /** Default retry config for subscription() method */
29
+ subscriptionRetry?: RetryConfig;
30
+ /** Default retry config for request() method */
31
+ requestRetry?: RetryConfig;
32
+ /** Default retry config for publish() method */
33
+ publishRetry?: RetryConfig;
28
34
  };
29
35
  export declare class Relay implements IRelay {
36
+ #private;
30
37
  url: string;
31
38
  protected log: typeof logger;
32
39
  protected socket: WebSocketSubject<any>;
33
40
  /** Whether the relay is ready for subscriptions or event publishing. setting this to false will cause all .req and .event observables to hang until the relay is ready */
34
- protected ready$: BehaviorSubject<boolean>;
41
+ ready$: Observable<boolean>;
35
42
  /** A method that returns an Observable that emits when the relay should reconnect */
36
43
  reconnectTimer: (error: CloseEvent | Error, attempts: number) => Observable<number>;
37
44
  /** How many times the relay has tried to reconnect */
@@ -61,6 +68,8 @@ export declare class Relay implements IRelay {
61
68
  /** An observable that emits the NIP-11 information document for the relay */
62
69
  information$: Observable<RelayInformation | null>;
63
70
  protected _nip11: RelayInformation | null;
71
+ /** An observable that emits the icon URL for the relay, or the favicon.ico URL for the relay */
72
+ icon$: Observable<string | undefined>;
64
73
  /** An observable that emits the limitations for the relay */
65
74
  limitations$: Observable<RelayInformation["limitation"] | null>;
66
75
  /** An array of supported NIPs from the NIP-11 information document */
@@ -71,6 +80,7 @@ export declare class Relay implements IRelay {
71
80
  close$: Subject<CloseEvent>;
72
81
  /** An observable that emits when underlying websocket is closing due to unsubscription */
73
82
  closing$: Subject<void>;
83
+ get ready(): boolean;
74
84
  get connected(): boolean;
75
85
  get challenge(): string | null;
76
86
  get notices(): string[];
@@ -85,6 +95,12 @@ export declare class Relay implements IRelay {
85
95
  publishTimeout: number;
86
96
  /** How long to keep the connection alive after nothing is subscribed (default 30s) */
87
97
  keepAlive: number;
98
+ /** Default retry config for subscription() method */
99
+ protected subscriptionReconnect: RetryConfig;
100
+ /** Default retry config for request() method */
101
+ protected requestReconnect: RetryConfig;
102
+ /** Default retry config for publish() method */
103
+ protected publishRetry: RetryConfig;
88
104
  protected receivedAuthRequiredForReq: BehaviorSubject<boolean>;
89
105
  protected receivedAuthRequiredForEvent: BehaviorSubject<boolean>;
90
106
  authRequiredForRead$: Observable<boolean>;
package/dist/relay.js CHANGED
@@ -8,7 +8,12 @@ import { webSocket } from "rxjs/webSocket";
8
8
  import { completeOnEose } from "./operators/complete-on-eose.js";
9
9
  import { markFromRelay } from "./operators/mark-from-relay.js";
10
10
  const AUTH_REQUIRED_PREFIX = "auth-required:";
11
- const DEFAULT_RETRY_CONFIG = { count: 10, delay: 1000, resetOnSuccess: true };
11
+ /** Default retry config for all methods */
12
+ const DEFAULT_RETRY_CONFIG = {
13
+ count: 3,
14
+ delay: 1000,
15
+ resetOnSuccess: true,
16
+ };
12
17
  /** Flags for the negentropy sync type */
13
18
  export var SyncDirection;
14
19
  (function (SyncDirection) {
@@ -23,8 +28,10 @@ export class Relay {
23
28
  url;
24
29
  log = logger.extend("Relay");
25
30
  socket;
31
+ /** Internal subject that tracks the ready state of the relay */
32
+ #ready$ = new BehaviorSubject(true);
26
33
  /** Whether the relay is ready for subscriptions or event publishing. setting this to false will cause all .req and .event observables to hang until the relay is ready */
27
- ready$ = new BehaviorSubject(true);
34
+ ready$ = this.#ready$.asObservable();
28
35
  /** A method that returns an Observable that emits when the relay should reconnect */
29
36
  reconnectTimer;
30
37
  /** How many times the relay has tried to reconnect */
@@ -54,6 +61,8 @@ export class Relay {
54
61
  /** An observable that emits the NIP-11 information document for the relay */
55
62
  information$;
56
63
  _nip11 = null;
64
+ /** An observable that emits the icon URL for the relay, or the favicon.ico URL for the relay */
65
+ icon$;
57
66
  /** An observable that emits the limitations for the relay */
58
67
  limitations$;
59
68
  /** An array of supported NIPs from the NIP-11 information document */
@@ -65,6 +74,9 @@ export class Relay {
65
74
  /** An observable that emits when underlying websocket is closing due to unsubscription */
66
75
  closing$ = new Subject();
67
76
  // sync state
77
+ get ready() {
78
+ return this.#ready$.value;
79
+ }
68
80
  get connected() {
69
81
  return this.connected$.value;
70
82
  }
@@ -91,6 +103,12 @@ export class Relay {
91
103
  publishTimeout = 30_000;
92
104
  /** How long to keep the connection alive after nothing is subscribed (default 30s) */
93
105
  keepAlive = 30_000;
106
+ /** Default retry config for subscription() method */
107
+ subscriptionReconnect;
108
+ /** Default retry config for request() method */
109
+ requestReconnect;
110
+ /** Default retry config for publish() method */
111
+ publishRetry;
94
112
  // Subjects that track if an "auth-required" message has been received for REQ or EVENT
95
113
  receivedAuthRequiredForReq = new BehaviorSubject(false);
96
114
  receivedAuthRequiredForEvent = new BehaviorSubject(false);
@@ -124,6 +142,10 @@ export class Relay {
124
142
  this.publishTimeout = opts.publishTimeout;
125
143
  if (opts?.keepAlive !== undefined)
126
144
  this.keepAlive = opts.keepAlive;
145
+ // Set retry configs
146
+ this.subscriptionReconnect = { ...DEFAULT_RETRY_CONFIG, ...(opts?.subscriptionRetry ?? {}) };
147
+ this.requestReconnect = { ...DEFAULT_RETRY_CONFIG, ...(opts?.requestRetry ?? {}) };
148
+ this.publishRetry = { ...DEFAULT_RETRY_CONFIG, ...(opts?.publishRetry ?? {}) };
127
149
  // Create an observable that tracks boolean authentication state
128
150
  this.authenticated$ = this.authenticationResponse$.pipe(map((response) => response?.ok === true));
129
151
  /** Use the static method to create a new reconnect method for this relay */
@@ -134,12 +156,20 @@ export class Relay {
134
156
  this.connected$.next(true);
135
157
  this.attempts$.next(0);
136
158
  this.error$.next(null);
159
+ // Reset to clean state
137
160
  this.resetState();
138
161
  });
139
162
  this.close$.subscribe((event) => {
140
- this.log("Disconnected");
141
- this.connected$.next(false);
163
+ if (this.connected$.value)
164
+ this.log("Disconnected");
165
+ else
166
+ this.log("Failed to connect");
167
+ // Chnaged the connected state to false
168
+ if (this.connected$.value)
169
+ this.connected$.next(false);
170
+ // Increment the attempts counter
142
171
  this.attempts$.next(this.attempts$.value + 1);
172
+ // Reset the state
143
173
  this.resetState();
144
174
  // Start the reconnect timer if the connection was not closed cleanly
145
175
  if (!event.wasClean)
@@ -165,6 +195,7 @@ export class Relay {
165
195
  shareReplay(1));
166
196
  this.limitations$ = this.information$.pipe(map((info) => (info ? info.limitation : null)));
167
197
  this.supported$ = this.information$.pipe(map((info) => info && Array.isArray(info.supported_nips) ? info.supported_nips.filter((n) => typeof n === "number") : null));
198
+ this.icon$ = this.information$.pipe(map((info) => info?.icon || new URL("/favicon.ico", ensureHttpURL(this.url)).toString()));
168
199
  // Create observables that track if auth is required for REQ or EVENT
169
200
  this.authRequiredForRead$ = this.receivedAuthRequiredForReq;
170
201
  this.authRequiredForPublish$ = this.receivedAuthRequiredForEvent;
@@ -226,13 +257,13 @@ export class Relay {
226
257
  }
227
258
  /** Set ready = false and start the reconnect timer */
228
259
  startReconnectTimer(error) {
229
- if (!this.ready$.value)
260
+ if (!this.ready)
230
261
  return;
231
262
  this.error$.next(error instanceof Error ? error : new Error("Connection error"));
232
- this.ready$.next(false);
263
+ this.#ready$.next(false);
233
264
  this.reconnectTimer(error, this.attempts$.value)
234
265
  .pipe(take(1))
235
- .subscribe(() => this.ready$.next(true));
266
+ .subscribe(() => this.#ready$.next(true));
236
267
  }
237
268
  /** Wait for authentication state, make connection and then wait for authentication if required */
238
269
  waitForAuth(
@@ -251,7 +282,7 @@ export class Relay {
251
282
  /** Wait for the relay to be ready to accept connections */
252
283
  waitForReady(observable) {
253
284
  // Don't wait if the relay is already ready
254
- if (this.ready$.value)
285
+ if (this.ready)
255
286
  return observable;
256
287
  else
257
288
  return this.ready$.pipe(
@@ -466,7 +497,7 @@ export class Relay {
466
497
  subscription(filters, opts) {
467
498
  return this.req(filters, opts?.id).pipe(
468
499
  // Retry on connection errors
469
- this.customRetryOperator(opts?.retries ?? opts?.reconnect ?? true, DEFAULT_RETRY_CONFIG),
500
+ this.customRetryOperator(opts?.reconnect ?? true, this.subscriptionReconnect),
470
501
  // Create resubscribe logic (repeat operator)
471
502
  this.customRepeatOperator(opts?.resubscribe),
472
503
  // Single subscription
@@ -476,7 +507,7 @@ export class Relay {
476
507
  request(filters, opts) {
477
508
  return this.req(filters, opts?.id).pipe(
478
509
  // Retry on connection errors
479
- this.customRetryOperator(opts?.retries ?? opts?.reconnect ?? true, DEFAULT_RETRY_CONFIG),
510
+ this.customRetryOperator(opts?.reconnect ?? true, this.requestReconnect),
480
511
  // Create resubscribe logic (repeat operator)
481
512
  this.customRepeatOperator(opts?.resubscribe),
482
513
  // Complete when EOSE is received
@@ -493,7 +524,7 @@ export class Relay {
493
524
  return of(result);
494
525
  }),
495
526
  // Retry the publish until it succeeds or the number of retries is reached
496
- this.customRetryOperator(opts?.retries ?? opts?.reconnect ?? true, DEFAULT_RETRY_CONFIG),
527
+ this.customRetryOperator(opts?.retries ?? opts?.reconnect ?? true, this.publishRetry),
497
528
  // Add timeout for publishing
498
529
  this.customTimeoutOperator(opts?.timeout, this.publishTimeout)));
499
530
  }
package/dist/types.d.ts CHANGED
@@ -19,14 +19,10 @@ export type CountResponse = {
19
19
  export type MultiplexWebSocket<T = any> = Pick<WebSocketSubject<T>, "multiplex">;
20
20
  /** Options for the publish method on the pool and relay */
21
21
  export type PublishOptions = {
22
+ /** Number of times to retry the publish. default is 3 */
23
+ retries?: boolean | number | Parameters<typeof retry>[0];
22
24
  /**
23
- * Number of times to retry the publish. default is 10
24
- * @see https://rxjs.dev/api/index/function/retry
25
- * @deprecated use `reconnect` instead
26
- */
27
- retries?: number | Parameters<typeof retry>[0];
28
- /**
29
- * Whether to reconnect when socket fails to connect. default is true (10 retries with 1 second delay)
25
+ * Whether to reconnect when socket fails to connect. default is true (3 retries with 1 second delay)
30
26
  * @see https://rxjs.dev/api/index/function/retry
31
27
  */
32
28
  reconnect?: boolean | number | Parameters<typeof retry>[0];
@@ -39,19 +35,13 @@ export type RequestOptions = SubscriptionOptions;
39
35
  export type SubscriptionOptions = {
40
36
  /** Custom REQ id for the subscription */
41
37
  id?: string;
42
- /**
43
- * Number of times to retry the subscription if the relay fails to connect. default is 10
44
- * @see https://rxjs.dev/api/index/function/retry
45
- * @deprecated use `reconnect` instead
46
- */
47
- retries?: number | Parameters<typeof retry>[0];
48
38
  /**
49
39
  * Whether to resubscribe if the subscription is closed by the relay. default is false
50
40
  * @see https://rxjs.dev/api/index/function/repeat
51
41
  */
52
42
  resubscribe?: boolean | number | Parameters<typeof repeat>[0];
53
43
  /**
54
- * Whether to reconnect when socket is closed. default is true (10 retries with 1 second delay)
44
+ * Whether to reconnect when socket is closed. default is true (3 retries with 1 second delay)
55
45
  * @see https://rxjs.dev/api/index/function/retry
56
46
  */
57
47
  reconnect?: boolean | number | Parameters<typeof retry>[0];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-relay",
3
- "version": "0.0.0-next-20251220152312",
3
+ "version": "0.0.0-next-20251231055351",
4
4
  "description": "nostr relay communication framework built on rxjs",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -57,14 +57,14 @@
57
57
  },
58
58
  "dependencies": {
59
59
  "@noble/hashes": "^1.7.1",
60
- "applesauce-core": "0.0.0-next-20251220152312",
60
+ "applesauce-core": "0.0.0-next-20251231055351",
61
61
  "nanoid": "^5.0.9",
62
62
  "nostr-tools": "~2.18",
63
63
  "rxjs": "^7.8.1"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@hirez_io/observer-spy": "^2.2.0",
67
- "applesauce-signers": "0.0.0-next-20251220152312",
67
+ "applesauce-signers": "0.0.0-next-20251231055351",
68
68
  "rimraf": "^6.0.1",
69
69
  "typescript": "^5.7.3",
70
70
  "vitest": "^4.0.15",