applesauce-relay 0.0.0-next-20260116173453 → 0.0.0-next-20260121015157

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 CHANGED
@@ -22,7 +22,7 @@ npm install applesauce-relay
22
22
 
23
23
  ## Examples
24
24
 
25
- Read the [documentation](https://hzrd149.github.io/applesauce/overview/relays.html) for more detailed explanation of all methods
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
- this.lastMessageReceivedAt = Date.now();
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
- // Send a ping request
278
- this.send(["REQ", "ping:" + nanoid(), PING_FILTER]);
279
- // Wait for the EOSE response
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
- // Complete after first message received
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
- console.warn(`Relay connection has become unresponsive: ${this.url}`);
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) => this.socket.next(["REQ", id, ...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(() => this.socket.next(["CLOSE", id])),
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-20260116173453",
3
+ "version": "0.0.0-next-20260121015157",
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-20260116173453",
60
+ "applesauce-core": "0.0.0-next-20260121015157",
61
61
  "nanoid": "^5.0.9",
62
62
  "nostr-tools": "~2.19",
63
63
  "rxjs": "^7.8.1"