applesauce-relay 0.0.0-next-20260116173453 → 0.0.0-next-20260120162357
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 +1 -1
- package/dist/group.d.ts +3 -1
- package/dist/group.js +20 -1
- package/dist/pool.d.ts +3 -1
- package/dist/pool.js +16 -1
- package/dist/relay.d.ts +26 -2
- package/dist/relay.js +89 -9
- package/dist/types.d.ts +26 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ npm install applesauce-relay
|
|
|
22
22
|
|
|
23
23
|
## Examples
|
|
24
24
|
|
|
25
|
-
Read the [documentation](https://
|
|
25
|
+
Read the [documentation](https://applesauce.build/overview/relays.html) for more detailed explanation of all methods
|
|
26
26
|
|
|
27
27
|
### Single Relay
|
|
28
28
|
|
package/dist/group.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Filter, NostrEvent } from "applesauce-core/helpers";
|
|
|
3
3
|
import { BehaviorSubject, MonoTypeOperatorFunction, Observable } from "rxjs";
|
|
4
4
|
import { NegentropySyncOptions, type ReconcileFunction } from "./negentropy.js";
|
|
5
5
|
import { SyncDirection } from "./relay.js";
|
|
6
|
-
import { CountResponse, FilterInput, IGroup, IGroupRelayInput, IRelay, NegentropyReadStore, NegentropySyncStore, PublishOptions, PublishResponse, RequestOptions, SubscriptionOptions, SubscriptionResponse } from "./types.js";
|
|
6
|
+
import { CountResponse, FilterInput, IGroup, IGroupRelayInput, IRelay, NegentropyReadStore, NegentropySyncStore, PublishOptions, PublishResponse, RelayStatus, 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) */
|
|
@@ -21,6 +21,8 @@ export type GroupRequestOptions = RequestOptions & {
|
|
|
21
21
|
};
|
|
22
22
|
export declare class RelayGroup implements IGroup {
|
|
23
23
|
protected relays$: BehaviorSubject<IRelay[]> | Observable<IRelay[]>;
|
|
24
|
+
/** Observable of relay status for all relays in the group */
|
|
25
|
+
status$: Observable<Record<string, RelayStatus>>;
|
|
24
26
|
get relays(): IRelay[];
|
|
25
27
|
constructor(relays: IGroupRelayInput);
|
|
26
28
|
/** Whether this group is controlled by an upstream observable */
|
package/dist/group.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { EventMemory } from "applesauce-core/event-store";
|
|
2
2
|
import { filterDuplicateEvents } from "applesauce-core/observable";
|
|
3
3
|
import { nanoid } from "nanoid";
|
|
4
|
-
import { BehaviorSubject, catchError, combineLatest, defaultIfEmpty, defer, endWith, filter, from, identity, ignoreElements, lastValueFrom, map, merge, of, scan, share, switchMap, take, takeWhile, toArray, } from "rxjs";
|
|
4
|
+
import { BehaviorSubject, catchError, combineLatest, defaultIfEmpty, defer, endWith, filter, from, identity, ignoreElements, lastValueFrom, map, merge, of, scan, share, shareReplay, startWith, switchMap, take, takeWhile, toArray, } from "rxjs";
|
|
5
5
|
import { completeOnEose } from "./operators/complete-on-eose.js";
|
|
6
6
|
import { onlyEvents } from "./operators/only-events.js";
|
|
7
7
|
import { reverseSwitchMap } from "./operators/reverse-switch-map.js";
|
|
@@ -11,6 +11,8 @@ function errorToPublishResponse(relay) {
|
|
|
11
11
|
}
|
|
12
12
|
export class RelayGroup {
|
|
13
13
|
relays$ = new BehaviorSubject([]);
|
|
14
|
+
/** Observable of relay status for all relays in the group */
|
|
15
|
+
status$;
|
|
14
16
|
get relays() {
|
|
15
17
|
if (this.relays$ instanceof BehaviorSubject)
|
|
16
18
|
return this.relays$.value;
|
|
@@ -18,6 +20,23 @@ export class RelayGroup {
|
|
|
18
20
|
}
|
|
19
21
|
constructor(relays) {
|
|
20
22
|
this.relays$ = Array.isArray(relays) ? new BehaviorSubject(relays) : relays;
|
|
23
|
+
// Initialize status$ observable
|
|
24
|
+
this.status$ = this.relays$.pipe(switchMap((relays) => {
|
|
25
|
+
// If no relays, return empty record
|
|
26
|
+
if (relays.length === 0)
|
|
27
|
+
return of({});
|
|
28
|
+
// Merge all relay status streams
|
|
29
|
+
return merge(...relays.map((relay) => relay.status$)).pipe(
|
|
30
|
+
// Accumulate into a Record
|
|
31
|
+
scan((acc, status) => ({
|
|
32
|
+
...acc,
|
|
33
|
+
[status.url]: status,
|
|
34
|
+
}), {}),
|
|
35
|
+
// Start with initial empty state
|
|
36
|
+
startWith({}));
|
|
37
|
+
}),
|
|
38
|
+
// Share the subscription
|
|
39
|
+
shareReplay(1));
|
|
21
40
|
}
|
|
22
41
|
/** Whether this group is controlled by an upstream observable */
|
|
23
42
|
get controlled() {
|
package/dist/pool.d.ts
CHANGED
|
@@ -5,11 +5,13 @@ 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, NegentropyReadStore, NegentropySyncStore, PublishResponse, SubscriptionResponse } from "./types.js";
|
|
8
|
+
import type { CountResponse, FilterInput, IPool, IPoolRelayInput, IRelay, NegentropyReadStore, NegentropySyncStore, PublishResponse, RelayStatus, 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
|
+
/** Observable of relay status for all relays in the pool */
|
|
14
|
+
status$: Observable<Record<string, RelayStatus>>;
|
|
13
15
|
/** Whether to ignore relays that are ready=false */
|
|
14
16
|
ignoreOffline: boolean;
|
|
15
17
|
/** A signal when a relay is added */
|
package/dist/pool.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isFilterEqual } from "applesauce-core/helpers/filter";
|
|
2
2
|
import { createFilterMap } from "applesauce-core/helpers/relay-selection";
|
|
3
3
|
import { normalizeURL } from "applesauce-core/helpers/url";
|
|
4
|
-
import { BehaviorSubject, distinctUntilChanged, isObservable, map, of, Subject } from "rxjs";
|
|
4
|
+
import { BehaviorSubject, distinctUntilChanged, isObservable, map, merge, of, scan, shareReplay, startWith, Subject, switchMap, } from "rxjs";
|
|
5
5
|
import { RelayGroup } from "./group.js";
|
|
6
6
|
import { Relay } from "./relay.js";
|
|
7
7
|
export class RelayPool {
|
|
@@ -10,6 +10,8 @@ export class RelayPool {
|
|
|
10
10
|
get relays() {
|
|
11
11
|
return this.relays$.value;
|
|
12
12
|
}
|
|
13
|
+
/** Observable of relay status for all relays in the pool */
|
|
14
|
+
status$;
|
|
13
15
|
/** Whether to ignore relays that are ready=false */
|
|
14
16
|
ignoreOffline = true;
|
|
15
17
|
/** A signal when a relay is added */
|
|
@@ -18,6 +20,19 @@ export class RelayPool {
|
|
|
18
20
|
remove$ = new Subject();
|
|
19
21
|
constructor(options) {
|
|
20
22
|
this.options = options;
|
|
23
|
+
// Initialize status$ observable
|
|
24
|
+
this.status$ = this.relays$.pipe(
|
|
25
|
+
// Convert Map to array of relays
|
|
26
|
+
map((relayMap) => Array.from(relayMap.values())),
|
|
27
|
+
// Use same pattern as RelayGroup
|
|
28
|
+
switchMap((relays) => {
|
|
29
|
+
if (relays.length === 0)
|
|
30
|
+
return of({});
|
|
31
|
+
return merge(...relays.map((relay) => relay.status$)).pipe(scan((acc, status) => ({
|
|
32
|
+
...acc,
|
|
33
|
+
[status.url]: status,
|
|
34
|
+
}), {}), startWith({}));
|
|
35
|
+
}), shareReplay(1));
|
|
21
36
|
}
|
|
22
37
|
/** Get or create a new relay connection */
|
|
23
38
|
relay(url) {
|
package/dist/relay.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { logger } from "applesauce-core";
|
|
2
|
-
import { NostrEvent } from "applesauce-core/helpers/event";
|
|
2
|
+
import { KnownEvent, NostrEvent } from "applesauce-core/helpers/event";
|
|
3
3
|
import { Filter } from "applesauce-core/helpers/filter";
|
|
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, CountResponse, FilterInput, IRelay, NegentropyReadStore, NegentropySyncStore, PublishOptions, PublishResponse, RelayInformation, RequestOptions, SubscriptionOptions, SubscriptionResponse } from "./types.js";
|
|
7
|
+
import { AuthSigner, CountResponse, FilterInput, IRelay, NegentropyReadStore, NegentropySyncStore, PublishOptions, PublishResponse, RelayInformation, RelayStatus, RequestOptions, SubscriptionOptions, SubscriptionResponse } from "./types.js";
|
|
8
8
|
/** Flags for the negentropy sync type */
|
|
9
9
|
export declare enum SyncDirection {
|
|
10
10
|
RECEIVE = 1,
|
|
@@ -31,6 +31,13 @@ export type RelayOptions = {
|
|
|
31
31
|
pingFrequency?: number;
|
|
32
32
|
/** How long to wait for EOSE response in milliseconds (default 20000) */
|
|
33
33
|
pingTimeout?: number;
|
|
34
|
+
/** Policy for handling unresponsive connections (default: reconnect) */
|
|
35
|
+
onUnresponsive?: (info: {
|
|
36
|
+
url: string;
|
|
37
|
+
lastMessageAt: number;
|
|
38
|
+
now: number;
|
|
39
|
+
attempts: number;
|
|
40
|
+
}) => "reconnect" | "close" | "ignore";
|
|
34
41
|
/** Default retry config for subscription() method */
|
|
35
42
|
subscriptionRetry?: RetryConfig;
|
|
36
43
|
/** Default retry config for request() method */
|
|
@@ -56,6 +63,10 @@ export declare class Relay implements IRelay {
|
|
|
56
63
|
challenge$: BehaviorSubject<string | null>;
|
|
57
64
|
/** Boolean authentication state (will be false if auth failed) */
|
|
58
65
|
authenticated$: Observable<boolean>;
|
|
66
|
+
/** The pubkey of the authenticated user, or null if not authenticated */
|
|
67
|
+
authenticatedAs$: Observable<string | null>;
|
|
68
|
+
/** The authentication event sent to the relay */
|
|
69
|
+
authentication$: BehaviorSubject<KnownEvent<22242> | null>;
|
|
59
70
|
/** The response to the last AUTH message sent to the relay */
|
|
60
71
|
authenticationResponse$: BehaviorSubject<PublishResponse | null>;
|
|
61
72
|
/** The notices from the relay */
|
|
@@ -74,6 +85,11 @@ export declare class Relay implements IRelay {
|
|
|
74
85
|
notice$: Observable<string>;
|
|
75
86
|
/** Timestamp of the last message received from the relay */
|
|
76
87
|
private lastMessageReceivedAt;
|
|
88
|
+
/** Observable of the timestamp when last message was received */
|
|
89
|
+
private _lastMessageAt$;
|
|
90
|
+
lastMessageAt$: Observable<number>;
|
|
91
|
+
/** Observable of relay status (connection, authentication, and ready state) */
|
|
92
|
+
status$: Observable<RelayStatus>;
|
|
77
93
|
/** An observable that emits the NIP-11 information document for the relay */
|
|
78
94
|
information$: Observable<RelayInformation | null>;
|
|
79
95
|
protected _nip11: RelayInformation | null;
|
|
@@ -89,13 +105,19 @@ export declare class Relay implements IRelay {
|
|
|
89
105
|
close$: Subject<CloseEvent>;
|
|
90
106
|
/** An observable that emits when underlying websocket is closing due to unsubscription */
|
|
91
107
|
closing$: Subject<void>;
|
|
108
|
+
/** Tracks active req() operations by subscription ID */
|
|
109
|
+
reqs$: BehaviorSubject<Record<string, Filter[]>>;
|
|
92
110
|
get ready(): boolean;
|
|
93
111
|
get connected(): boolean;
|
|
94
112
|
get challenge(): string | null;
|
|
95
113
|
get notices(): string[];
|
|
96
114
|
get authenticated(): boolean;
|
|
115
|
+
get authentication(): KnownEvent<22242> | null;
|
|
116
|
+
get authenticatedAs(): string | null;
|
|
97
117
|
get authenticationResponse(): PublishResponse | null;
|
|
98
118
|
get information(): RelayInformation | null;
|
|
119
|
+
get lastMessageAt(): number;
|
|
120
|
+
get reqs(): Record<string, Filter[]>;
|
|
99
121
|
/** If an EOSE message is not seen in this time, emit one locally (default 10s) */
|
|
100
122
|
eoseTimeout: number;
|
|
101
123
|
/** How long to wait for an OK message from the relay (default 10s) */
|
|
@@ -116,6 +138,8 @@ export declare class Relay implements IRelay {
|
|
|
116
138
|
protected requestReconnect: RetryConfig;
|
|
117
139
|
/** Default retry config for publish() method */
|
|
118
140
|
protected publishRetry: RetryConfig;
|
|
141
|
+
/** Policy hook for unresponsive connections */
|
|
142
|
+
protected onUnresponsive?: RelayOptions["onUnresponsive"];
|
|
119
143
|
protected receivedAuthRequiredForReq: BehaviorSubject<boolean>;
|
|
120
144
|
protected receivedAuthRequiredForEvent: BehaviorSubject<boolean>;
|
|
121
145
|
authRequiredForRead$: Observable<boolean>;
|
package/dist/relay.js
CHANGED
|
@@ -47,6 +47,10 @@ export class Relay {
|
|
|
47
47
|
challenge$ = new BehaviorSubject(null);
|
|
48
48
|
/** Boolean authentication state (will be false if auth failed) */
|
|
49
49
|
authenticated$;
|
|
50
|
+
/** The pubkey of the authenticated user, or null if not authenticated */
|
|
51
|
+
authenticatedAs$;
|
|
52
|
+
/** The authentication event sent to the relay */
|
|
53
|
+
authentication$ = new BehaviorSubject(null);
|
|
50
54
|
/** The response to the last AUTH message sent to the relay */
|
|
51
55
|
authenticationResponse$ = new BehaviorSubject(null);
|
|
52
56
|
/** The notices from the relay */
|
|
@@ -65,6 +69,11 @@ export class Relay {
|
|
|
65
69
|
notice$;
|
|
66
70
|
/** Timestamp of the last message received from the relay */
|
|
67
71
|
lastMessageReceivedAt = 0;
|
|
72
|
+
/** Observable of the timestamp when last message was received */
|
|
73
|
+
_lastMessageAt$ = new BehaviorSubject(0);
|
|
74
|
+
lastMessageAt$ = this._lastMessageAt$.asObservable();
|
|
75
|
+
/** Observable of relay status (connection, authentication, and ready state) */
|
|
76
|
+
status$;
|
|
68
77
|
/** An observable that emits the NIP-11 information document for the relay */
|
|
69
78
|
information$;
|
|
70
79
|
_nip11 = null;
|
|
@@ -80,6 +89,8 @@ export class Relay {
|
|
|
80
89
|
close$ = new Subject();
|
|
81
90
|
/** An observable that emits when underlying websocket is closing due to unsubscription */
|
|
82
91
|
closing$ = new Subject();
|
|
92
|
+
/** Tracks active req() operations by subscription ID */
|
|
93
|
+
reqs$ = new BehaviorSubject({});
|
|
83
94
|
// sync state
|
|
84
95
|
get ready() {
|
|
85
96
|
return this._ready$.value;
|
|
@@ -96,12 +107,24 @@ export class Relay {
|
|
|
96
107
|
get authenticated() {
|
|
97
108
|
return this.authenticationResponse?.ok === true;
|
|
98
109
|
}
|
|
110
|
+
get authentication() {
|
|
111
|
+
return this.authentication$.value;
|
|
112
|
+
}
|
|
113
|
+
get authenticatedAs() {
|
|
114
|
+
return this.authenticated ? (this.authentication?.pubkey ?? null) : null;
|
|
115
|
+
}
|
|
99
116
|
get authenticationResponse() {
|
|
100
117
|
return this.authenticationResponse$.value;
|
|
101
118
|
}
|
|
102
119
|
get information() {
|
|
103
120
|
return this._nip11;
|
|
104
121
|
}
|
|
122
|
+
get lastMessageAt() {
|
|
123
|
+
return this._lastMessageAt$.value;
|
|
124
|
+
}
|
|
125
|
+
get reqs() {
|
|
126
|
+
return this.reqs$.value;
|
|
127
|
+
}
|
|
105
128
|
/** If an EOSE message is not seen in this time, emit one locally (default 10s) */
|
|
106
129
|
eoseTimeout = 10_000;
|
|
107
130
|
/** How long to wait for an OK message from the relay (default 10s) */
|
|
@@ -122,6 +145,8 @@ export class Relay {
|
|
|
122
145
|
requestReconnect;
|
|
123
146
|
/** Default retry config for publish() method */
|
|
124
147
|
publishRetry;
|
|
148
|
+
/** Policy hook for unresponsive connections */
|
|
149
|
+
onUnresponsive;
|
|
125
150
|
// Subjects that track if an "auth-required" message has been received for REQ or EVENT
|
|
126
151
|
receivedAuthRequiredForReq = new BehaviorSubject(false);
|
|
127
152
|
receivedAuthRequiredForEvent = new BehaviorSubject(false);
|
|
@@ -134,6 +159,8 @@ export class Relay {
|
|
|
134
159
|
this.challenge$.next(null);
|
|
135
160
|
if (this.authenticationResponse$.value)
|
|
136
161
|
this.authenticationResponse$.next(null);
|
|
162
|
+
if (this.authentication$.value !== null)
|
|
163
|
+
this.authentication$.next(null);
|
|
137
164
|
if (this.notices$.value.length > 0)
|
|
138
165
|
this.notices$.next([]);
|
|
139
166
|
if (this.receivedAuthRequiredForReq.value)
|
|
@@ -161,12 +188,16 @@ export class Relay {
|
|
|
161
188
|
this.pingFrequency = opts.pingFrequency;
|
|
162
189
|
if (opts?.pingTimeout !== undefined)
|
|
163
190
|
this.pingTimeout = opts.pingTimeout;
|
|
191
|
+
if (opts?.onUnresponsive !== undefined)
|
|
192
|
+
this.onUnresponsive = opts.onUnresponsive;
|
|
164
193
|
// Set retry configs
|
|
165
194
|
this.subscriptionReconnect = { ...DEFAULT_RETRY_CONFIG, ...(opts?.subscriptionRetry ?? {}) };
|
|
166
195
|
this.requestReconnect = { ...DEFAULT_RETRY_CONFIG, ...(opts?.requestRetry ?? {}) };
|
|
167
196
|
this.publishRetry = { ...DEFAULT_RETRY_CONFIG, ...(opts?.publishRetry ?? {}) };
|
|
168
197
|
// Create an observable that tracks boolean authentication state
|
|
169
198
|
this.authenticated$ = this.authenticationResponse$.pipe(map((response) => response?.ok === true));
|
|
199
|
+
// Create an observable that returns the pubkey when authenticated, or null otherwise
|
|
200
|
+
this.authenticatedAs$ = combineLatest([this.authenticated$, this.authentication$]).pipe(map(([authenticated, authEvent]) => (authenticated && authEvent ? authEvent.pubkey : null)));
|
|
170
201
|
/** Use the static method to create a new reconnect method for this relay */
|
|
171
202
|
this.reconnectTimer = Relay.createReconnectTimer(url);
|
|
172
203
|
// Subscribe to open and close events
|
|
@@ -225,6 +256,16 @@ export class Relay {
|
|
|
225
256
|
this.authRequiredForPublish$
|
|
226
257
|
.pipe(filter((r) => r === true), take(1))
|
|
227
258
|
.subscribe(() => this.log("Auth required for EVENT"));
|
|
259
|
+
// Create status$ observable by combining state observables
|
|
260
|
+
this.status$ = combineLatest({
|
|
261
|
+
url: of(this.url),
|
|
262
|
+
connected: this.connected$,
|
|
263
|
+
authenticated: this.authenticated$,
|
|
264
|
+
authenticatedAs: this.authenticatedAs$,
|
|
265
|
+
ready: this._ready$,
|
|
266
|
+
authRequiredForRead: this.authRequiredForRead$,
|
|
267
|
+
authRequiredForPublish: this.authRequiredForPublish$,
|
|
268
|
+
}).pipe(shareReplay(1));
|
|
228
269
|
// Update the notices state
|
|
229
270
|
const listenForNotice = this.socket.pipe(
|
|
230
271
|
// listen for NOTICE messages
|
|
@@ -249,7 +290,9 @@ export class Relay {
|
|
|
249
290
|
const allMessagesSubject = new Subject();
|
|
250
291
|
const listenForAllMessages = this.socket.pipe(tap((message) => {
|
|
251
292
|
// Update the last message received at timestamp
|
|
252
|
-
|
|
293
|
+
const now = Date.now();
|
|
294
|
+
this.lastMessageReceivedAt = now;
|
|
295
|
+
this._lastMessageAt$.next(now);
|
|
253
296
|
// Pass to the message subject
|
|
254
297
|
allMessagesSubject.next(message);
|
|
255
298
|
}));
|
|
@@ -274,20 +317,46 @@ export class Relay {
|
|
|
274
317
|
// Skip ping if we have received a message in the last pingFrequency milliseconds
|
|
275
318
|
if (Date.now() - this.lastMessageReceivedAt < this.pingFrequency)
|
|
276
319
|
return NEVER;
|
|
277
|
-
//
|
|
278
|
-
|
|
279
|
-
|
|
320
|
+
// Generate unique ping ID for correlation
|
|
321
|
+
const pingId = "ping:" + nanoid();
|
|
322
|
+
this.send(["REQ", pingId, PING_FILTER]);
|
|
323
|
+
// Wait for the EOSE or CLOSED response for this specific ping
|
|
280
324
|
return this.message$.pipe(
|
|
281
|
-
//
|
|
325
|
+
// Wait specifically for response to OUR ping
|
|
326
|
+
filter((m) => Array.isArray(m) && (m[0] === "EOSE" || m[0] === "CLOSED") && m[1] === pingId),
|
|
327
|
+
// Complete after first matching message received
|
|
282
328
|
take(1),
|
|
283
329
|
// Add timeout to detect unresponsive connections
|
|
284
330
|
timeout({
|
|
285
331
|
first: this.pingTimeout,
|
|
286
332
|
with: () => {
|
|
287
|
-
|
|
333
|
+
// Determine action via policy hook (default: reconnect)
|
|
334
|
+
const now = Date.now();
|
|
335
|
+
const action = this.onUnresponsive?.({
|
|
336
|
+
url: this.url,
|
|
337
|
+
lastMessageAt: this.lastMessageReceivedAt,
|
|
338
|
+
now,
|
|
339
|
+
attempts: this.attempts$.value,
|
|
340
|
+
}) ?? "reconnect";
|
|
341
|
+
const err = new Error(`Relay ping timeout after ${this.pingTimeout}ms`);
|
|
342
|
+
if (action === "reconnect") {
|
|
343
|
+
this.log("Relay connection has become unresponsive, triggering reconnect");
|
|
344
|
+
this.startReconnectTimer(err);
|
|
345
|
+
}
|
|
346
|
+
else if (action === "close") {
|
|
347
|
+
this.log("Relay connection has become unresponsive, closing connection");
|
|
348
|
+
this.error$.next(err);
|
|
349
|
+
this.socket.complete();
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
// "ignore" - log but don't take action
|
|
353
|
+
this.log("Relay connection has become unresponsive (ignoring per policy)");
|
|
354
|
+
}
|
|
288
355
|
return NEVER;
|
|
289
356
|
},
|
|
290
|
-
})
|
|
357
|
+
}),
|
|
358
|
+
// Close the ping subscription when done
|
|
359
|
+
finalize(() => this.send(["CLOSE", pingId])));
|
|
291
360
|
}));
|
|
292
361
|
}),
|
|
293
362
|
// Catch errors to prevent breaking the watchTower
|
|
@@ -379,9 +448,18 @@ export class Relay {
|
|
|
379
448
|
// Create an observable that controls sending the filters and closing the REQ
|
|
380
449
|
const control = input.pipe(
|
|
381
450
|
// Send the filters when they change
|
|
382
|
-
tap((filters) =>
|
|
451
|
+
tap((filters) => {
|
|
452
|
+
this.socket.next(["REQ", id, ...filters]);
|
|
453
|
+
// Add to tracking when REQ is sent
|
|
454
|
+
this.reqs$.next({ ...this.reqs$.value, [id]: filters });
|
|
455
|
+
}),
|
|
383
456
|
// Send the CLOSE message when unsubscribed or input completes
|
|
384
|
-
finalize(() =>
|
|
457
|
+
finalize(() => {
|
|
458
|
+
this.socket.next(["CLOSE", id]);
|
|
459
|
+
// Remove from tracking when REQ closes
|
|
460
|
+
const { [id]: _, ...rest } = this.reqs$.value;
|
|
461
|
+
this.reqs$.next(rest);
|
|
462
|
+
}),
|
|
385
463
|
// Once filters have been sent, switch to listening for messages
|
|
386
464
|
switchMap(() => messages));
|
|
387
465
|
// Start the watch tower with the observables
|
|
@@ -481,6 +559,8 @@ export class Relay {
|
|
|
481
559
|
}
|
|
482
560
|
/** send and AUTH message */
|
|
483
561
|
auth(event) {
|
|
562
|
+
// Save the authentication event
|
|
563
|
+
this.authentication$.next(event);
|
|
484
564
|
return lastValueFrom(this.event(event, "AUTH").pipe(
|
|
485
565
|
// update authenticated
|
|
486
566
|
tap((result) => this.authenticationResponse$.next(result))));
|
package/dist/types.d.ts
CHANGED
|
@@ -16,6 +16,23 @@ export type PublishResponse = {
|
|
|
16
16
|
export type CountResponse = {
|
|
17
17
|
count: number;
|
|
18
18
|
};
|
|
19
|
+
/** Status information for a single relay */
|
|
20
|
+
export interface RelayStatus {
|
|
21
|
+
/** Relay URL */
|
|
22
|
+
url: string;
|
|
23
|
+
/** WebSocket connection state (true = socket is open) */
|
|
24
|
+
connected: boolean;
|
|
25
|
+
/** Authentication state (true = successfully authenticated) */
|
|
26
|
+
authenticated: boolean;
|
|
27
|
+
/** The pubkey of the authenticated user, or null if not authenticated */
|
|
28
|
+
authenticatedAs: string | null;
|
|
29
|
+
/** Application-layer ready state (true = safe to use) */
|
|
30
|
+
ready: boolean;
|
|
31
|
+
/** Whether authentication is required for read operations (REQ/COUNT) */
|
|
32
|
+
authRequiredForRead: boolean;
|
|
33
|
+
/** Whether authentication is required for publish operations (EVENT) */
|
|
34
|
+
authRequiredForPublish: boolean;
|
|
35
|
+
}
|
|
19
36
|
export type MultiplexWebSocket<T = any> = Pick<WebSocketSubject<T>, "multiplex">;
|
|
20
37
|
/** Options for the publish method on the pool and relay */
|
|
21
38
|
export type PublishOptions = {
|
|
@@ -66,6 +83,7 @@ export interface IRelay extends MultiplexWebSocket {
|
|
|
66
83
|
message$: Observable<any>;
|
|
67
84
|
notice$: Observable<string>;
|
|
68
85
|
connected$: Observable<boolean>;
|
|
86
|
+
ready$: Observable<boolean>;
|
|
69
87
|
challenge$: Observable<string | null>;
|
|
70
88
|
authenticated$: Observable<boolean>;
|
|
71
89
|
notices$: Observable<string[]>;
|
|
@@ -73,10 +91,14 @@ export interface IRelay extends MultiplexWebSocket {
|
|
|
73
91
|
close$: Observable<CloseEvent>;
|
|
74
92
|
closing$: Observable<void>;
|
|
75
93
|
error$: Observable<Error | null>;
|
|
94
|
+
lastMessageAt$: Observable<number>;
|
|
95
|
+
status$: Observable<RelayStatus>;
|
|
76
96
|
readonly connected: boolean;
|
|
97
|
+
readonly ready: boolean;
|
|
77
98
|
readonly authenticated: boolean;
|
|
78
99
|
readonly challenge: string | null;
|
|
79
100
|
readonly notices: string[];
|
|
101
|
+
readonly lastMessageAt: number;
|
|
80
102
|
/** Force close the connection */
|
|
81
103
|
close(): void;
|
|
82
104
|
/** Send a REQ message */
|
|
@@ -108,6 +130,8 @@ export interface IRelay extends MultiplexWebSocket {
|
|
|
108
130
|
}
|
|
109
131
|
export type IGroupRelayInput = IRelay[] | Observable<IRelay[]>;
|
|
110
132
|
export interface IGroup {
|
|
133
|
+
/** Observable of relay status for all relays in the group */
|
|
134
|
+
status$: Observable<Record<string, RelayStatus>>;
|
|
111
135
|
/** Send a REQ message */
|
|
112
136
|
req(filters: Parameters<IRelay["req"]>[0], id?: string): Observable<SubscriptionResponse>;
|
|
113
137
|
/** Send an EVENT message */
|
|
@@ -138,6 +162,8 @@ export interface IPoolSignals {
|
|
|
138
162
|
}
|
|
139
163
|
export type IPoolRelayInput = string[] | Observable<string[]>;
|
|
140
164
|
export interface IPool extends IPoolSignals {
|
|
165
|
+
/** Observable of relay status for all relays in the pool */
|
|
166
|
+
status$: Observable<Record<string, RelayStatus>>;
|
|
141
167
|
/** Get or create a relay */
|
|
142
168
|
relay(url: string): IRelay;
|
|
143
169
|
/** Create a relay group */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "applesauce-relay",
|
|
3
|
-
"version": "0.0.0-next-
|
|
3
|
+
"version": "0.0.0-next-20260120162357",
|
|
4
4
|
"description": "nostr relay communication framework built on rxjs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
59
|
"@noble/hashes": "^1.7.1",
|
|
60
|
-
"applesauce-core": "0.0.0-next-
|
|
60
|
+
"applesauce-core": "0.0.0-next-20260120162357",
|
|
61
61
|
"nanoid": "^5.0.9",
|
|
62
62
|
"nostr-tools": "~2.19",
|
|
63
63
|
"rxjs": "^7.8.1"
|