applesauce-relay 4.0.0 → 4.1.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/README.md +2 -3
- package/dist/group.d.ts +7 -2
- package/dist/group.js +7 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/liveness.d.ts +123 -0
- package/dist/liveness.js +327 -0
- package/dist/operators/index.d.ts +1 -0
- package/dist/operators/index.js +1 -0
- package/dist/operators/liveness.d.ts +17 -0
- package/dist/operators/liveness.js +47 -0
- package/dist/pool.d.ts +3 -1
- package/dist/pool.js +4 -0
- package/dist/relay.d.ts +5 -1
- package/dist/relay.js +51 -14
- package/dist/types.d.ts +12 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -10,16 +10,15 @@ npm install applesauce-relay
|
|
|
10
10
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
13
|
-
- [x] NIP-01
|
|
13
|
+
- [x] NIP-01 `REQ` and `EVENT` messages
|
|
14
14
|
- [x] Relay pool and groups
|
|
15
|
-
- [x] Fetch NIP-11 information before connecting
|
|
16
15
|
- [x] NIP-11 `auth_required` limitation
|
|
17
16
|
- [ ] NIP-11 `max_subscriptions` limitation
|
|
18
17
|
- [x] Client negentropy sync
|
|
19
18
|
- [x] Reconnection backoff logic
|
|
20
19
|
- [x] republish event on reconnect and auth-required
|
|
21
20
|
- [x] Resubscribe on reconnect and auth-required
|
|
22
|
-
- [
|
|
21
|
+
- [x] NIP-45 COUNT
|
|
23
22
|
|
|
24
23
|
## Examples
|
|
25
24
|
|
package/dist/group.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Filter, type NostrEvent } from "nostr-tools";
|
|
|
2
2
|
import { Observable } from "rxjs";
|
|
3
3
|
import { IAsyncEventStoreActions, IAsyncEventStoreRead, IEventStoreActions, IEventStoreRead } from "applesauce-core";
|
|
4
4
|
import { NegentropySyncOptions, type ReconcileFunction } from "./negentropy.js";
|
|
5
|
-
import { FilterInput, IGroup, IRelay, PublishOptions, PublishResponse, RequestOptions, SubscriptionOptions, SubscriptionResponse } from "./types.js";
|
|
5
|
+
import { CountResponse, FilterInput, IGroup, IRelay, PublishOptions, PublishResponse, RequestOptions, SubscriptionOptions, SubscriptionResponse } from "./types.js";
|
|
6
6
|
import { SyncDirection } from "./relay.js";
|
|
7
7
|
/** Options for negentropy sync on a group of relays */
|
|
8
8
|
export type GroupNegentropySyncOptions = NegentropySyncOptions & {
|
|
@@ -28,7 +28,10 @@ export declare class RelayGroup implements IGroup {
|
|
|
28
28
|
* Make a request to all relays
|
|
29
29
|
* @note This does not deduplicate events
|
|
30
30
|
*/
|
|
31
|
-
req(filters: FilterInput, id?: string
|
|
31
|
+
req(filters: FilterInput, id?: string, opts?: {
|
|
32
|
+
/** Deduplicate events with an event store (default is a temporary instance of EventMemory), null will disable deduplication */
|
|
33
|
+
eventStore?: IEventStoreActions | IAsyncEventStoreActions | null;
|
|
34
|
+
}): Observable<SubscriptionResponse>;
|
|
32
35
|
/** Send an event to all relays */
|
|
33
36
|
event(event: NostrEvent): Observable<PublishResponse>;
|
|
34
37
|
/** Negentropy sync events with the relays and an event store */
|
|
@@ -39,6 +42,8 @@ export declare class RelayGroup implements IGroup {
|
|
|
39
42
|
request(filters: FilterInput, opts?: GroupRequestOptions): Observable<NostrEvent>;
|
|
40
43
|
/** Open a subscription to all relays with retries ( default 3 retries ) */
|
|
41
44
|
subscription(filters: FilterInput, opts?: GroupSubscriptionOptions): Observable<SubscriptionResponse>;
|
|
45
|
+
/** Count events on all relays in the group */
|
|
46
|
+
count(filters: Filter | Filter[], id?: string): Observable<Record<string, CountResponse>>;
|
|
42
47
|
/** Negentropy sync events with the relays and an event store */
|
|
43
48
|
sync(store: IEventStoreRead | IAsyncEventStoreRead | NostrEvent[], filter: Filter, direction?: SyncDirection): Observable<NostrEvent>;
|
|
44
49
|
}
|
package/dist/group.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { nanoid } from "nanoid";
|
|
2
|
-
import { catchError, defer, EMPTY, endWith, identity, ignoreElements, merge, of, share, switchMap, } from "rxjs";
|
|
2
|
+
import { catchError, combineLatest, defer, EMPTY, endWith, identity, ignoreElements, merge, of, share, switchMap, } from "rxjs";
|
|
3
3
|
import { EventMemory, filterDuplicateEvents, } from "applesauce-core";
|
|
4
4
|
import { completeOnEose } from "./operators/complete-on-eose.js";
|
|
5
5
|
import { onlyEvents } from "./operators/only-events.js";
|
|
@@ -28,12 +28,12 @@ export class RelayGroup {
|
|
|
28
28
|
* Make a request to all relays
|
|
29
29
|
* @note This does not deduplicate events
|
|
30
30
|
*/
|
|
31
|
-
req(filters, id = nanoid(
|
|
31
|
+
req(filters, id = nanoid(), opts) {
|
|
32
32
|
const requests = this.relays.map((relay) => relay.req(filters, id).pipe(
|
|
33
33
|
// Ignore connection errors
|
|
34
34
|
catchError(() => of("EOSE"))));
|
|
35
35
|
// Merge events and the single EOSE stream
|
|
36
|
-
return this.mergeEOSE(requests);
|
|
36
|
+
return this.mergeEOSE(requests, opts?.eventStore);
|
|
37
37
|
}
|
|
38
38
|
/** Send an event to all relays */
|
|
39
39
|
event(event) {
|
|
@@ -77,6 +77,10 @@ export class RelayGroup {
|
|
|
77
77
|
// Pass event store so that duplicate events are removed
|
|
78
78
|
opts?.eventStore);
|
|
79
79
|
}
|
|
80
|
+
/** Count events on all relays in the group */
|
|
81
|
+
count(filters, id = nanoid()) {
|
|
82
|
+
return combineLatest(Object.fromEntries(this.relays.map((relay) => [relay.url, relay.count(filters, id)])));
|
|
83
|
+
}
|
|
80
84
|
/** Negentropy sync events with the relays and an event store */
|
|
81
85
|
sync(store, filter, direction) {
|
|
82
86
|
// Get an array of relays that support NIP-77 negentropy sync
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Observable } from "rxjs";
|
|
2
|
+
import { IPool } from "./types.js";
|
|
3
|
+
/** Relay health states for liveness tracking */
|
|
4
|
+
export type RelayHealthState = "online" | "offline" | "dead";
|
|
5
|
+
/** State information for a relay's health tracking */
|
|
6
|
+
export interface RelayState {
|
|
7
|
+
/** Current relay health state */
|
|
8
|
+
state: RelayHealthState;
|
|
9
|
+
/** Number of consecutive failures */
|
|
10
|
+
failureCount: number;
|
|
11
|
+
/** Timestamp of last failure */
|
|
12
|
+
lastFailureTime: number;
|
|
13
|
+
/** Timestamp of last success */
|
|
14
|
+
lastSuccessTime: number;
|
|
15
|
+
/** When the backoff period ends (timestamp) */
|
|
16
|
+
backoffUntil?: number;
|
|
17
|
+
}
|
|
18
|
+
/** Storage adapter interface for persisting relay liveness state */
|
|
19
|
+
export interface LivenessStorage {
|
|
20
|
+
/**
|
|
21
|
+
* Get an item from storage
|
|
22
|
+
* @param key The storage key
|
|
23
|
+
* @returns The stored value or null if not found
|
|
24
|
+
*/
|
|
25
|
+
getItem(key: string): Promise<any> | any;
|
|
26
|
+
/**
|
|
27
|
+
* Set an item in storage
|
|
28
|
+
* @param key The storage key
|
|
29
|
+
* @param value The value to store
|
|
30
|
+
*/
|
|
31
|
+
setItem(key: string, value: any): Promise<void> | void;
|
|
32
|
+
}
|
|
33
|
+
/** Configuration options for RelayLiveness */
|
|
34
|
+
export interface LivenessOptions {
|
|
35
|
+
/** Optional async storage adapter for persistence */
|
|
36
|
+
storage?: LivenessStorage;
|
|
37
|
+
/** Maximum failures before moving from offline to dead */
|
|
38
|
+
maxFailuresBeforeDead?: number;
|
|
39
|
+
/** Base delay for exponential backoff (ms) */
|
|
40
|
+
backoffBaseDelay?: number;
|
|
41
|
+
/** Maximum backoff delay (ms) */
|
|
42
|
+
backoffMaxDelay?: number;
|
|
43
|
+
}
|
|
44
|
+
/** Record and manage liveness reports for relays */
|
|
45
|
+
export declare class RelayLiveness {
|
|
46
|
+
private log;
|
|
47
|
+
private readonly options;
|
|
48
|
+
private readonly states$;
|
|
49
|
+
/** Relays that have been seen this session. this should be used when checking dead relays for liveness */
|
|
50
|
+
readonly seen: Set<string>;
|
|
51
|
+
/** Storage adapter for persistence */
|
|
52
|
+
readonly storage?: LivenessStorage;
|
|
53
|
+
/** An observable of all relays that are online */
|
|
54
|
+
online$: Observable<string[]>;
|
|
55
|
+
/** An observable of all relays that are offline */
|
|
56
|
+
offline$: Observable<string[]>;
|
|
57
|
+
/** An observable of all relays that are dead */
|
|
58
|
+
dead$: Observable<string[]>;
|
|
59
|
+
/** An observable of all relays that are online or not in backoff */
|
|
60
|
+
healthy$: Observable<string[]>;
|
|
61
|
+
/** An observable of all relays that are dead or in backoff */
|
|
62
|
+
unhealthy$: Observable<string[]>;
|
|
63
|
+
/** Relays that are known to be online */
|
|
64
|
+
get online(): string[];
|
|
65
|
+
/** Relays that are known to be offline */
|
|
66
|
+
get offline(): string[];
|
|
67
|
+
/** Relays that are known to be dead */
|
|
68
|
+
get dead(): string[];
|
|
69
|
+
/** Relays that are online or not in backoff */
|
|
70
|
+
get healthy(): string[];
|
|
71
|
+
/** Relays that are dead or in backoff */
|
|
72
|
+
get unhealthy(): string[];
|
|
73
|
+
/**
|
|
74
|
+
* Create a new RelayLiveness instance
|
|
75
|
+
* @param options Configuration options for the liveness tracker
|
|
76
|
+
*/
|
|
77
|
+
constructor(options?: LivenessOptions);
|
|
78
|
+
/** Load relay states from storage */
|
|
79
|
+
load(): Promise<void>;
|
|
80
|
+
/** Save all known relays and their states to storage */
|
|
81
|
+
save(): Promise<void>;
|
|
82
|
+
/** Filter relay list, removing dead relays and relays in backoff */
|
|
83
|
+
filter(relays: string[]): string[];
|
|
84
|
+
/** Subscribe to a relays state */
|
|
85
|
+
state(relay: string): Observable<RelayState | undefined>;
|
|
86
|
+
/** Revive a dead relay with the max backoff delay */
|
|
87
|
+
revive(relay: string): void;
|
|
88
|
+
/** Get current relay health state for a relay */
|
|
89
|
+
getState(relay: string): RelayState | undefined;
|
|
90
|
+
/** Check if a relay is currently in backoff period */
|
|
91
|
+
isInBackoff(relay: string): boolean;
|
|
92
|
+
/** Get remaining backoff time for a relay (in ms) */
|
|
93
|
+
getBackoffRemaining(relay: string): number;
|
|
94
|
+
/** Calculate backoff delay based on failure count */
|
|
95
|
+
private calculateBackoffDelay;
|
|
96
|
+
/**
|
|
97
|
+
* Record a successful connection
|
|
98
|
+
* @param relay The relay URL that succeeded
|
|
99
|
+
*/
|
|
100
|
+
recordSuccess(relay: string): void;
|
|
101
|
+
/**
|
|
102
|
+
* Record a failed connection
|
|
103
|
+
* @param relay The relay URL that failed
|
|
104
|
+
*/
|
|
105
|
+
recordFailure(relay: string): void;
|
|
106
|
+
/**
|
|
107
|
+
* Get all seen relays (for debugging/monitoring)
|
|
108
|
+
*/
|
|
109
|
+
getSeenRelays(): string[];
|
|
110
|
+
/**
|
|
111
|
+
* Reset state for one or all relays
|
|
112
|
+
* @param relay Optional specific relay URL to reset, or reset all if not provided
|
|
113
|
+
*/
|
|
114
|
+
reset(relay?: string): void;
|
|
115
|
+
private connections;
|
|
116
|
+
/** Connect to a {@link RelayPool} instance and track relay connections */
|
|
117
|
+
connectToPool(pool: IPool): void;
|
|
118
|
+
/** Disconnect from a {@link RelayPool} instance */
|
|
119
|
+
disconnectFromPool(pool: IPool): void;
|
|
120
|
+
private updateRelayState;
|
|
121
|
+
private saveKnownRelays;
|
|
122
|
+
private saveRelayState;
|
|
123
|
+
}
|
package/dist/liveness.js
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { logger } from "applesauce-core";
|
|
2
|
+
import { BehaviorSubject, map } from "rxjs";
|
|
3
|
+
/** Record and manage liveness reports for relays */
|
|
4
|
+
export class RelayLiveness {
|
|
5
|
+
log = logger.extend("RelayLiveness");
|
|
6
|
+
options;
|
|
7
|
+
states$ = new BehaviorSubject({});
|
|
8
|
+
/** Relays that have been seen this session. this should be used when checking dead relays for liveness */
|
|
9
|
+
seen = new Set();
|
|
10
|
+
/** Storage adapter for persistence */
|
|
11
|
+
storage;
|
|
12
|
+
/** An observable of all relays that are online */
|
|
13
|
+
online$;
|
|
14
|
+
/** An observable of all relays that are offline */
|
|
15
|
+
offline$;
|
|
16
|
+
/** An observable of all relays that are dead */
|
|
17
|
+
dead$;
|
|
18
|
+
/** An observable of all relays that are online or not in backoff */
|
|
19
|
+
healthy$;
|
|
20
|
+
/** An observable of all relays that are dead or in backoff */
|
|
21
|
+
unhealthy$;
|
|
22
|
+
/** Relays that are known to be online */
|
|
23
|
+
get online() {
|
|
24
|
+
return Object.keys(this.states$.value).filter((relay) => this.states$.value[relay].state === "online");
|
|
25
|
+
}
|
|
26
|
+
/** Relays that are known to be offline */
|
|
27
|
+
get offline() {
|
|
28
|
+
return Object.keys(this.states$.value).filter((relay) => this.states$.value[relay].state === "offline");
|
|
29
|
+
}
|
|
30
|
+
/** Relays that are known to be dead */
|
|
31
|
+
get dead() {
|
|
32
|
+
return Object.keys(this.states$.value).filter((relay) => this.states$.value[relay].state === "dead");
|
|
33
|
+
}
|
|
34
|
+
/** Relays that are online or not in backoff */
|
|
35
|
+
get healthy() {
|
|
36
|
+
return Object.keys(this.states$.value).filter((relay) => {
|
|
37
|
+
const state = this.states$.value[relay];
|
|
38
|
+
return state.state === "online" || (state.state === "offline" && !this.isInBackoff(relay));
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/** Relays that are dead or in backoff */
|
|
42
|
+
get unhealthy() {
|
|
43
|
+
return Object.keys(this.states$.value).filter((relay) => {
|
|
44
|
+
const state = this.states$.value[relay];
|
|
45
|
+
return state.state === "dead" || (state.state === "offline" && this.isInBackoff(relay));
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Create a new RelayLiveness instance
|
|
50
|
+
* @param options Configuration options for the liveness tracker
|
|
51
|
+
*/
|
|
52
|
+
constructor(options = {}) {
|
|
53
|
+
this.options = {
|
|
54
|
+
maxFailuresBeforeDead: options.maxFailuresBeforeDead ?? 5,
|
|
55
|
+
backoffBaseDelay: options.backoffBaseDelay ?? 30 * 1000, // 30 seconds
|
|
56
|
+
backoffMaxDelay: options.backoffMaxDelay ?? 5 * 60 * 1000, // 5 minutes
|
|
57
|
+
};
|
|
58
|
+
this.storage = options.storage;
|
|
59
|
+
// Create observable interfaces
|
|
60
|
+
this.online$ = this.states$.pipe(map((states) => Object.keys(states).filter((relay) => states[relay].state === "online")));
|
|
61
|
+
this.offline$ = this.states$.pipe(map((states) => Object.keys(states).filter((relay) => states[relay].state === "offline")));
|
|
62
|
+
this.dead$ = this.states$.pipe(map((states) => Object.keys(states).filter((relay) => states[relay].state === "dead")));
|
|
63
|
+
this.healthy$ = this.states$.pipe(map((states) => Object.keys(states).filter((relay) => {
|
|
64
|
+
const state = states[relay];
|
|
65
|
+
return state.state === "online" || (state.state === "offline" && !this.isInBackoff(relay));
|
|
66
|
+
})));
|
|
67
|
+
this.unhealthy$ = this.states$.pipe(map((states) => Object.keys(states).filter((relay) => {
|
|
68
|
+
const state = states[relay];
|
|
69
|
+
return state.state === "dead" || (state.state === "offline" && this.isInBackoff(relay));
|
|
70
|
+
})));
|
|
71
|
+
}
|
|
72
|
+
/** Load relay states from storage */
|
|
73
|
+
async load() {
|
|
74
|
+
if (!this.storage)
|
|
75
|
+
return;
|
|
76
|
+
const known = await this.storage.getItem("known");
|
|
77
|
+
if (!Array.isArray(known))
|
|
78
|
+
return;
|
|
79
|
+
this.log(`Loading states for ${known.length} known relays`);
|
|
80
|
+
const states = {};
|
|
81
|
+
for (const relay of known) {
|
|
82
|
+
try {
|
|
83
|
+
const state = await this.storage.getItem(relay);
|
|
84
|
+
if (state)
|
|
85
|
+
states[relay] = state;
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
// Ignore relay loading errors
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
this.states$.next(states);
|
|
92
|
+
}
|
|
93
|
+
/** Save all known relays and their states to storage */
|
|
94
|
+
async save() {
|
|
95
|
+
await this.saveKnownRelays();
|
|
96
|
+
await Promise.all(Object.entries(this.states$.value).map(([relay, state]) => this.saveRelayState(relay, state)));
|
|
97
|
+
this.log("Relay states saved to storage");
|
|
98
|
+
}
|
|
99
|
+
/** Filter relay list, removing dead relays and relays in backoff */
|
|
100
|
+
filter(relays) {
|
|
101
|
+
const results = [];
|
|
102
|
+
for (const relay of relays) {
|
|
103
|
+
// Track that this relay has been seen
|
|
104
|
+
this.seen.add(relay);
|
|
105
|
+
const state = this.getState(relay);
|
|
106
|
+
// Filter based on state and backoff
|
|
107
|
+
switch (state?.state) {
|
|
108
|
+
case undefined: // unknown state
|
|
109
|
+
case "online":
|
|
110
|
+
results.push(relay);
|
|
111
|
+
break;
|
|
112
|
+
case "offline":
|
|
113
|
+
// Only include if not in backoff
|
|
114
|
+
if (!this.isInBackoff(relay))
|
|
115
|
+
results.push(relay);
|
|
116
|
+
break;
|
|
117
|
+
case "dead":
|
|
118
|
+
default:
|
|
119
|
+
// Don't include dead relays
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return results;
|
|
124
|
+
}
|
|
125
|
+
/** Subscribe to a relays state */
|
|
126
|
+
state(relay) {
|
|
127
|
+
return this.states$.pipe(map((states) => states[relay]));
|
|
128
|
+
}
|
|
129
|
+
/** Revive a dead relay with the max backoff delay */
|
|
130
|
+
revive(relay) {
|
|
131
|
+
const state = this.getState(relay);
|
|
132
|
+
if (!state)
|
|
133
|
+
return;
|
|
134
|
+
this.updateRelayState(relay, {
|
|
135
|
+
state: "offline",
|
|
136
|
+
failureCount: 0,
|
|
137
|
+
lastFailureTime: 0,
|
|
138
|
+
lastSuccessTime: Date.now(),
|
|
139
|
+
backoffUntil: this.options.backoffMaxDelay,
|
|
140
|
+
});
|
|
141
|
+
this.log(`Relay ${relay} revived to offline state with max backoff delay`);
|
|
142
|
+
}
|
|
143
|
+
/** Get current relay health state for a relay */
|
|
144
|
+
getState(relay) {
|
|
145
|
+
return this.states$.value[relay];
|
|
146
|
+
}
|
|
147
|
+
/** Check if a relay is currently in backoff period */
|
|
148
|
+
isInBackoff(relay) {
|
|
149
|
+
const state = this.getState(relay);
|
|
150
|
+
if (!state?.backoffUntil)
|
|
151
|
+
return false;
|
|
152
|
+
return Date.now() < state.backoffUntil;
|
|
153
|
+
}
|
|
154
|
+
/** Get remaining backoff time for a relay (in ms) */
|
|
155
|
+
getBackoffRemaining(relay) {
|
|
156
|
+
const state = this.getState(relay);
|
|
157
|
+
if (!state?.backoffUntil)
|
|
158
|
+
return 0;
|
|
159
|
+
return Math.max(0, state.backoffUntil - Date.now());
|
|
160
|
+
}
|
|
161
|
+
/** Calculate backoff delay based on failure count */
|
|
162
|
+
calculateBackoffDelay(failureCount) {
|
|
163
|
+
const delay = this.options.backoffBaseDelay * Math.pow(2, failureCount - 1);
|
|
164
|
+
return Math.min(delay, this.options.backoffMaxDelay);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Record a successful connection
|
|
168
|
+
* @param relay The relay URL that succeeded
|
|
169
|
+
*/
|
|
170
|
+
recordSuccess(relay) {
|
|
171
|
+
const now = Date.now();
|
|
172
|
+
const state = this.getState(relay);
|
|
173
|
+
// Don't update dead relays
|
|
174
|
+
if (state?.state === "dead") {
|
|
175
|
+
this.log(`Ignoring success for dead relay ${relay}`);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
// Record new relays
|
|
179
|
+
if (state === undefined) {
|
|
180
|
+
this.seen.add(relay);
|
|
181
|
+
this.saveKnownRelays();
|
|
182
|
+
}
|
|
183
|
+
// TODO: resetting the state back to online might be too aggressive?
|
|
184
|
+
const newState = {
|
|
185
|
+
state: "online",
|
|
186
|
+
failureCount: 0,
|
|
187
|
+
lastFailureTime: 0,
|
|
188
|
+
lastSuccessTime: now,
|
|
189
|
+
};
|
|
190
|
+
this.updateRelayState(relay, newState);
|
|
191
|
+
// Log transition if it's not the first time we've seen the relay
|
|
192
|
+
if (state && state.state !== newState.state)
|
|
193
|
+
this.log(`Relay ${relay} transitioned ${state?.state} -> online`);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Record a failed connection
|
|
197
|
+
* @param relay The relay URL that failed
|
|
198
|
+
*/
|
|
199
|
+
recordFailure(relay) {
|
|
200
|
+
const state = this.getState(relay);
|
|
201
|
+
// Don't update dead relays
|
|
202
|
+
if (state?.state === "dead")
|
|
203
|
+
return;
|
|
204
|
+
// Ignore failures during backoff, this should help catch double reporting of failures
|
|
205
|
+
if (this.isInBackoff(relay))
|
|
206
|
+
return;
|
|
207
|
+
const now = Date.now();
|
|
208
|
+
const failureCount = (state?.failureCount || 0) + 1;
|
|
209
|
+
// Record new relays
|
|
210
|
+
if (state === undefined) {
|
|
211
|
+
this.seen.add(relay);
|
|
212
|
+
this.saveKnownRelays();
|
|
213
|
+
}
|
|
214
|
+
// Calculate backoff delay
|
|
215
|
+
const backoffDelay = this.calculateBackoffDelay(failureCount);
|
|
216
|
+
const newState = failureCount >= this.options.maxFailuresBeforeDead ? "dead" : "offline";
|
|
217
|
+
const relayState = {
|
|
218
|
+
state: newState,
|
|
219
|
+
failureCount,
|
|
220
|
+
lastFailureTime: now,
|
|
221
|
+
lastSuccessTime: state?.lastSuccessTime || 0,
|
|
222
|
+
backoffUntil: now + backoffDelay,
|
|
223
|
+
};
|
|
224
|
+
this.updateRelayState(relay, relayState);
|
|
225
|
+
// Log transition if it's not the first time we've seen the relay
|
|
226
|
+
if (newState !== state?.state)
|
|
227
|
+
this.log(`Relay ${relay} transitioned ${state?.state} -> ${newState}`);
|
|
228
|
+
// Set a timeout that will clear the backoff period
|
|
229
|
+
setTimeout(() => {
|
|
230
|
+
const state = this.getState(relay);
|
|
231
|
+
if (!state || state.backoffUntil === undefined)
|
|
232
|
+
return;
|
|
233
|
+
this.updateRelayState(relay, { ...state, backoffUntil: undefined });
|
|
234
|
+
}, backoffDelay);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Get all seen relays (for debugging/monitoring)
|
|
238
|
+
*/
|
|
239
|
+
getSeenRelays() {
|
|
240
|
+
return Array.from(this.seen);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Reset state for one or all relays
|
|
244
|
+
* @param relay Optional specific relay URL to reset, or reset all if not provided
|
|
245
|
+
*/
|
|
246
|
+
reset(relay) {
|
|
247
|
+
if (relay) {
|
|
248
|
+
const newStates = { ...this.states$.value };
|
|
249
|
+
delete newStates[relay];
|
|
250
|
+
this.states$.next(newStates);
|
|
251
|
+
this.seen.delete(relay);
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
// Reset all relays
|
|
255
|
+
this.states$.next({});
|
|
256
|
+
this.seen.clear();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// The connected pools and cleanup methods
|
|
260
|
+
connections = new Map();
|
|
261
|
+
/** Connect to a {@link RelayPool} instance and track relay connections */
|
|
262
|
+
connectToPool(pool) {
|
|
263
|
+
// Relay cleanup methods
|
|
264
|
+
const relays = new Map();
|
|
265
|
+
// Listen for relays being added
|
|
266
|
+
const add = pool.add$.subscribe((relay) => {
|
|
267
|
+
// Record seen relays
|
|
268
|
+
this.seen.add(relay.url);
|
|
269
|
+
const open = relay.open$.subscribe(() => {
|
|
270
|
+
this.recordSuccess(relay.url);
|
|
271
|
+
});
|
|
272
|
+
const close = relay.close$.subscribe((event) => {
|
|
273
|
+
if (event.wasClean === false)
|
|
274
|
+
this.recordFailure(relay.url);
|
|
275
|
+
});
|
|
276
|
+
// Register the cleanup method
|
|
277
|
+
relays.set(relay, () => {
|
|
278
|
+
open.unsubscribe();
|
|
279
|
+
close.unsubscribe();
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
// Listen for relays being removed
|
|
283
|
+
const remove = pool.remove$.subscribe((relay) => {
|
|
284
|
+
const cleanup = relays.get(relay);
|
|
285
|
+
if (cleanup)
|
|
286
|
+
cleanup();
|
|
287
|
+
relays.delete(relay);
|
|
288
|
+
});
|
|
289
|
+
// register the cleanup method
|
|
290
|
+
this.connections.set(pool, () => {
|
|
291
|
+
add.unsubscribe();
|
|
292
|
+
remove.unsubscribe();
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
/** Disconnect from a {@link RelayPool} instance */
|
|
296
|
+
disconnectFromPool(pool) {
|
|
297
|
+
const cleanup = this.connections.get(pool);
|
|
298
|
+
if (cleanup)
|
|
299
|
+
cleanup();
|
|
300
|
+
this.connections.delete(pool);
|
|
301
|
+
}
|
|
302
|
+
updateRelayState(relay, state) {
|
|
303
|
+
this.states$.next({ ...this.states$.value, [relay]: state });
|
|
304
|
+
// Auto-save to storage
|
|
305
|
+
this.saveRelayState(relay, state);
|
|
306
|
+
}
|
|
307
|
+
async saveKnownRelays() {
|
|
308
|
+
if (!this.storage)
|
|
309
|
+
return;
|
|
310
|
+
try {
|
|
311
|
+
await this.storage.setItem("known", Object.keys(this.states$.value));
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
// Ignore storage errors
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
async saveRelayState(relay, state) {
|
|
318
|
+
if (!this.storage)
|
|
319
|
+
return;
|
|
320
|
+
try {
|
|
321
|
+
await this.storage.setItem(relay, state);
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
// Ignore storage errors
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
package/dist/operators/index.js
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { MonoTypeOperatorFunction, Observable } from "rxjs";
|
|
2
|
+
type ILivenessTracker = {
|
|
3
|
+
unhealthy$: Observable<string[]>;
|
|
4
|
+
seen?: Set<string>;
|
|
5
|
+
};
|
|
6
|
+
/** Filters out unhealthy relays from an array of pointers */
|
|
7
|
+
export declare function ignoreUnhealthyRelaysOnPointers<T extends {
|
|
8
|
+
relays?: string[];
|
|
9
|
+
}, Tracker extends ILivenessTracker>(liveness: Tracker): MonoTypeOperatorFunction<T[]>;
|
|
10
|
+
/** Filters out unhealthy relays from the inboxes and outboxes */
|
|
11
|
+
export declare function ignoreUnhealthyMailboxes<T extends {
|
|
12
|
+
inboxes?: string[];
|
|
13
|
+
outboxes?: string[];
|
|
14
|
+
}, Tracker extends ILivenessTracker>(liveness: Tracker): MonoTypeOperatorFunction<T>;
|
|
15
|
+
/** Filters out unhealthy relays from an array of relays */
|
|
16
|
+
export declare function ignoreUnhealthyRelays<Tracker extends ILivenessTracker>(liveness: Tracker): MonoTypeOperatorFunction<string[]>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { combineLatestWith, map } from "rxjs";
|
|
2
|
+
function filterRelays(relays, unhealthy, liveness) {
|
|
3
|
+
return (relays &&
|
|
4
|
+
relays.filter((relay) => {
|
|
5
|
+
// Notify the liveness tracker that we've seen this relay
|
|
6
|
+
liveness.seen?.add(relay);
|
|
7
|
+
// Exclude unhealthy relays
|
|
8
|
+
return !unhealthy.includes(relay);
|
|
9
|
+
}));
|
|
10
|
+
}
|
|
11
|
+
/** Filters out unhealthy relays from an array of pointers */
|
|
12
|
+
export function ignoreUnhealthyRelaysOnPointers(liveness) {
|
|
13
|
+
return (source) => source.pipe(
|
|
14
|
+
// Combine with the liveness observable
|
|
15
|
+
combineLatestWith(liveness.unhealthy$),
|
|
16
|
+
// Filters out unhealthy relays from the pointers
|
|
17
|
+
map(([pointers, unhealthy]) => pointers.map((pointer) => {
|
|
18
|
+
if (!pointer.relays)
|
|
19
|
+
return pointer;
|
|
20
|
+
// Exclude unhealthy relays
|
|
21
|
+
return { ...pointer, relays: filterRelays(pointer.relays, unhealthy, liveness) };
|
|
22
|
+
})));
|
|
23
|
+
}
|
|
24
|
+
/** Filters out unhealthy relays from the inboxes and outboxes */
|
|
25
|
+
export function ignoreUnhealthyMailboxes(liveness) {
|
|
26
|
+
return (source) => source.pipe(
|
|
27
|
+
// Combine with the liveness observable
|
|
28
|
+
combineLatestWith(liveness.unhealthy$),
|
|
29
|
+
// Filters out unhealthy relays from the inboxes and outboxes
|
|
30
|
+
map(([mailboxes, unhealthy]) => {
|
|
31
|
+
if (!mailboxes.inboxes && !mailboxes.outboxes)
|
|
32
|
+
return mailboxes;
|
|
33
|
+
return {
|
|
34
|
+
...mailboxes,
|
|
35
|
+
inboxes: filterRelays(mailboxes.inboxes, unhealthy, liveness),
|
|
36
|
+
outboxes: filterRelays(mailboxes.outboxes, unhealthy, liveness),
|
|
37
|
+
};
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
/** Filters out unhealthy relays from an array of relays */
|
|
41
|
+
export function ignoreUnhealthyRelays(liveness) {
|
|
42
|
+
return (source) => source.pipe(
|
|
43
|
+
// Combine with the liveness observable
|
|
44
|
+
combineLatestWith(liveness.unhealthy$),
|
|
45
|
+
// Filters out unhealthy relays from the array
|
|
46
|
+
map(([relays, unhealthy]) => filterRelays(relays, unhealthy, liveness)));
|
|
47
|
+
}
|
package/dist/pool.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { BehaviorSubject, Observable, Subject } from "rxjs";
|
|
|
4
4
|
import { RelayGroup } from "./group.js";
|
|
5
5
|
import type { NegentropySyncOptions, ReconcileFunction } from "./negentropy.js";
|
|
6
6
|
import { Relay, SyncDirection, type RelayOptions } from "./relay.js";
|
|
7
|
-
import type { FilterInput, IPool, IRelay, PublishResponse, SubscriptionResponse } from "./types.js";
|
|
7
|
+
import type { CountResponse, FilterInput, IPool, IRelay, PublishResponse, SubscriptionResponse } from "./types.js";
|
|
8
8
|
export declare class RelayPool implements IPool {
|
|
9
9
|
options?: RelayOptions | undefined;
|
|
10
10
|
groups$: BehaviorSubject<Map<string, RelayGroup>>;
|
|
@@ -37,6 +37,8 @@ export declare class RelayPool implements IPool {
|
|
|
37
37
|
request(relays: string[], filters: Parameters<RelayGroup["request"]>[0], opts?: Parameters<RelayGroup["request"]>[1]): Observable<NostrEvent>;
|
|
38
38
|
/** Open a subscription to multiple relays */
|
|
39
39
|
subscription(relays: string[], filters: Parameters<RelayGroup["subscription"]>[0], options?: Parameters<RelayGroup["subscription"]>[1]): Observable<SubscriptionResponse>;
|
|
40
|
+
/** Count events on multiple relays */
|
|
41
|
+
count(relays: string[], filters: Filter | Filter[], id?: string): Observable<Record<string, CountResponse>>;
|
|
40
42
|
/** Negentropy sync events with the relays and an event store */
|
|
41
43
|
sync(relays: string[], store: IEventStoreRead | IAsyncEventStoreRead | NostrEvent[], filter: Filter, direction?: SyncDirection): Observable<NostrEvent>;
|
|
42
44
|
}
|
package/dist/pool.js
CHANGED
|
@@ -112,6 +112,10 @@ export class RelayPool {
|
|
|
112
112
|
subscription(relays, filters, options) {
|
|
113
113
|
return this.group(relays).subscription(filters, options);
|
|
114
114
|
}
|
|
115
|
+
/** Count events on multiple relays */
|
|
116
|
+
count(relays, filters, id) {
|
|
117
|
+
return this.group(relays).count(filters, id);
|
|
118
|
+
}
|
|
115
119
|
/** Negentropy sync events with the relays and an event store */
|
|
116
120
|
sync(relays, store, filter, direction) {
|
|
117
121
|
return this.group(relays).sync(store, filter, direction);
|
package/dist/relay.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { RelayInformation } from "nostr-tools/nip11";
|
|
|
4
4
|
import { BehaviorSubject, MonoTypeOperatorFunction, Observable, RepeatConfig, RetryConfig, Subject } from "rxjs";
|
|
5
5
|
import { WebSocketSubject, WebSocketSubjectConfig } from "rxjs/webSocket";
|
|
6
6
|
import { type NegentropySyncOptions, type ReconcileFunction } from "./negentropy.js";
|
|
7
|
-
import { AuthSigner, FilterInput, IRelay, PublishOptions, PublishResponse, RequestOptions, SubscriptionOptions, SubscriptionResponse } from "./types.js";
|
|
7
|
+
import { AuthSigner, CountResponse, FilterInput, IRelay, PublishOptions, PublishResponse, RequestOptions, SubscriptionOptions, SubscriptionResponse } from "./types.js";
|
|
8
8
|
/** Flags for the negentropy sync type */
|
|
9
9
|
export declare enum SyncDirection {
|
|
10
10
|
RECEIVE = 1,
|
|
@@ -104,6 +104,8 @@ export declare class Relay implements IRelay {
|
|
|
104
104
|
send(message: any): void;
|
|
105
105
|
/** Create a REQ observable that emits events or "EOSE" or errors */
|
|
106
106
|
req(filters: FilterInput, id?: string): Observable<SubscriptionResponse>;
|
|
107
|
+
/** Create a COUNT observable that emits a single count response */
|
|
108
|
+
count(filters: Filter | Filter[], id?: string): Observable<CountResponse>;
|
|
107
109
|
/** Send an EVENT or AUTH message and return an observable of PublishResponse that completes or errors */
|
|
108
110
|
event(event: NostrEvent, verb?: "EVENT" | "AUTH"): Observable<PublishResponse>;
|
|
109
111
|
/** send and AUTH message */
|
|
@@ -118,6 +120,8 @@ export declare class Relay implements IRelay {
|
|
|
118
120
|
protected customRepeatOperator<T extends unknown = unknown>(times: undefined | boolean | number | RepeatConfig | undefined): MonoTypeOperatorFunction<T>;
|
|
119
121
|
/** Internal operator for creating the timeout() operator */
|
|
120
122
|
protected customTimeoutOperator<T extends unknown = unknown>(timeout: undefined | boolean | number, defaultTimeout: number): MonoTypeOperatorFunction<T>;
|
|
123
|
+
/** Internal operator for handling auth-required errors from REQ/COUNT operations */
|
|
124
|
+
protected handleAuthRequiredForReq(operation: "REQ" | "COUNT"): MonoTypeOperatorFunction<any>;
|
|
121
125
|
/** Creates a REQ that retries when relay errors ( default 3 retries ) */
|
|
122
126
|
subscription(filters: Filter | Filter[], opts?: SubscriptionOptions): Observable<SubscriptionResponse>;
|
|
123
127
|
/** Makes a single request that retires on errors and completes on EOSE */
|
package/dist/relay.js
CHANGED
|
@@ -7,6 +7,7 @@ import { BehaviorSubject, catchError, combineLatest, defer, endWith, filter, fin
|
|
|
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
|
+
const AUTH_REQUIRED_PREFIX = "auth-required:";
|
|
10
11
|
const DEFAULT_RETRY_CONFIG = { count: 10, delay: 1000, resetOnSuccess: true };
|
|
11
12
|
/** Flags for the negentropy sync type */
|
|
12
13
|
export var SyncDirection;
|
|
@@ -294,21 +295,11 @@ export class Relay {
|
|
|
294
295
|
throw new ReqCloseError(message[2]);
|
|
295
296
|
else
|
|
296
297
|
return message[2];
|
|
297
|
-
}),
|
|
298
|
-
// Set REQ auth required if the REQ is closed with auth-required
|
|
299
|
-
if (error instanceof ReqCloseError &&
|
|
300
|
-
error.message.startsWith("auth-required") &&
|
|
301
|
-
!this.receivedAuthRequiredForReq.value) {
|
|
302
|
-
this.log("Auth required for REQ");
|
|
303
|
-
this.receivedAuthRequiredForReq.next(true);
|
|
304
|
-
}
|
|
305
|
-
// Pass the error through
|
|
306
|
-
return throwError(() => error);
|
|
307
|
-
}),
|
|
298
|
+
}), this.handleAuthRequiredForReq("REQ"),
|
|
308
299
|
// mark events as from relays
|
|
309
300
|
markFromRelay(this.url),
|
|
310
301
|
// if no events are seen in 10s, emit EOSE
|
|
311
|
-
// TODO: this should emit EOSE
|
|
302
|
+
// TODO: this timeout should only emit EOSE after the last event is seen and no EOSE has been sent in (timeout ms)
|
|
312
303
|
timeout({
|
|
313
304
|
first: this.eoseTimeout,
|
|
314
305
|
with: () => merge(of("EOSE"), NEVER),
|
|
@@ -318,6 +309,38 @@ export class Relay {
|
|
|
318
309
|
// Wait for auth if required and make sure to start the watch tower
|
|
319
310
|
return this.waitForReady(this.waitForAuth(this.authRequiredForRead$, observable));
|
|
320
311
|
}
|
|
312
|
+
/** Create a COUNT observable that emits a single count response */
|
|
313
|
+
count(filters, id = nanoid()) {
|
|
314
|
+
// Create an observable that filters responses from the relay to just the ones for this COUNT
|
|
315
|
+
const messages = this.socket.pipe(filter((m) => Array.isArray(m) && (m[0] === "COUNT" || m[0] === "CLOSED") && m[1] === id),
|
|
316
|
+
// Singleton (prevents duplicate subscriptions)
|
|
317
|
+
share());
|
|
318
|
+
// Send the COUNT message and listen for response
|
|
319
|
+
const observable = defer(() => {
|
|
320
|
+
// Send the COUNT message when subscription starts
|
|
321
|
+
this.socket.next(Array.isArray(filters) ? ["COUNT", id, ...filters] : ["COUNT", id, filters]);
|
|
322
|
+
// Merge with watch tower to keep connection alive
|
|
323
|
+
return merge(this.watchTower, messages);
|
|
324
|
+
}).pipe(
|
|
325
|
+
// Map the messages to count responses or throw an error
|
|
326
|
+
map((message) => {
|
|
327
|
+
if (message[0] === "COUNT")
|
|
328
|
+
return message[2];
|
|
329
|
+
else
|
|
330
|
+
throw new ReqCloseError(message[2]);
|
|
331
|
+
}), this.handleAuthRequiredForReq("COUNT"),
|
|
332
|
+
// Complete on first value (COUNT responses are single-shot)
|
|
333
|
+
take(1),
|
|
334
|
+
// Add timeout
|
|
335
|
+
timeout({
|
|
336
|
+
first: this.eoseTimeout,
|
|
337
|
+
with: () => throwError(() => new Error("COUNT timeout")),
|
|
338
|
+
}),
|
|
339
|
+
// Only create one upstream subscription
|
|
340
|
+
share());
|
|
341
|
+
// Start the watch tower and wait for auth if required
|
|
342
|
+
return this.waitForReady(this.waitForAuth(this.authRequiredForRead$, observable));
|
|
343
|
+
}
|
|
321
344
|
/** Send an EVENT or AUTH message and return an observable of PublishResponse that completes or errors */
|
|
322
345
|
event(event, verb = "EVENT") {
|
|
323
346
|
const messages = defer(() => {
|
|
@@ -338,7 +361,7 @@ export class Relay {
|
|
|
338
361
|
take(1),
|
|
339
362
|
// listen for OK auth-required
|
|
340
363
|
tap(({ ok, message }) => {
|
|
341
|
-
if (ok === false && message?.startsWith(
|
|
364
|
+
if (ok === false && message?.startsWith(AUTH_REQUIRED_PREFIX) && !this.receivedAuthRequiredForEvent.value) {
|
|
342
365
|
this.log("Auth required for publish");
|
|
343
366
|
this.receivedAuthRequiredForEvent.next(true);
|
|
344
367
|
}
|
|
@@ -414,6 +437,20 @@ export class Relay {
|
|
|
414
437
|
else
|
|
415
438
|
return simpleTimeout(timeout ?? defaultTimeout);
|
|
416
439
|
}
|
|
440
|
+
/** Internal operator for handling auth-required errors from REQ/COUNT operations */
|
|
441
|
+
handleAuthRequiredForReq(operation) {
|
|
442
|
+
return catchError((error) => {
|
|
443
|
+
// Set auth required if the operation is closed with auth-required
|
|
444
|
+
if (error instanceof ReqCloseError &&
|
|
445
|
+
error.message.startsWith(AUTH_REQUIRED_PREFIX) &&
|
|
446
|
+
!this.receivedAuthRequiredForReq.value) {
|
|
447
|
+
this.log(`Auth required for ${operation}`);
|
|
448
|
+
this.receivedAuthRequiredForReq.next(true);
|
|
449
|
+
}
|
|
450
|
+
// Pass the error through
|
|
451
|
+
return throwError(() => error);
|
|
452
|
+
});
|
|
453
|
+
}
|
|
417
454
|
/** Creates a REQ that retries when relay errors ( default 3 retries ) */
|
|
418
455
|
subscription(filters, opts) {
|
|
419
456
|
return this.req(filters, opts?.id).pipe(
|
|
@@ -440,7 +477,7 @@ export class Relay {
|
|
|
440
477
|
publish(event, opts) {
|
|
441
478
|
return lastValueFrom(this.event(event).pipe(mergeMap((result) => {
|
|
442
479
|
// If the relay responds with auth-required, throw an error for the retry operator to handle
|
|
443
|
-
if (result.ok === false && result.message?.startsWith(
|
|
480
|
+
if (result.ok === false && result.message?.startsWith(AUTH_REQUIRED_PREFIX))
|
|
444
481
|
return throwError(() => new Error(result.message));
|
|
445
482
|
return of(result);
|
|
446
483
|
}),
|
package/dist/types.d.ts
CHANGED
|
@@ -12,6 +12,9 @@ export type PublishResponse = {
|
|
|
12
12
|
message?: string;
|
|
13
13
|
from: string;
|
|
14
14
|
};
|
|
15
|
+
export type CountResponse = {
|
|
16
|
+
count: number;
|
|
17
|
+
};
|
|
15
18
|
export type MultiplexWebSocket<T = any> = Pick<WebSocketSubject<T>, "multiplex">;
|
|
16
19
|
/** Options for the publish method on the pool and relay */
|
|
17
20
|
export type PublishOptions = {
|
|
@@ -65,6 +68,9 @@ export interface IRelay extends MultiplexWebSocket {
|
|
|
65
68
|
challenge$: Observable<string | null>;
|
|
66
69
|
authenticated$: Observable<boolean>;
|
|
67
70
|
notices$: Observable<string[]>;
|
|
71
|
+
open$: Observable<Event>;
|
|
72
|
+
close$: Observable<CloseEvent>;
|
|
73
|
+
closing$: Observable<void>;
|
|
68
74
|
error$: Observable<Error | null>;
|
|
69
75
|
readonly connected: boolean;
|
|
70
76
|
readonly authenticated: boolean;
|
|
@@ -74,6 +80,8 @@ export interface IRelay extends MultiplexWebSocket {
|
|
|
74
80
|
close(): void;
|
|
75
81
|
/** Send a REQ message */
|
|
76
82
|
req(filters: FilterInput, id?: string): Observable<SubscriptionResponse>;
|
|
83
|
+
/** Send a COUNT message */
|
|
84
|
+
count(filters: Filter | Filter[], id?: string): Observable<CountResponse>;
|
|
77
85
|
/** Send an EVENT message */
|
|
78
86
|
event(event: NostrEvent): Observable<PublishResponse>;
|
|
79
87
|
/** Send an AUTH message */
|
|
@@ -110,6 +118,8 @@ export interface IGroup {
|
|
|
110
118
|
request(filters: FilterInput, opts?: GroupRequestOptions): Observable<NostrEvent>;
|
|
111
119
|
/** Open a subscription with retries */
|
|
112
120
|
subscription(filters: FilterInput, opts?: GroupSubscriptionOptions): Observable<SubscriptionResponse>;
|
|
121
|
+
/** Count events on the relays and an event store */
|
|
122
|
+
count(filters: Filter | Filter[], id?: string): Observable<Record<string, CountResponse>>;
|
|
113
123
|
/** Negentropy sync events with the relay and an event store */
|
|
114
124
|
sync(store: IEventStoreRead | IAsyncEventStoreRead | NostrEvent[], filter: Filter, direction?: SyncDirection): Observable<NostrEvent>;
|
|
115
125
|
}
|
|
@@ -137,6 +147,8 @@ export interface IPool extends IPoolSignals {
|
|
|
137
147
|
request(relays: string[], filters: Parameters<IGroup["request"]>[0], opts?: Parameters<IGroup["request"]>[1]): Observable<NostrEvent>;
|
|
138
148
|
/** Open a subscription to relays with retries */
|
|
139
149
|
subscription(relays: string[], filters: Parameters<IGroup["subscription"]>[0], opts?: Parameters<IGroup["subscription"]>[1]): Observable<SubscriptionResponse>;
|
|
150
|
+
/** Count events on the relays and an event store */
|
|
151
|
+
count(relays: string[], filters: Filter | Filter[], id?: string): Observable<Record<string, CountResponse>>;
|
|
140
152
|
/** Negentropy sync events with the relay and an event store */
|
|
141
153
|
sync(relays: string[], store: IEventStoreRead | IAsyncEventStoreRead | NostrEvent[], filter: Filter, direction?: SyncDirection): Observable<NostrEvent>;
|
|
142
154
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "applesauce-relay",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "nostr relay communication framework built on rxjs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -52,14 +52,14 @@
|
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@noble/hashes": "^1.7.1",
|
|
55
|
-
"applesauce-core": "^4.
|
|
55
|
+
"applesauce-core": "^4.1.0",
|
|
56
56
|
"nanoid": "^5.0.9",
|
|
57
57
|
"nostr-tools": "~2.17",
|
|
58
58
|
"rxjs": "^7.8.1"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@hirez_io/observer-spy": "^2.2.0",
|
|
62
|
-
"applesauce-signers": "^4.
|
|
62
|
+
"applesauce-signers": "^4.1.0",
|
|
63
63
|
"rimraf": "^6.0.1",
|
|
64
64
|
"typescript": "^5.7.3",
|
|
65
65
|
"vitest": "^3.2.4",
|