applesauce-relay 4.0.0 → 4.2.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 +24 -8
- package/dist/group.js +140 -38
- 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/negentropy.js +41 -8
- package/dist/operators/index.d.ts +2 -0
- package/dist/operators/index.js +2 -0
- package/dist/operators/liveness.d.ts +17 -0
- package/dist/operators/liveness.js +47 -0
- package/dist/operators/reverse-switch-map.d.ts +9 -0
- package/dist/operators/reverse-switch-map.js +46 -0
- package/dist/pool.d.ts +16 -14
- package/dist/pool.js +33 -38
- package/dist/relay.d.ts +7 -3
- package/dist/relay.js +78 -19
- package/dist/types.d.ts +35 -15
- 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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Filter, type NostrEvent } from "nostr-tools";
|
|
2
|
-
import { Observable } from "rxjs";
|
|
2
|
+
import { BehaviorSubject, MonoTypeOperatorFunction, 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";
|
|
6
5
|
import { SyncDirection } from "./relay.js";
|
|
6
|
+
import { CountResponse, FilterInput, IGroup, IGroupRelayInput, IRelay, PublishOptions, PublishResponse, RequestOptions, SubscriptionOptions, SubscriptionResponse } from "./types.js";
|
|
7
7
|
/** Options for negentropy sync on a group of relays */
|
|
8
8
|
export type GroupNegentropySyncOptions = NegentropySyncOptions & {
|
|
9
9
|
/** Whether to sync in parallel (default true) */
|
|
@@ -20,25 +20,41 @@ export type GroupRequestOptions = RequestOptions & {
|
|
|
20
20
|
eventStore?: IEventStoreActions | IAsyncEventStoreActions | null;
|
|
21
21
|
};
|
|
22
22
|
export declare class RelayGroup implements IGroup {
|
|
23
|
-
relays
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
protected relays$: BehaviorSubject<IRelay[]> | Observable<IRelay[]>;
|
|
24
|
+
get relays(): IRelay[];
|
|
25
|
+
constructor(relays: IGroupRelayInput);
|
|
26
|
+
/** Whether this group is controlled by an upstream observable */
|
|
27
|
+
private get controlled();
|
|
28
|
+
/** Check if a relay is in the group */
|
|
29
|
+
has(relay: IRelay | string): boolean;
|
|
30
|
+
/** Add a relay to the group */
|
|
31
|
+
add(relay: IRelay): void;
|
|
32
|
+
/** Remove a relay from the group */
|
|
33
|
+
remove(relay: IRelay): void;
|
|
34
|
+
/** Internal logic for handling requests to multiple relays */
|
|
35
|
+
protected internalSubscription(project: (relay: IRelay) => Observable<SubscriptionResponse>, eventOperator?: MonoTypeOperatorFunction<NostrEvent>): Observable<SubscriptionResponse>;
|
|
36
|
+
/** Internal logic for handling publishes to multiple relays */
|
|
37
|
+
protected internalPublish(project: (relay: IRelay) => Observable<PublishResponse>): Observable<PublishResponse>;
|
|
27
38
|
/**
|
|
28
39
|
* Make a request to all relays
|
|
29
40
|
* @note This does not deduplicate events
|
|
30
41
|
*/
|
|
31
|
-
req(filters: FilterInput, id?: string
|
|
42
|
+
req(filters: FilterInput, id?: string, opts?: {
|
|
43
|
+
/** Deduplicate events with an event store (default is a temporary instance of EventMemory), null will disable deduplication */
|
|
44
|
+
eventStore?: IEventStoreActions | IAsyncEventStoreActions | null;
|
|
45
|
+
}): Observable<SubscriptionResponse>;
|
|
32
46
|
/** Send an event to all relays */
|
|
33
47
|
event(event: NostrEvent): Observable<PublishResponse>;
|
|
34
48
|
/** Negentropy sync events with the relays and an event store */
|
|
35
49
|
negentropy(store: IEventStoreRead | IAsyncEventStoreRead | NostrEvent[], filter: Filter, reconcile: ReconcileFunction, opts?: GroupNegentropySyncOptions): Promise<boolean>;
|
|
36
50
|
/** Publish an event to all relays with retries ( default 3 retries ) */
|
|
37
51
|
publish(event: NostrEvent, opts?: PublishOptions): Promise<PublishResponse[]>;
|
|
38
|
-
/** Request events from all relays
|
|
52
|
+
/** Request events from all relays and complete on EOSE */
|
|
39
53
|
request(filters: FilterInput, opts?: GroupRequestOptions): Observable<NostrEvent>;
|
|
40
54
|
/** Open a subscription to all relays with retries ( default 3 retries ) */
|
|
41
55
|
subscription(filters: FilterInput, opts?: GroupSubscriptionOptions): Observable<SubscriptionResponse>;
|
|
56
|
+
/** Count events on all relays in the group */
|
|
57
|
+
count(filters: Filter | Filter[], id?: string): Observable<Record<string, CountResponse>>;
|
|
42
58
|
/** Negentropy sync events with the relays and an event store */
|
|
43
59
|
sync(store: IEventStoreRead | IAsyncEventStoreRead | NostrEvent[], filter: Filter, direction?: SyncDirection): Observable<NostrEvent>;
|
|
44
60
|
}
|
package/dist/group.js
CHANGED
|
@@ -1,45 +1,143 @@
|
|
|
1
1
|
import { nanoid } from "nanoid";
|
|
2
|
-
import { catchError,
|
|
2
|
+
import { BehaviorSubject, catchError, combineLatest, defaultIfEmpty, defer, endWith, filter, from, identity, ignoreElements, lastValueFrom, map, merge, of, scan, share, switchMap, take, takeWhile, toArray, } 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";
|
|
6
|
+
import { reverseSwitchMap } from "./operators/reverse-switch-map.js";
|
|
7
|
+
/** Convert an error to a PublishResponse */
|
|
8
|
+
function errorToPublishResponse(relay) {
|
|
9
|
+
return catchError((err) => of({ ok: false, from: relay.url, message: err?.message || "Unknown error" }));
|
|
10
|
+
}
|
|
6
11
|
export class RelayGroup {
|
|
7
|
-
relays;
|
|
12
|
+
relays$ = new BehaviorSubject([]);
|
|
13
|
+
get relays() {
|
|
14
|
+
if (this.relays$ instanceof BehaviorSubject)
|
|
15
|
+
return this.relays$.value;
|
|
16
|
+
throw new Error("This group was created with an observable, relays are not available");
|
|
17
|
+
}
|
|
8
18
|
constructor(relays) {
|
|
9
|
-
this.relays = relays;
|
|
19
|
+
this.relays$ = Array.isArray(relays) ? new BehaviorSubject(relays) : relays;
|
|
20
|
+
}
|
|
21
|
+
/** Whether this group is controlled by an upstream observable */
|
|
22
|
+
get controlled() {
|
|
23
|
+
return this.relays$ instanceof BehaviorSubject === false;
|
|
24
|
+
}
|
|
25
|
+
/** Check if a relay is in the group */
|
|
26
|
+
has(relay) {
|
|
27
|
+
if (this.controlled)
|
|
28
|
+
throw new Error("This group was created with an observable, relays are not available");
|
|
29
|
+
if (typeof relay === "string")
|
|
30
|
+
return this.relays.some((r) => r.url === relay);
|
|
31
|
+
return this.relays.includes(relay);
|
|
32
|
+
}
|
|
33
|
+
/** Add a relay to the group */
|
|
34
|
+
add(relay) {
|
|
35
|
+
if (this.has(relay))
|
|
36
|
+
return;
|
|
37
|
+
this.relays$.next([...this.relays, relay]);
|
|
10
38
|
}
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
39
|
+
/** Remove a relay from the group */
|
|
40
|
+
remove(relay) {
|
|
41
|
+
if (!this.has(relay))
|
|
42
|
+
return;
|
|
43
|
+
this.relays$.next(this.relays.filter((r) => r !== relay));
|
|
44
|
+
}
|
|
45
|
+
/** Internal logic for handling requests to multiple relays */
|
|
46
|
+
internalSubscription(project, eventOperator = identity) {
|
|
47
|
+
// Keep a cache of upstream observables for each relay
|
|
48
|
+
const upstream = new WeakMap();
|
|
49
|
+
// Subscribe to the group relays
|
|
50
|
+
const main = this.relays$.pipe(
|
|
51
|
+
// Every time they change switch to a new observable
|
|
52
|
+
// Using reverseSwitchMap to subscribe to the new relays before unsubscribing from the old ones
|
|
53
|
+
// This avoids sending duplicate REQ messages to the relays
|
|
54
|
+
reverseSwitchMap((relays) => {
|
|
55
|
+
const observables = [];
|
|
56
|
+
for (const relay of relays) {
|
|
57
|
+
// If an upstream observable exists for this relay, use it
|
|
58
|
+
if (upstream.has(relay)) {
|
|
59
|
+
observables.push(upstream.get(relay));
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const observable = project(relay).pipe(
|
|
63
|
+
// Catch connection errors and return EOSE
|
|
64
|
+
catchError(() => of("EOSE")),
|
|
65
|
+
// Map values into tuple of relay and value
|
|
66
|
+
map((value) => [relay, value]));
|
|
67
|
+
observables.push(observable);
|
|
68
|
+
upstream.set(relay, observable);
|
|
69
|
+
}
|
|
70
|
+
return merge(...observables);
|
|
71
|
+
}),
|
|
72
|
+
// Only create one upstream subscription
|
|
73
|
+
share());
|
|
74
|
+
// Create an observable that only emits the events from the relays
|
|
75
|
+
const events = main.pipe(
|
|
76
|
+
// Pick the value from the tuple
|
|
77
|
+
map(([_, value]) => value),
|
|
78
|
+
// Only return events
|
|
16
79
|
onlyEvents(),
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
// Create
|
|
20
|
-
const eose =
|
|
21
|
-
//
|
|
22
|
-
|
|
80
|
+
// Add event operations
|
|
81
|
+
eventOperator);
|
|
82
|
+
// Create an observable that emits EOSE when all relays have sent EOSE
|
|
83
|
+
const eose = this.relays$.pipe(
|
|
84
|
+
// When the relays change, switch to a new observable
|
|
85
|
+
switchMap((relays) =>
|
|
86
|
+
// Subscribe to the events, and wait for EOSE from all relays
|
|
87
|
+
main.pipe(
|
|
88
|
+
// Only select EOSE messages
|
|
89
|
+
filter(([_, value]) => value === "EOSE"),
|
|
90
|
+
// Track the relays that have sent EOSE
|
|
91
|
+
scan((received, [relay]) => [...received, relay], []),
|
|
92
|
+
// Keep the observable open while there are relays that have not sent EOSE
|
|
93
|
+
takeWhile((received) => relays.some((r) => !received.includes(r))),
|
|
94
|
+
// Ignore all values
|
|
95
|
+
ignoreElements(),
|
|
23
96
|
// When all relays have sent EOSE, emit EOSE
|
|
24
|
-
endWith("EOSE"));
|
|
25
|
-
return merge(events, eose)
|
|
97
|
+
endWith("EOSE"))));
|
|
98
|
+
return merge(events, eose).pipe(
|
|
99
|
+
// Ensure a single upstream
|
|
100
|
+
share());
|
|
101
|
+
}
|
|
102
|
+
/** Internal logic for handling publishes to multiple relays */
|
|
103
|
+
internalPublish(project) {
|
|
104
|
+
// Keep a cache of upstream observables for each relay
|
|
105
|
+
const upstream = new WeakMap();
|
|
106
|
+
// Subscribe to the group relays
|
|
107
|
+
return this.relays$.pipe(
|
|
108
|
+
// Take a snapshot of relays (no updates yet...)
|
|
109
|
+
take(1),
|
|
110
|
+
// Every time they change switch to a new observable
|
|
111
|
+
switchMap((relays) => {
|
|
112
|
+
const observables = [];
|
|
113
|
+
for (const relay of relays) {
|
|
114
|
+
// If an upstream observable exists for this relay, use it
|
|
115
|
+
if (upstream.has(relay)) {
|
|
116
|
+
observables.push(upstream.get(relay));
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
// Create a new upstream observable for this relay
|
|
120
|
+
const observable = project(relay).pipe(
|
|
121
|
+
// Catch error and return as PublishResponse
|
|
122
|
+
errorToPublishResponse(relay));
|
|
123
|
+
observables.push(observable);
|
|
124
|
+
upstream.set(relay, observable);
|
|
125
|
+
}
|
|
126
|
+
return merge(...observables);
|
|
127
|
+
}),
|
|
128
|
+
// Ensure a single upstream
|
|
129
|
+
share());
|
|
26
130
|
}
|
|
27
131
|
/**
|
|
28
132
|
* Make a request to all relays
|
|
29
133
|
* @note This does not deduplicate events
|
|
30
134
|
*/
|
|
31
|
-
req(filters, id = nanoid(
|
|
32
|
-
|
|
33
|
-
// Ignore connection errors
|
|
34
|
-
catchError(() => of("EOSE"))));
|
|
35
|
-
// Merge events and the single EOSE stream
|
|
36
|
-
return this.mergeEOSE(requests);
|
|
135
|
+
req(filters, id = nanoid(), opts) {
|
|
136
|
+
return this.internalSubscription((relay) => relay.req(filters, id), opts?.eventStore ? filterDuplicateEvents(opts?.eventStore) : identity);
|
|
37
137
|
}
|
|
38
138
|
/** Send an event to all relays */
|
|
39
139
|
event(event) {
|
|
40
|
-
return
|
|
41
|
-
// Catch error and return as PublishResponse
|
|
42
|
-
catchError((err) => of({ ok: false, from: relay.url, message: err?.message || "Unknown error" })))));
|
|
140
|
+
return this.internalPublish((relay) => relay.event(event));
|
|
43
141
|
}
|
|
44
142
|
/** Negentropy sync events with the relays and an event store */
|
|
45
143
|
async negentropy(store, filter, reconcile, opts) {
|
|
@@ -57,25 +155,29 @@ export class RelayGroup {
|
|
|
57
155
|
}
|
|
58
156
|
/** Publish an event to all relays with retries ( default 3 retries ) */
|
|
59
157
|
publish(event, opts) {
|
|
60
|
-
return
|
|
61
|
-
// Catch error and return as PublishResponse
|
|
62
|
-
(err) => ({ ok: false, from: relay.url, message: err?.message || "Unknown error" }))));
|
|
158
|
+
return lastValueFrom(this.internalPublish((relay) => from(relay.publish(event, opts))).pipe(toArray(), defaultIfEmpty([])));
|
|
63
159
|
}
|
|
64
|
-
/** Request events from all relays
|
|
160
|
+
/** Request events from all relays and complete on EOSE */
|
|
65
161
|
request(filters, opts) {
|
|
66
|
-
return
|
|
67
|
-
//
|
|
68
|
-
|
|
162
|
+
return this.internalSubscription((relay) => relay.request(filters, opts).pipe(
|
|
163
|
+
// Simulate EOSE on completion
|
|
164
|
+
endWith("EOSE")),
|
|
69
165
|
// If an event store is provided, filter duplicate events
|
|
70
|
-
opts?.eventStore == null ? identity : filterDuplicateEvents(opts?.eventStore ?? new EventMemory()))
|
|
166
|
+
opts?.eventStore == null ? identity : filterDuplicateEvents(opts?.eventStore ?? new EventMemory())).pipe(
|
|
167
|
+
// Complete when all relays have sent EOSE
|
|
168
|
+
completeOnEose());
|
|
71
169
|
}
|
|
72
170
|
/** Open a subscription to all relays with retries ( default 3 retries ) */
|
|
73
171
|
subscription(filters, opts) {
|
|
74
|
-
return this.
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
172
|
+
return this.internalSubscription((relay) => relay.subscription(filters, opts),
|
|
173
|
+
// If an event store is provided, filter duplicate events
|
|
174
|
+
opts?.eventStore == null ? identity : filterDuplicateEvents(opts?.eventStore ?? new EventMemory()));
|
|
175
|
+
}
|
|
176
|
+
/** Count events on all relays in the group */
|
|
177
|
+
count(filters, id = nanoid()) {
|
|
178
|
+
return this.relays$.pipe(switchMap((relays) => combineLatest(Object.fromEntries(relays.map((relay) => [relay.url, relay.count(filters, id)])))),
|
|
179
|
+
// Ensure a single upstream
|
|
180
|
+
share());
|
|
79
181
|
}
|
|
80
182
|
/** Negentropy sync events with the relays and an event store */
|
|
81
183
|
sync(store, filter, direction) {
|
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
|
+
}
|