applesauce-relay 4.4.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/group.d.ts CHANGED
@@ -1,10 +1,9 @@
1
- import { type NostrEvent } from "nostr-tools";
1
+ import { IAsyncEventStoreActions, IEventStoreActions } from "applesauce-core/event-store";
2
+ import type { Filter, NostrEvent } from "applesauce-core/helpers";
2
3
  import { BehaviorSubject, MonoTypeOperatorFunction, Observable } from "rxjs";
3
- import { IAsyncEventStoreActions, IAsyncEventStoreRead, IEventStoreActions, IEventStoreRead } from "applesauce-core";
4
- import { type FilterWithAnd } from "applesauce-core/helpers";
5
4
  import { NegentropySyncOptions, type ReconcileFunction } from "./negentropy.js";
6
5
  import { SyncDirection } from "./relay.js";
7
- import { CountResponse, FilterInput, IGroup, IGroupRelayInput, IRelay, PublishOptions, PublishResponse, RequestOptions, SubscriptionOptions, SubscriptionResponse } from "./types.js";
6
+ import { CountResponse, FilterInput, IGroup, IGroupRelayInput, IRelay, NegentropyReadStore, NegentropySyncStore, PublishOptions, PublishResponse, RequestOptions, SubscriptionOptions, SubscriptionResponse } from "./types.js";
8
7
  /** Options for negentropy sync on a group of relays */
9
8
  export type GroupNegentropySyncOptions = NegentropySyncOptions & {
10
9
  /** Whether to sync in parallel (default true) */
@@ -47,7 +46,7 @@ export declare class RelayGroup implements IGroup {
47
46
  /** Send an event to all relays */
48
47
  event(event: NostrEvent): Observable<PublishResponse>;
49
48
  /** Negentropy sync events with the relays and an event store */
50
- negentropy(store: IEventStoreRead | IAsyncEventStoreRead | NostrEvent[], filter: FilterWithAnd, reconcile: ReconcileFunction, opts?: GroupNegentropySyncOptions): Promise<boolean>;
49
+ negentropy(store: NegentropyReadStore, filter: Filter, reconcile: ReconcileFunction, opts?: GroupNegentropySyncOptions): Promise<boolean>;
51
50
  /** Publish an event to all relays with retries ( default 3 retries ) */
52
51
  publish(event: NostrEvent, opts?: PublishOptions): Promise<PublishResponse[]>;
53
52
  /** Request events from all relays and complete on EOSE */
@@ -55,7 +54,7 @@ export declare class RelayGroup implements IGroup {
55
54
  /** Open a subscription to all relays with retries ( default 3 retries ) */
56
55
  subscription(filters: FilterInput, opts?: GroupSubscriptionOptions): Observable<SubscriptionResponse>;
57
56
  /** Count events on all relays in the group */
58
- count(filters: FilterWithAnd | FilterWithAnd[], id?: string): Observable<Record<string, CountResponse>>;
57
+ count(filters: Filter | Filter[], id?: string): Observable<Record<string, CountResponse>>;
59
58
  /** Negentropy sync events with the relays and an event store */
60
- sync(store: IEventStoreRead | IAsyncEventStoreRead | NostrEvent[], filter: FilterWithAnd, direction?: SyncDirection): Observable<NostrEvent>;
59
+ sync(store: NegentropySyncStore | NostrEvent[], filter: Filter, direction?: SyncDirection): Observable<NostrEvent>;
61
60
  }
package/dist/group.js CHANGED
@@ -1,6 +1,7 @@
1
+ import { EventMemory } from "applesauce-core/event-store";
2
+ import { filterDuplicateEvents } from "applesauce-core/observable";
1
3
  import { nanoid } from "nanoid";
2
4
  import { BehaviorSubject, catchError, combineLatest, defaultIfEmpty, defer, endWith, filter, from, identity, ignoreElements, lastValueFrom, map, merge, of, scan, share, switchMap, take, takeWhile, toArray, } from "rxjs";
3
- import { EventMemory, filterDuplicateEvents, } from "applesauce-core";
4
5
  import { completeOnEose } from "./operators/complete-on-eose.js";
5
6
  import { onlyEvents } from "./operators/only-events.js";
6
7
  import { reverseSwitchMap } from "./operators/reverse-switch-map.js";
@@ -124,9 +125,7 @@ export class RelayGroup {
124
125
  upstream.set(relay, observable);
125
126
  }
126
127
  return merge(...observables);
127
- }),
128
- // Ensure a single upstream
129
- share());
128
+ }));
130
129
  }
131
130
  /**
132
131
  * Make a request to all relays
@@ -137,7 +136,9 @@ export class RelayGroup {
137
136
  }
138
137
  /** Send an event to all relays */
139
138
  event(event) {
140
- return this.internalPublish((relay) => relay.event(event));
139
+ return this.internalPublish((relay) => relay.event(event)).pipe(
140
+ // Ensure a single upstream subscription
141
+ share());
141
142
  }
142
143
  /** Negentropy sync events with the relays and an event store */
143
144
  async negentropy(store, filter, reconcile, opts) {
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";
@@ -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/sha256";
3
+ import { sha256 } from "@noble/hashes/sha2";
4
4
  const PROTOCOL_VERSION = 0x61; // Version 1
5
5
  const ID_SIZE = 32;
6
6
  const FINGERPRINT_SIZE = 16;
@@ -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
+ }
@@ -1,5 +1,5 @@
1
1
  import { IAsyncEventStoreRead, IEventStoreRead } from "applesauce-core";
2
- import { type FilterWithAnd } from "applesauce-core/helpers";
2
+ import { type Filter } from "applesauce-core/helpers";
3
3
  import { NegentropyStorageVector } from "./lib/negentropy.js";
4
4
  import { MultiplexWebSocket } from "./types.js";
5
5
  /**
@@ -15,7 +15,7 @@ export type NegentropySyncOptions = {
15
15
  signal?: AbortSignal;
16
16
  };
17
17
  /** Creates a NegentropyStorageVector from an event store and filter */
18
- export declare function buildStorageFromFilter(store: IEventStoreRead | IAsyncEventStoreRead, filter: FilterWithAnd): Promise<NegentropyStorageVector>;
18
+ export declare function buildStorageFromFilter(store: IEventStoreRead | IAsyncEventStoreRead, filter: Filter): Promise<NegentropyStorageVector>;
19
19
  /** Creates a NegentropyStorageVector from an array of items */
20
20
  export declare function buildStorageVector(items: {
21
21
  id: string;
@@ -28,4 +28,4 @@ export declare function buildStorageVector(items: {
28
28
  */
29
29
  export declare function negentropySync(storage: NegentropyStorageVector, socket: MultiplexWebSocket & {
30
30
  next: (msg: any) => void;
31
- }, filter: FilterWithAnd, reconcile: ReconcileFunction, opts?: NegentropySyncOptions): Promise<boolean>;
31
+ }, filter: Filter, reconcile: ReconcileFunction, opts?: NegentropySyncOptions): Promise<boolean>;
@@ -1,5 +1,5 @@
1
1
  import { MonoTypeOperatorFunction, OperatorFunction } from "rxjs";
2
- import { NostrEvent } from "nostr-tools";
2
+ import { NostrEvent } from "applesauce-core/helpers/event";
3
3
  import { SubscriptionResponse } from "../types.js";
4
4
  export declare function completeOnEose(includeEose: true): MonoTypeOperatorFunction<SubscriptionResponse>;
5
5
  export declare function completeOnEose(): OperatorFunction<SubscriptionResponse, NostrEvent>;
@@ -1,5 +1,5 @@
1
1
  import { OperatorFunction } from "rxjs";
2
- import { NostrEvent } from "nostr-tools";
2
+ import { NostrEvent } from "applesauce-core/helpers/event";
3
3
  import { SubscriptionResponse } from "../types.js";
4
4
  /** Filter subscription responses and only return the events */
5
5
  export declare function onlyEvents(): OperatorFunction<SubscriptionResponse, NostrEvent>;
@@ -1,6 +1,6 @@
1
1
  import { OperatorFunction } from "rxjs";
2
2
  import { IEventStore } from "applesauce-core";
3
- import { NostrEvent } from "nostr-tools";
3
+ import { NostrEvent } from "applesauce-core/helpers/event";
4
4
  import { SubscriptionResponse } from "../types.js";
5
5
  /**
6
6
  * Adds all events to event store and returns a deduplicated timeline when EOSE is received
package/dist/pool.d.ts CHANGED
@@ -1,15 +1,17 @@
1
- import type { IAsyncEventStoreRead, IEventStoreRead } from "applesauce-core";
2
- import { FilterMap, OutboxMap, type FilterWithAnd } from "applesauce-core/helpers";
3
- import { Filter, type NostrEvent } from "nostr-tools";
1
+ import type { NostrEvent } from "applesauce-core/helpers/event";
2
+ import { Filter } from "applesauce-core/helpers/filter";
3
+ import { FilterMap, OutboxMap } from "applesauce-core/helpers/relay-selection";
4
4
  import { BehaviorSubject, Observable, Subject } from "rxjs";
5
5
  import { RelayGroup } from "./group.js";
6
6
  import type { NegentropySyncOptions, ReconcileFunction } from "./negentropy.js";
7
7
  import { Relay, SyncDirection, type RelayOptions } from "./relay.js";
8
- import type { CountResponse, FilterInput, IPool, IPoolRelayInput, IRelay, PublishResponse, SubscriptionResponse } from "./types.js";
8
+ import type { CountResponse, FilterInput, IPool, IPoolRelayInput, IRelay, NegentropyReadStore, NegentropySyncStore, PublishResponse, SubscriptionResponse } from "./types.js";
9
9
  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 */
@@ -26,7 +28,7 @@ export declare class RelayPool implements IPool {
26
28
  /** Send an EVENT message to multiple relays */
27
29
  event(relays: IPoolRelayInput, event: NostrEvent): Observable<PublishResponse>;
28
30
  /** Negentropy sync event ids with the relays and an event store */
29
- negentropy(relays: IPoolRelayInput, store: IEventStoreRead | IAsyncEventStoreRead | NostrEvent[], filter: FilterWithAnd, reconcile: ReconcileFunction, opts?: NegentropySyncOptions): Promise<boolean>;
31
+ negentropy(relays: IPoolRelayInput, store: NegentropyReadStore, filter: Filter, reconcile: ReconcileFunction, opts?: NegentropySyncOptions): Promise<boolean>;
30
32
  /** Publish an event to multiple relays */
31
33
  publish(relays: IPoolRelayInput, event: Parameters<RelayGroup["publish"]>[0], opts?: Parameters<RelayGroup["publish"]>[1]): Promise<PublishResponse[]>;
32
34
  /** Request events from multiple relays */
@@ -38,7 +40,7 @@ export declare class RelayPool implements IPool {
38
40
  /** Open a subscription for an {@link OutboxMap} and filter */
39
41
  outboxSubscription(outboxes: OutboxMap | Observable<OutboxMap>, filter: Omit<Filter, "authors">, options?: Parameters<RelayGroup["subscription"]>[1]): Observable<SubscriptionResponse>;
40
42
  /** Count events on multiple relays */
41
- count(relays: IPoolRelayInput, filters: FilterWithAnd | FilterWithAnd[], id?: string): Observable<Record<string, CountResponse>>;
43
+ count(relays: IPoolRelayInput, filters: Filter | Filter[], id?: string): Observable<Record<string, CountResponse>>;
42
44
  /** Negentropy sync events with the relays and an event store */
43
- sync(relays: IPoolRelayInput, store: IEventStoreRead | IAsyncEventStoreRead | NostrEvent[], filter: FilterWithAnd, direction?: SyncDirection): Observable<NostrEvent>;
45
+ sync(relays: IPoolRelayInput, store: NegentropySyncStore | NostrEvent[], filter: Filter, direction?: SyncDirection): Observable<NostrEvent>;
44
46
  }
package/dist/pool.js CHANGED
@@ -1,4 +1,6 @@
1
- import { createFilterMap, isFilterEqual, normalizeURL, } from "applesauce-core/helpers";
1
+ import { isFilterEqual } from "applesauce-core/helpers/filter";
2
+ import { createFilterMap } from "applesauce-core/helpers/relay-selection";
3
+ import { normalizeURL } from "applesauce-core/helpers/url";
2
4
  import { BehaviorSubject, distinctUntilChanged, isObservable, map, of, Subject } from "rxjs";
3
5
  import { RelayGroup } from "./group.js";
4
6
  import { Relay } from "./relay.js";
@@ -8,6 +10,8 @@ export class RelayPool {
8
10
  get relays() {
9
11
  return this.relays$.value;
10
12
  }
13
+ /** Whether to ignore relays that are ready=false */
14
+ ignoreOffline = true;
11
15
  /** A signal when a relay is added */
12
16
  add$ = new Subject();
13
17
  /** A signal when a relay is removed */
@@ -27,14 +31,21 @@ export class RelayPool {
27
31
  relay = new Relay(url, this.options);
28
32
  this.relays.set(url, relay);
29
33
  this.relays$.next(this.relays);
30
- this.add$.next(relay);
31
34
  return relay;
32
35
  }
33
36
  /** Create a group of relays */
34
- group(relays) {
35
- return new RelayGroup(Array.isArray(relays)
37
+ group(relays, ignoreOffline = this.ignoreOffline) {
38
+ let input = Array.isArray(relays)
36
39
  ? relays.map((url) => this.relay(url))
37
- : 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);
38
49
  }
39
50
  /** Removes a relay from the pool and defaults to closing the connection */
40
51
  remove(relay, close = true) {
@@ -57,15 +68,17 @@ export class RelayPool {
57
68
  }
58
69
  /** Make a REQ to multiple relays that does not deduplicate events */
59
70
  req(relays, filters, id) {
60
- 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);
61
73
  }
62
74
  /** Send an EVENT message to multiple relays */
63
75
  event(relays, event) {
64
- return this.group(relays).event(event);
76
+ // Never filter out offline relays in manual methods
77
+ return this.group(relays, false).event(event);
65
78
  }
66
79
  /** Negentropy sync event ids with the relays and an event store */
67
80
  negentropy(relays, store, filter, reconcile, opts) {
68
- return this.group(relays).negentropy(store, filter, reconcile, opts);
81
+ return this.group(relays, false).negentropy(store, filter, reconcile, opts);
69
82
  }
70
83
  /** Publish an event to multiple relays */
71
84
  publish(relays, event, opts) {
package/dist/relay.d.ts CHANGED
@@ -1,11 +1,10 @@
1
- import { IAsyncEventStoreRead, IEventStoreRead, logger } from "applesauce-core";
2
- import { type FilterWithAnd } from "applesauce-core/helpers";
3
- import { type NostrEvent } from "nostr-tools";
4
- import { RelayInformation } from "nostr-tools/nip11";
1
+ import { logger } from "applesauce-core";
2
+ import { NostrEvent } from "applesauce-core/helpers/event";
3
+ import { Filter } from "applesauce-core/helpers/filter";
5
4
  import { BehaviorSubject, MonoTypeOperatorFunction, Observable, RepeatConfig, RetryConfig, Subject } from "rxjs";
6
5
  import { WebSocketSubject, WebSocketSubjectConfig } from "rxjs/webSocket";
7
6
  import { type NegentropySyncOptions, type ReconcileFunction } from "./negentropy.js";
8
- import { AuthSigner, CountResponse, FilterInput, IRelay, PublishOptions, PublishResponse, RequestOptions, SubscriptionOptions, SubscriptionResponse } from "./types.js";
7
+ import { AuthSigner, CountResponse, FilterInput, IRelay, NegentropyReadStore, NegentropySyncStore, PublishOptions, PublishResponse, RelayInformation, RequestOptions, SubscriptionOptions, SubscriptionResponse } from "./types.js";
9
8
  /** Flags for the negentropy sync type */
10
9
  export declare enum SyncDirection {
11
10
  RECEIVE = 1,
@@ -26,13 +25,21 @@ export type RelayOptions = {
26
25
  publishTimeout?: number;
27
26
  /** How long to keep the connection alive after nothing is subscribed (default 30s) */
28
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;
29
34
  };
30
35
  export declare class Relay implements IRelay {
31
36
  url: string;
32
37
  protected log: typeof logger;
33
38
  protected socket: WebSocketSubject<any>;
39
+ /** Internal subject that tracks the ready state of the relay */
40
+ protected _ready$: BehaviorSubject<boolean>;
34
41
  /** 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 */
35
- protected ready$: BehaviorSubject<boolean>;
42
+ ready$: Observable<boolean>;
36
43
  /** A method that returns an Observable that emits when the relay should reconnect */
37
44
  reconnectTimer: (error: CloseEvent | Error, attempts: number) => Observable<number>;
38
45
  /** How many times the relay has tried to reconnect */
@@ -62,6 +69,8 @@ export declare class Relay implements IRelay {
62
69
  /** An observable that emits the NIP-11 information document for the relay */
63
70
  information$: Observable<RelayInformation | null>;
64
71
  protected _nip11: RelayInformation | null;
72
+ /** An observable that emits the icon URL for the relay, or the favicon.ico URL for the relay */
73
+ icon$: Observable<string | undefined>;
65
74
  /** An observable that emits the limitations for the relay */
66
75
  limitations$: Observable<RelayInformation["limitation"] | null>;
67
76
  /** An array of supported NIPs from the NIP-11 information document */
@@ -72,6 +81,7 @@ export declare class Relay implements IRelay {
72
81
  close$: Subject<CloseEvent>;
73
82
  /** An observable that emits when underlying websocket is closing due to unsubscription */
74
83
  closing$: Subject<void>;
84
+ get ready(): boolean;
75
85
  get connected(): boolean;
76
86
  get challenge(): string | null;
77
87
  get notices(): string[];
@@ -86,6 +96,12 @@ export declare class Relay implements IRelay {
86
96
  publishTimeout: number;
87
97
  /** How long to keep the connection alive after nothing is subscribed (default 30s) */
88
98
  keepAlive: number;
99
+ /** Default retry config for subscription() method */
100
+ protected subscriptionReconnect: RetryConfig;
101
+ /** Default retry config for request() method */
102
+ protected requestReconnect: RetryConfig;
103
+ /** Default retry config for publish() method */
104
+ protected publishRetry: RetryConfig;
89
105
  protected receivedAuthRequiredForReq: BehaviorSubject<boolean>;
90
106
  protected receivedAuthRequiredForEvent: BehaviorSubject<boolean>;
91
107
  authRequiredForRead$: Observable<boolean>;
@@ -106,13 +122,13 @@ export declare class Relay implements IRelay {
106
122
  /** Create a REQ observable that emits events or "EOSE" or errors */
107
123
  req(filters: FilterInput, id?: string): Observable<SubscriptionResponse>;
108
124
  /** Create a COUNT observable that emits a single count response */
109
- count(filters: FilterWithAnd | FilterWithAnd[], id?: string): Observable<CountResponse>;
125
+ count(filters: Filter | Filter[], id?: string): Observable<CountResponse>;
110
126
  /** Send an EVENT or AUTH message and return an observable of PublishResponse that completes or errors */
111
127
  event(event: NostrEvent, verb?: "EVENT" | "AUTH"): Observable<PublishResponse>;
112
128
  /** send and AUTH message */
113
129
  auth(event: NostrEvent): Promise<PublishResponse>;
114
130
  /** Negentropy sync event ids with the relay and an event store */
115
- negentropy(store: IEventStoreRead | IAsyncEventStoreRead | NostrEvent[], filter: FilterWithAnd, reconcile: ReconcileFunction, opts?: NegentropySyncOptions): Promise<boolean>;
131
+ negentropy(store: NegentropyReadStore, filter: Filter, reconcile: ReconcileFunction, opts?: NegentropySyncOptions): Promise<boolean>;
116
132
  /** Authenticate with the relay using a signer */
117
133
  authenticate(signer: AuthSigner): Promise<PublishResponse>;
118
134
  /** Internal operator for creating the retry() operator */
@@ -130,7 +146,7 @@ export declare class Relay implements IRelay {
130
146
  /** Publishes an event to the relay and retries when relay errors or responds with auth-required ( default 3 retries ) */
131
147
  publish(event: NostrEvent, opts?: PublishOptions): Promise<PublishResponse>;
132
148
  /** Negentropy sync events with the relay and an event store */
133
- sync(store: IEventStoreRead | IAsyncEventStoreRead | NostrEvent[], filter: FilterWithAnd, direction?: SyncDirection): Observable<NostrEvent>;
149
+ sync(store: NegentropySyncStore, filter: Filter, direction?: SyncDirection): Observable<NostrEvent>;
134
150
  /** Force close the connection */
135
151
  close(): void;
136
152
  /** An async method that returns the NIP-11 information document for the relay */
package/dist/relay.js CHANGED
@@ -1,14 +1,19 @@
1
1
  import { logger } from "applesauce-core";
2
- import { ensureHttpURL } from "applesauce-core/helpers";
3
- import { simpleTimeout } from "applesauce-core/observable";
2
+ import { ensureHttpURL } from "applesauce-core/helpers/url";
3
+ import { mapEventsToStore, simpleTimeout } from "applesauce-core/observable";
4
4
  import { nanoid } from "nanoid";
5
- import { nip42 } from "nostr-tools";
5
+ import { makeAuthEvent } from "nostr-tools/nip42";
6
6
  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, Subject, switchMap, take, takeUntil, tap, throwError, timeout, timer, } from "rxjs";
7
7
  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,9 +195,17 @@ 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
- this.authRequiredForRead$ = this.receivedAuthRequiredForReq.pipe(tap((required) => required && this.log("Auth required for REQ")), shareReplay(1));
170
- this.authRequiredForPublish$ = this.receivedAuthRequiredForEvent.pipe(tap((required) => required && this.log("Auth required for EVENT")), shareReplay(1));
200
+ this.authRequiredForRead$ = this.receivedAuthRequiredForReq;
201
+ this.authRequiredForPublish$ = this.receivedAuthRequiredForEvent;
202
+ // Log when auth is required
203
+ this.authRequiredForRead$
204
+ .pipe(filter((r) => r === true), take(1))
205
+ .subscribe(() => this.log("Auth required for REQ"));
206
+ this.authRequiredForPublish$
207
+ .pipe(filter((r) => r === true), take(1))
208
+ .subscribe(() => this.log("Auth required for EVENT"));
171
209
  // Update the notices state
172
210
  const listenForNotice = this.socket.pipe(
173
211
  // listen for NOTICE messages
@@ -219,13 +257,15 @@ export class Relay {
219
257
  }
220
258
  /** Set ready = false and start the reconnect timer */
221
259
  startReconnectTimer(error) {
222
- if (!this.ready$.value)
260
+ if (!this.ready)
223
261
  return;
224
262
  this.error$.next(error instanceof Error ? error : new Error("Connection error"));
225
- this.ready$.next(false);
263
+ this._ready$.next(false);
226
264
  this.reconnectTimer(error, this.attempts$.value)
227
265
  .pipe(take(1))
228
- .subscribe(() => this.ready$.next(true));
266
+ .subscribe(() => {
267
+ this._ready$.next(true);
268
+ });
229
269
  }
230
270
  /** Wait for authentication state, make connection and then wait for authentication if required */
231
271
  waitForAuth(
@@ -244,7 +284,7 @@ export class Relay {
244
284
  /** Wait for the relay to be ready to accept connections */
245
285
  waitForReady(observable) {
246
286
  // Don't wait if the relay is already ready
247
- if (this.ready$.value)
287
+ if (this.ready)
248
288
  return observable;
249
289
  else
250
290
  return this.ready$.pipe(
@@ -320,9 +360,7 @@ export class Relay {
320
360
  /** Create a COUNT observable that emits a single count response */
321
361
  count(filters, id = nanoid()) {
322
362
  // Create an observable that filters responses from the relay to just the ones for this COUNT
323
- const messages = this.socket.pipe(filter((m) => Array.isArray(m) && (m[0] === "COUNT" || m[0] === "CLOSED") && m[1] === id),
324
- // Singleton (prevents duplicate subscriptions)
325
- share());
363
+ const messages = this.socket.pipe(filter((m) => Array.isArray(m) && (m[0] === "COUNT" || m[0] === "CLOSED") && m[1] === id));
326
364
  // Send the COUNT message and listen for response
327
365
  const observable = defer(() => {
328
366
  // Send the COUNT message when subscription starts
@@ -343,11 +381,10 @@ export class Relay {
343
381
  timeout({
344
382
  first: this.eoseTimeout,
345
383
  with: () => throwError(() => new Error("COUNT timeout")),
346
- }),
347
- // Only create one upstream subscription
348
- share());
384
+ }));
349
385
  // Start the watch tower and wait for auth if required
350
- return this.waitForReady(this.waitForAuth(this.authRequiredForRead$, observable));
386
+ // Use share() to prevent multiple subscriptions from creating duplicate COUNT messages
387
+ return this.waitForReady(this.waitForAuth(this.authRequiredForRead$, observable)).pipe(share());
351
388
  }
352
389
  /** Send an EVENT or AUTH message and return an observable of PublishResponse that completes or errors */
353
390
  event(event, verb = "EVENT") {
@@ -378,14 +415,13 @@ export class Relay {
378
415
  timeout({
379
416
  first: this.eventTimeout,
380
417
  with: () => of({ ok: false, from: this.url, message: "Timeout" }),
381
- }),
382
- // Only create one upstream subscription
383
- share());
418
+ }));
384
419
  // skip wait for auth if verb is AUTH
420
+ // Use share() to prevent multiple subscriptions from creating duplicate EVENT messages
385
421
  if (verb === "AUTH")
386
- return this.waitForReady(observable);
422
+ return this.waitForReady(observable).pipe(share());
387
423
  else
388
- return this.waitForReady(this.waitForAuth(this.authRequiredForPublish$, observable));
424
+ return this.waitForReady(this.waitForAuth(this.authRequiredForPublish$, observable)).pipe(share());
389
425
  }
390
426
  /** send and AUTH message */
391
427
  auth(event) {
@@ -407,7 +443,7 @@ export class Relay {
407
443
  authenticate(signer) {
408
444
  if (!this.challenge)
409
445
  throw new Error("Have not received authentication challenge");
410
- const p = signer.signEvent(nip42.makeAuthEvent(this.url, this.challenge));
446
+ const p = signer.signEvent(makeAuthEvent(this.url, this.challenge));
411
447
  const start = p instanceof Promise ? from(p) : of(p);
412
448
  return lastValueFrom(start.pipe(switchMap((event) => this.auth(event))));
413
449
  }
@@ -463,7 +499,7 @@ export class Relay {
463
499
  subscription(filters, opts) {
464
500
  return this.req(filters, opts?.id).pipe(
465
501
  // Retry on connection errors
466
- this.customRetryOperator(opts?.retries ?? opts?.reconnect ?? true, DEFAULT_RETRY_CONFIG),
502
+ this.customRetryOperator(opts?.reconnect ?? true, this.subscriptionReconnect),
467
503
  // Create resubscribe logic (repeat operator)
468
504
  this.customRepeatOperator(opts?.resubscribe),
469
505
  // Single subscription
@@ -473,7 +509,7 @@ export class Relay {
473
509
  request(filters, opts) {
474
510
  return this.req(filters, opts?.id).pipe(
475
511
  // Retry on connection errors
476
- this.customRetryOperator(opts?.retries ?? opts?.reconnect ?? true, DEFAULT_RETRY_CONFIG),
512
+ this.customRetryOperator(opts?.reconnect ?? true, this.requestReconnect),
477
513
  // Create resubscribe logic (repeat operator)
478
514
  this.customRepeatOperator(opts?.resubscribe),
479
515
  // Complete when EOSE is received
@@ -490,11 +526,9 @@ export class Relay {
490
526
  return of(result);
491
527
  }),
492
528
  // Retry the publish until it succeeds or the number of retries is reached
493
- this.customRetryOperator(opts?.retries ?? opts?.reconnect ?? true, DEFAULT_RETRY_CONFIG),
529
+ this.customRetryOperator(opts?.retries ?? opts?.reconnect ?? true, this.publishRetry),
494
530
  // Add timeout for publishing
495
- this.customTimeoutOperator(opts?.timeout, this.publishTimeout),
496
- // Single subscription
497
- share()));
531
+ this.customTimeoutOperator(opts?.timeout, this.publishTimeout)));
498
532
  }
499
533
  /** Negentropy sync events with the relay and an event store */
500
534
  sync(store, filter, direction = SyncDirection.RECEIVE) {
@@ -524,7 +558,15 @@ export class Relay {
524
558
  }
525
559
  // Fetch missing events from the relay
526
560
  if (direction & SyncDirection.RECEIVE && need.length > 0) {
527
- await lastValueFrom(this.req({ ids: need }).pipe(completeOnEose(), tap((event) => observer.next(event))));
561
+ await lastValueFrom(this.req({ ids: need }).pipe(
562
+ // Complete when EOSE is received
563
+ completeOnEose(),
564
+ // Add events to the store if its writable
565
+ Reflect.has(store, "add")
566
+ ? mapEventsToStore(store)
567
+ : identity,
568
+ // Pass events to observer
569
+ tap((event) => observer.next(event))));
528
570
  }
529
571
  }, { signal: controller.signal })
530
572
  // Complete the observable when the sync is complete
package/dist/types.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import type { IAsyncEventStoreRead, IEventStoreRead } from "applesauce-core";
2
- import type { FilterWithAnd } from "applesauce-core/helpers";
3
- import type { EventTemplate, NostrEvent } from "nostr-tools";
4
- import type { RelayInformation } from "nostr-tools/nip11";
1
+ import type { IAsyncEventStoreActions, IAsyncEventStoreRead, IEventStoreRead } from "applesauce-core/event-store";
2
+ import type { Filter } from "applesauce-core/helpers/filter";
3
+ import type { EventTemplate, NostrEvent } from "applesauce-core/helpers/event";
4
+ import type { RelayInformation as CoreRelayInformation } from "nostr-tools/nip11";
5
5
  import type { Observable, repeat, retry } from "rxjs";
6
6
  import type { WebSocketSubject } from "rxjs/webSocket";
7
7
  import type { GroupNegentropySyncOptions, GroupRequestOptions, GroupSubscriptionOptions } from "./group.js";
@@ -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];
@@ -60,7 +50,17 @@ export type AuthSigner = {
60
50
  signEvent: (event: EventTemplate) => NostrEvent | Promise<NostrEvent>;
61
51
  };
62
52
  /** Filters that can be passed to request methods on the pool or relay */
63
- export type FilterInput = FilterWithAnd | FilterWithAnd[] | Observable<FilterWithAnd | FilterWithAnd[]> | ((relay: IRelay) => FilterWithAnd | FilterWithAnd[] | Observable<FilterWithAnd | FilterWithAnd[]>);
53
+ export type FilterInput = Filter | Filter[] | Observable<Filter | Filter[]> | ((relay: IRelay) => Filter | Filter[] | Observable<Filter | Filter[]>);
54
+ export type RelayInformation = CoreRelayInformation & {
55
+ /** An array of attributes that describe the relay type/characteristics */
56
+ attributes?: string[];
57
+ };
58
+ /** A read only event store for negentropy sync */
59
+ export type NegentropyReadStore = IEventStoreRead | IAsyncEventStoreRead | NostrEvent[];
60
+ /** A writeable event store for negentropy sync */
61
+ export type NegentropyWriteStore = (IAsyncEventStoreRead & IAsyncEventStoreActions) | (IEventStoreRead & IAsyncEventStoreActions);
62
+ /** An event store that can be used for negentropy sync */
63
+ export type NegentropySyncStore = NegentropyReadStore | NegentropyWriteStore;
64
64
  export interface IRelay extends MultiplexWebSocket {
65
65
  url: string;
66
66
  message$: Observable<any>;
@@ -82,13 +82,13 @@ export interface IRelay extends MultiplexWebSocket {
82
82
  /** Send a REQ message */
83
83
  req(filters: FilterInput, id?: string): Observable<SubscriptionResponse>;
84
84
  /** Send a COUNT message */
85
- count(filters: FilterWithAnd | FilterWithAnd[], id?: string): Observable<CountResponse>;
85
+ count(filters: Filter | Filter[], id?: string): Observable<CountResponse>;
86
86
  /** Send an EVENT message */
87
87
  event(event: NostrEvent): Observable<PublishResponse>;
88
88
  /** Send an AUTH message */
89
89
  auth(event: NostrEvent): Promise<PublishResponse>;
90
90
  /** Negentropy sync event ids with the relay and an event store */
91
- negentropy(store: IEventStoreRead | IAsyncEventStoreRead | NostrEvent[], filter: FilterWithAnd, reconcile: ReconcileFunction, opts?: NegentropySyncOptions): Promise<boolean>;
91
+ negentropy(store: NegentropyReadStore, filter: Filter, reconcile: ReconcileFunction, opts?: NegentropySyncOptions): Promise<boolean>;
92
92
  /** Authenticate with the relay using a signer */
93
93
  authenticate(signer: AuthSigner): Promise<PublishResponse>;
94
94
  /** Send an EVENT message with retries */
@@ -98,7 +98,7 @@ export interface IRelay extends MultiplexWebSocket {
98
98
  /** Open a subscription with retries */
99
99
  subscription(filters: FilterInput, opts?: SubscriptionOptions): Observable<SubscriptionResponse>;
100
100
  /** Negentropy sync events with the relay and an event store */
101
- sync(store: IEventStoreRead | IAsyncEventStoreRead | NostrEvent[], filter: FilterWithAnd, direction?: SyncDirection): Observable<NostrEvent>;
101
+ sync(store: NegentropySyncStore, filter: Filter, direction?: SyncDirection): Observable<NostrEvent>;
102
102
  /** Get the NIP-11 information document for the relay */
103
103
  getInformation(): Promise<RelayInformation | null>;
104
104
  /** Get the limitations for the relay */
@@ -113,7 +113,7 @@ export interface IGroup {
113
113
  /** Send an EVENT message */
114
114
  event(event: Parameters<IRelay["event"]>[0]): Observable<PublishResponse>;
115
115
  /** Negentropy sync event ids with the relays and an event store */
116
- negentropy(store: IEventStoreRead | IAsyncEventStoreRead | NostrEvent[], filter: FilterWithAnd, reconcile: ReconcileFunction, opts?: NegentropySyncOptions): Promise<boolean>;
116
+ negentropy(store: NegentropyReadStore, filter: Filter, reconcile: ReconcileFunction, opts?: NegentropySyncOptions): Promise<boolean>;
117
117
  /** Add a relay to the group */
118
118
  add(relay: IRelay): void;
119
119
  /** Remove a relay from the group */
@@ -127,9 +127,9 @@ export interface IGroup {
127
127
  /** Open a subscription with retries */
128
128
  subscription(filters: Parameters<IRelay["subscription"]>[0], opts?: GroupSubscriptionOptions): Observable<SubscriptionResponse>;
129
129
  /** Count events on the relays and an event store */
130
- count(filters: FilterWithAnd | FilterWithAnd[], id?: string): Observable<Record<string, CountResponse>>;
130
+ count(filters: Filter | Filter[], id?: string): Observable<Record<string, CountResponse>>;
131
131
  /** Negentropy sync events with the relay and an event store */
132
- sync(store: IEventStoreRead | IAsyncEventStoreRead | NostrEvent[], filter: FilterWithAnd, direction?: SyncDirection): Observable<NostrEvent>;
132
+ sync(store: NegentropySyncStore, filter: Filter, direction?: SyncDirection): Observable<NostrEvent>;
133
133
  }
134
134
  /** Signals emitted by the pool */
135
135
  export interface IPoolSignals {
@@ -149,7 +149,7 @@ export interface IPool extends IPoolSignals {
149
149
  /** Send an EVENT message */
150
150
  event(relays: IPoolRelayInput, event: NostrEvent): Observable<PublishResponse>;
151
151
  /** Negentropy sync event ids with the relays and an event store */
152
- negentropy(relays: IPoolRelayInput, store: IEventStoreRead | IAsyncEventStoreRead | NostrEvent[], filter: FilterWithAnd, reconcile: ReconcileFunction, opts?: GroupNegentropySyncOptions): Promise<boolean>;
152
+ negentropy(relays: IPoolRelayInput, store: NegentropyReadStore, filter: Filter, reconcile: ReconcileFunction, opts?: GroupNegentropySyncOptions): Promise<boolean>;
153
153
  /** Send an EVENT message to relays with retries */
154
154
  publish(relays: IPoolRelayInput, event: Parameters<IGroup["publish"]>[0], opts?: Parameters<IGroup["publish"]>[1]): Promise<PublishResponse[]>;
155
155
  /** Send a REQ message to relays with retries */
@@ -157,7 +157,7 @@ export interface IPool extends IPoolSignals {
157
157
  /** Open a subscription to relays with retries */
158
158
  subscription(relays: IPoolRelayInput, filters: Parameters<IGroup["subscription"]>[0], opts?: Parameters<IGroup["subscription"]>[1]): Observable<SubscriptionResponse>;
159
159
  /** Count events on the relays and an event store */
160
- count(relays: IPoolRelayInput, filters: FilterWithAnd | FilterWithAnd[], id?: string): Observable<Record<string, CountResponse>>;
160
+ count(relays: IPoolRelayInput, filters: Filter | Filter[], id?: string): Observable<Record<string, CountResponse>>;
161
161
  /** Negentropy sync events with the relay and an event store */
162
- sync(relays: IPoolRelayInput, store: IEventStoreRead | IAsyncEventStoreRead | NostrEvent[], filter: FilterWithAnd, direction?: SyncDirection): Observable<NostrEvent>;
162
+ sync(relays: IPoolRelayInput, store: NegentropySyncStore, filter: Filter, direction?: SyncDirection): Observable<NostrEvent>;
163
163
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-relay",
3
- "version": "4.4.0",
3
+ "version": "5.0.0",
4
4
  "description": "nostr relay communication framework built on rxjs",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -24,6 +24,11 @@
24
24
  "require": "./dist/pool.js",
25
25
  "types": "./dist/pool.d.ts"
26
26
  },
27
+ "./group": {
28
+ "import": "./dist/group.js",
29
+ "require": "./dist/group.js",
30
+ "types": "./dist/group.d.ts"
31
+ },
27
32
  "./relay": {
28
33
  "import": "./dist/relay.js",
29
34
  "require": "./dist/relay.js",
@@ -52,17 +57,17 @@
52
57
  },
53
58
  "dependencies": {
54
59
  "@noble/hashes": "^1.7.1",
55
- "applesauce-core": "^4.4.0",
60
+ "applesauce-core": "^5.0.0",
56
61
  "nanoid": "^5.0.9",
57
- "nostr-tools": "~2.17",
62
+ "nostr-tools": "~2.19",
58
63
  "rxjs": "^7.8.1"
59
64
  },
60
65
  "devDependencies": {
61
66
  "@hirez_io/observer-spy": "^2.2.0",
62
- "applesauce-signers": "^4.2.0",
67
+ "applesauce-signers": "^5.0.0",
63
68
  "rimraf": "^6.0.1",
64
69
  "typescript": "^5.7.3",
65
- "vitest": "^3.2.4",
70
+ "vitest": "^4.0.15",
66
71
  "vitest-websocket-mock": "^0.5.0"
67
72
  },
68
73
  "funding": {