applesauce-relay 4.4.2 → 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";
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,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,15 @@ export class Relay {
226
257
  }
227
258
  /** Set ready = false and start the reconnect timer */
228
259
  startReconnectTimer(error) {
229
- if (!this.ready$.value)
260
+ if (!this.ready)
230
261
  return;
231
262
  this.error$.next(error instanceof Error ? error : new Error("Connection error"));
232
- this.ready$.next(false);
263
+ this._ready$.next(false);
233
264
  this.reconnectTimer(error, this.attempts$.value)
234
265
  .pipe(take(1))
235
- .subscribe(() => this.ready$.next(true));
266
+ .subscribe(() => {
267
+ this._ready$.next(true);
268
+ });
236
269
  }
237
270
  /** Wait for authentication state, make connection and then wait for authentication if required */
238
271
  waitForAuth(
@@ -251,7 +284,7 @@ export class Relay {
251
284
  /** Wait for the relay to be ready to accept connections */
252
285
  waitForReady(observable) {
253
286
  // Don't wait if the relay is already ready
254
- if (this.ready$.value)
287
+ if (this.ready)
255
288
  return observable;
256
289
  else
257
290
  return this.ready$.pipe(
@@ -410,7 +443,7 @@ export class Relay {
410
443
  authenticate(signer) {
411
444
  if (!this.challenge)
412
445
  throw new Error("Have not received authentication challenge");
413
- const p = signer.signEvent(nip42.makeAuthEvent(this.url, this.challenge));
446
+ const p = signer.signEvent(makeAuthEvent(this.url, this.challenge));
414
447
  const start = p instanceof Promise ? from(p) : of(p);
415
448
  return lastValueFrom(start.pipe(switchMap((event) => this.auth(event))));
416
449
  }
@@ -466,7 +499,7 @@ export class Relay {
466
499
  subscription(filters, opts) {
467
500
  return this.req(filters, opts?.id).pipe(
468
501
  // Retry on connection errors
469
- this.customRetryOperator(opts?.retries ?? opts?.reconnect ?? true, DEFAULT_RETRY_CONFIG),
502
+ this.customRetryOperator(opts?.reconnect ?? true, this.subscriptionReconnect),
470
503
  // Create resubscribe logic (repeat operator)
471
504
  this.customRepeatOperator(opts?.resubscribe),
472
505
  // Single subscription
@@ -476,7 +509,7 @@ export class Relay {
476
509
  request(filters, opts) {
477
510
  return this.req(filters, opts?.id).pipe(
478
511
  // Retry on connection errors
479
- this.customRetryOperator(opts?.retries ?? opts?.reconnect ?? true, DEFAULT_RETRY_CONFIG),
512
+ this.customRetryOperator(opts?.reconnect ?? true, this.requestReconnect),
480
513
  // Create resubscribe logic (repeat operator)
481
514
  this.customRepeatOperator(opts?.resubscribe),
482
515
  // Complete when EOSE is received
@@ -493,7 +526,7 @@ export class Relay {
493
526
  return of(result);
494
527
  }),
495
528
  // Retry the publish until it succeeds or the number of retries is reached
496
- this.customRetryOperator(opts?.retries ?? opts?.reconnect ?? true, DEFAULT_RETRY_CONFIG),
529
+ this.customRetryOperator(opts?.retries ?? opts?.reconnect ?? true, this.publishRetry),
497
530
  // Add timeout for publishing
498
531
  this.customTimeoutOperator(opts?.timeout, this.publishTimeout)));
499
532
  }
@@ -525,7 +558,15 @@ export class Relay {
525
558
  }
526
559
  // Fetch missing events from the relay
527
560
  if (direction & SyncDirection.RECEIVE && need.length > 0) {
528
- 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))));
529
570
  }
530
571
  }, { signal: controller.signal })
531
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.2",
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": {