applesauce-relay 0.0.0-next-20250327153627 → 0.0.0-next-20250330150216

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.
@@ -0,0 +1,10 @@
1
+ import { NostrEvent } from "nostr-tools";
2
+ import { Observable } from "rxjs";
3
+ import { Filter } from "nostr-tools";
4
+ import { IRelay, Nip01Actions, PublishResponse, SubscriptionResponse } from "./types.js";
5
+ export declare class RelayGroup implements Nip01Actions {
6
+ relays: IRelay[];
7
+ constructor(relays: IRelay[]);
8
+ req(filters: Filter | Filter[], id?: string): Observable<SubscriptionResponse>;
9
+ event(event: NostrEvent): Observable<PublishResponse>;
10
+ }
package/dist/group.js ADDED
@@ -0,0 +1,23 @@
1
+ import { combineLatest, filter, map, merge } from "rxjs";
2
+ import { onlyEvents } from "./operators/only-events.js";
3
+ import { nanoid } from "nanoid";
4
+ export class RelayGroup {
5
+ relays;
6
+ constructor(relays) {
7
+ this.relays = relays;
8
+ }
9
+ req(filters, id = nanoid()) {
10
+ const requests = this.relays.reduce((acc, r) => ({ ...acc, [r.url]: r.req(filters, id) }), {});
11
+ // Create stream of events only
12
+ const events = merge(...Object.values(requests)).pipe(onlyEvents());
13
+ // Create stream that emits EOSE when all relays have sent EOSE
14
+ const eose = combineLatest(
15
+ // Create a new map of requests that only emits EOSE
16
+ Object.fromEntries(Object.entries(requests).map(([url, observable]) => [url, observable.pipe(filter((m) => m === "EOSE"))]))).pipe(map(() => "EOSE"));
17
+ // Merge events and the single EOSE stream
18
+ return merge(events, eose);
19
+ }
20
+ event(event) {
21
+ return merge(...this.relays.map((r) => r.event(event)));
22
+ }
23
+ }
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
+ export * from "./group.js";
1
2
  export * from "./pool.js";
2
3
  export * from "./relay.js";
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
+ export * from "./group.js";
1
2
  export * from "./pool.js";
2
3
  export * from "./relay.js";
@@ -1,4 +1,4 @@
1
1
  import { MonoTypeOperatorFunction } from "rxjs";
2
- import { SubscriptionResponse } from "../relay.js";
2
+ import { SubscriptionResponse } from "../types.js";
3
3
  /** Marks all events as from the relay */
4
4
  export declare function markFromRelay(relay: string): MonoTypeOperatorFunction<SubscriptionResponse>;
@@ -1,5 +1,5 @@
1
1
  import { OperatorFunction } from "rxjs";
2
2
  import { NostrEvent } from "nostr-tools";
3
- import { SubscriptionResponse } from "../relay.js";
3
+ import { SubscriptionResponse } from "../types.js";
4
4
  /** Filter subscription responses and only return the events */
5
5
  export declare function onlyEvents(): OperatorFunction<SubscriptionResponse, NostrEvent>;
package/dist/pool.d.ts CHANGED
@@ -1,12 +1,19 @@
1
1
  import { NostrEvent, type Filter } from "nostr-tools";
2
2
  import { Observable } from "rxjs";
3
- import { Relay, PublishResponse, SubscriptionResponse } from "./relay.js";
3
+ import { Relay, RelayOptions } from "./relay.js";
4
+ import { PublishResponse, SubscriptionResponse } from "./types.js";
5
+ import { RelayGroup } from "./group.js";
4
6
  export declare class RelayPool {
7
+ options?: RelayOptions | undefined;
5
8
  relays: Map<string, Relay>;
9
+ groups: Map<string, RelayGroup>;
10
+ constructor(options?: RelayOptions | undefined);
6
11
  /** Get or create a new relay connection */
7
12
  relay(url: string): Relay;
8
- /** Make a REQ to multiple relays but does not deduplicate events */
9
- req(relays: string[], filters: Filter[], id?: string): Observable<SubscriptionResponse>;
13
+ /** Create a group of relays */
14
+ group(relays: string[]): RelayGroup;
15
+ /** Make a REQ to multiple relays that does not deduplicate events */
16
+ req(relays: string[], filters: Filter | Filter[], id?: string): Observable<SubscriptionResponse>;
10
17
  /** Send an EVENT message to multiple relays */
11
- event(relays: string[], event: NostrEvent): Observable<PublishResponse[]>;
18
+ event(relays: string[], event: NostrEvent): Observable<PublishResponse>;
12
19
  }
package/dist/pool.js CHANGED
@@ -1,41 +1,39 @@
1
- import { combineLatest, endWith, ignoreElements, merge, takeWhile } from "rxjs";
2
- import { nanoid } from "nanoid";
3
1
  import { Relay } from "./relay.js";
4
- import { onlyEvents } from "./operators/only-events.js";
2
+ import { RelayGroup } from "./group.js";
5
3
  export class RelayPool {
4
+ options;
6
5
  relays = new Map();
6
+ groups = new Map();
7
+ constructor(options) {
8
+ this.options = options;
9
+ }
7
10
  /** Get or create a new relay connection */
8
11
  relay(url) {
9
12
  let relay = this.relays.get(url);
10
13
  if (relay)
11
14
  return relay;
12
15
  else {
13
- relay = new Relay(url);
16
+ relay = new Relay(url, this.options);
14
17
  this.relays.set(url, relay);
15
18
  return relay;
16
19
  }
17
20
  }
18
- /** Make a REQ to multiple relays but does not deduplicate events */
19
- req(relays, filters, id = nanoid()) {
20
- // create a REQ observable for each relay
21
- const requests = relays.map((url) => this.relay(url).req(filters, id));
22
- // create an observable that completes when all relays send EOSE
23
- const eose = merge(
24
- // create an array of observables for each relay that completes when EOSE
25
- ...requests.map((o) => o.pipe(
26
- // complete on EOSE message
27
- takeWhile((m) => m !== "EOSE")))).pipe(
28
- // ignore all messages
29
- ignoreElements(),
30
- // emit EOSE on complete
31
- endWith("EOSE"));
32
- // create a stream that only emits events
33
- const events = merge(...requests).pipe(onlyEvents());
34
- // merge events and single EOSE streams
35
- return merge(events, eose);
21
+ /** Create a group of relays */
22
+ group(relays) {
23
+ const key = relays.sort().join(",");
24
+ let group = this.groups.get(key);
25
+ if (group)
26
+ return group;
27
+ group = new RelayGroup(relays.map((url) => this.relay(url)));
28
+ this.groups.set(key, group);
29
+ return group;
30
+ }
31
+ /** Make a REQ to multiple relays that does not deduplicate events */
32
+ req(relays, filters, id) {
33
+ return this.group(relays).req(filters, id);
36
34
  }
37
35
  /** Send an EVENT message to multiple relays */
38
36
  event(relays, event) {
39
- return combineLatest(relays.map((url) => this.relay(url).event(event)));
37
+ return this.group(relays).event(event);
40
38
  }
41
39
  }
package/dist/relay.d.ts CHANGED
@@ -2,29 +2,25 @@ import { BehaviorSubject, Observable } from "rxjs";
2
2
  import { WebSocketSubject, WebSocketSubjectConfig } from "rxjs/webSocket";
3
3
  import { type Filter, type NostrEvent } from "nostr-tools";
4
4
  import { logger } from "applesauce-core";
5
- export type SubscriptionResponse = "EOSE" | NostrEvent;
6
- export type PublishResponse = {
7
- ok: boolean;
8
- message?: string;
9
- from: string;
10
- };
5
+ import { IRelay, PublishResponse, SubscriptionResponse } from "./types.js";
11
6
  export type RelayOptions = {
12
7
  WebSocket?: WebSocketSubjectConfig<any>["WebSocketCtor"];
13
8
  };
14
- export declare class Relay {
9
+ export declare class Relay implements IRelay {
15
10
  url: string;
16
- log: typeof logger;
11
+ protected log: typeof logger;
17
12
  socket$: WebSocketSubject<any>;
18
13
  connected$: BehaviorSubject<boolean>;
19
14
  challenge$: Observable<string>;
20
15
  authenticated$: BehaviorSubject<boolean>;
21
- notices$: Observable<string>;
16
+ notice$: Observable<string>;
22
17
  protected authRequiredForReq: BehaviorSubject<boolean>;
23
18
  protected authRequiredForPublish: BehaviorSubject<boolean>;
24
19
  protected reset(): void;
25
20
  constructor(url: string, opts?: RelayOptions);
26
21
  protected waitForAuth<T extends unknown = unknown>(requireAuth: Observable<boolean>, observable: Observable<T>): Observable<T>;
27
- req(filters: Filter[], id?: string): Observable<SubscriptionResponse>;
22
+ multiplex<T>(open: () => any, close: () => any, filter: (message: any) => boolean): Observable<T>;
23
+ req(filters: Filter | Filter[], id?: string): Observable<SubscriptionResponse>;
28
24
  /** send an Event message */
29
25
  event(event: NostrEvent, verb?: "EVENT" | "AUTH"): Observable<PublishResponse>;
30
26
  /** send and AUTH message */
package/dist/relay.js CHANGED
@@ -1,22 +1,26 @@
1
- import { BehaviorSubject, combineLatest, EMPTY, filter, map, merge, NEVER, of, shareReplay, switchMap, take, takeWhile, tap, timeout, } from "rxjs";
1
+ import { BehaviorSubject, combineLatest, filter, map, merge, NEVER, of, shareReplay, switchMap, take, takeWhile, tap, timeout, } from "rxjs";
2
2
  import { webSocket } from "rxjs/webSocket";
3
3
  import { nanoid } from "nanoid";
4
4
  import { logger } from "applesauce-core";
5
5
  import { markFromRelay } from "./operators/mark-from-relay.js";
6
6
  export class Relay {
7
7
  url;
8
- log = logger.extend("Bakery");
8
+ log = logger.extend("Relay");
9
9
  socket$;
10
10
  connected$ = new BehaviorSubject(false);
11
11
  challenge$;
12
12
  authenticated$ = new BehaviorSubject(false);
13
- notices$;
13
+ notice$;
14
14
  authRequiredForReq = new BehaviorSubject(false);
15
15
  authRequiredForPublish = new BehaviorSubject(false);
16
16
  reset() {
17
- this.authenticated$.next(false);
18
- this.authRequiredForReq.next(false);
19
- this.authRequiredForPublish.next(false);
17
+ // NOTE: only update the values if they need to be changed, otherwise this will cause an infinite loop
18
+ if (this.authenticated$.value)
19
+ this.authenticated$.next(false);
20
+ if (this.authRequiredForReq.value)
21
+ this.authRequiredForReq.next(false);
22
+ if (this.authRequiredForPublish.value)
23
+ this.authRequiredForPublish.next(false);
20
24
  }
21
25
  constructor(url, opts) {
22
26
  this.url = url;
@@ -47,7 +51,7 @@ export class Relay {
47
51
  map((m) => m[1]),
48
52
  // cache and share the challenge
49
53
  shareReplay(1));
50
- this.notices$ = this.socket$.pipe(
54
+ this.notice$ = this.socket$.pipe(
51
55
  // listen for NOTICE messages
52
56
  filter((m) => m[0] === "NOTICE"),
53
57
  // pick the string out of the message
@@ -58,14 +62,17 @@ export class Relay {
58
62
  // return EMPTY if auth is required and not authenticated
59
63
  switchMap(([required, authenticated]) => {
60
64
  if (required && !authenticated)
61
- return EMPTY;
65
+ return NEVER;
62
66
  else
63
67
  return observable;
64
68
  }));
65
69
  }
70
+ multiplex(open, close, filter) {
71
+ return this.socket$.multiplex(open, close, filter);
72
+ }
66
73
  req(filters, id = nanoid()) {
67
74
  return this.waitForAuth(this.authRequiredForReq, this.socket$
68
- .multiplex(() => ["REQ", id, ...filters], () => ["CLOSE", id], (message) => (message[0] === "EVENT" || message[0] === "CLOSE" || message[0] === "EOSE") && message[1] === id)
75
+ .multiplex(() => (Array.isArray(filters) ? ["REQ", id, ...filters] : ["REQ", id, filters]), () => ["CLOSE", id], (message) => (message[0] === "EVENT" || message[0] === "CLOSE" || message[0] === "EOSE") && message[1] === id)
69
76
  .pipe(
70
77
  // listen for CLOSE auth-required
71
78
  tap((m) => {
@@ -85,6 +92,7 @@ export class Relay {
85
92
  // mark events as from relays
86
93
  markFromRelay(this.url),
87
94
  // if no events are seen in 10s, emit EOSE
95
+ // TODO: this should emit EOSE event if events are seen, the timeout should be for only the EOSE message
88
96
  timeout({
89
97
  first: 10_000,
90
98
  with: () => merge(of("EOSE"), NEVER),
@@ -0,0 +1,36 @@
1
+ import { Filter, NostrEvent } from "nostr-tools";
2
+ import { Observable } from "rxjs";
3
+ import { WebSocketSubject } from "rxjs/webSocket";
4
+ export type SubscriptionResponse = "EOSE" | NostrEvent;
5
+ export type PublishResponse = {
6
+ ok: boolean;
7
+ message?: string;
8
+ from: string;
9
+ };
10
+ export type MultiplexWebSocket<T = any> = Pick<WebSocketSubject<T>, "multiplex">;
11
+ export interface IRelayState {
12
+ connected$: Observable<boolean>;
13
+ challenge$: Observable<string>;
14
+ authenticated$: Observable<boolean>;
15
+ notice$: Observable<string>;
16
+ }
17
+ export interface Nip01Actions {
18
+ /** Send an EVENT message */
19
+ event(event: NostrEvent): Observable<PublishResponse>;
20
+ /** Send a REQ message */
21
+ req(filters: Filter | Filter[], id?: string): Observable<SubscriptionResponse>;
22
+ }
23
+ export interface IRelay extends MultiplexWebSocket, Nip01Actions, IRelayState {
24
+ url: string;
25
+ /** Send an AUTH message */
26
+ auth(event: NostrEvent): Observable<{
27
+ ok: boolean;
28
+ message?: string;
29
+ }>;
30
+ }
31
+ export interface IPoolActions {
32
+ /** Send an EVENT message */
33
+ event(relays: string[], event: NostrEvent): Observable<PublishResponse[]>;
34
+ /** Send a REQ message */
35
+ req(relays: string[], filters: Filter | Filter[]): Observable<SubscriptionResponse[]>;
36
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-relay",
3
- "version": "0.0.0-next-20250327153627",
3
+ "version": "0.0.0-next-20250330150216",
4
4
  "description": "A collection of observable based loaders built on rx-nostr",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -33,7 +33,7 @@
33
33
  }
34
34
  },
35
35
  "dependencies": {
36
- "applesauce-core": "0.0.0-next-20250327153627",
36
+ "applesauce-core": "0.0.0-next-20250330150216",
37
37
  "nanoid": "^5.0.9",
38
38
  "nostr-tools": "^2.10.4",
39
39
  "rxjs": "^7.8.1"