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 +1 -0
- package/dist/index.js +1 -0
- package/dist/management.d.ts +169 -0
- package/dist/management.js +202 -0
- package/dist/pool.d.ts +3 -1
- package/dist/pool.js +18 -7
- package/dist/relay.d.ts +17 -1
- package/dist/relay.js +42 -11
- package/dist/types.d.ts +4 -14
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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$ =
|
|
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.
|
|
141
|
-
|
|
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
|
|
260
|
+
if (!this.ready)
|
|
230
261
|
return;
|
|
231
262
|
this.error$.next(error instanceof Error ? error : new Error("Connection error"));
|
|
232
|
-
this
|
|
263
|
+
this.#ready$.next(false);
|
|
233
264
|
this.reconnectTimer(error, this.attempts$.value)
|
|
234
265
|
.pipe(take(1))
|
|
235
|
-
.subscribe(() => this
|
|
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
|
|
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?.
|
|
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?.
|
|
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,
|
|
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
|
-
*
|
|
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 (
|
|
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-
|
|
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-
|
|
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-
|
|
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",
|