applesauce-relay 0.0.0-next-20250330150216 → 0.0.0-next-20250330153313

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
@@ -1,3 +1,97 @@
1
1
  # Applesauce Relay
2
2
 
3
3
  `applesauce-relay` is a nostr relay communication framework built on top of [RxJS](https://rxjs.dev/)
4
+
5
+ ## Examples
6
+
7
+ ### Single Relay
8
+
9
+ ```typescript
10
+ import { Relay } from "./relay";
11
+
12
+ // Connect to a single relay
13
+ const relay = new Relay("wss://relay.example.com");
14
+
15
+ // Subscribe to events
16
+ relay
17
+ .req({
18
+ kinds: [1],
19
+ limit: 10,
20
+ })
21
+ .subscribe((response) => {
22
+ if (response === "EOSE") {
23
+ console.log("End of stored events");
24
+ } else {
25
+ console.log("Received event:", response);
26
+ }
27
+ });
28
+
29
+ // Publish an event
30
+ const event = {
31
+ kind: 1,
32
+ content: "Hello Nostr!",
33
+ created_at: Math.floor(Date.now() / 1000),
34
+ tags: [],
35
+ // ... other required fields
36
+ };
37
+
38
+ relay.event(event).subscribe((response) => {
39
+ console.log(`Published:`, response.ok);
40
+ });
41
+ ```
42
+
43
+ ### Relay Pool
44
+
45
+ ```typescript
46
+ import { RelayPool } from "./pool";
47
+
48
+ // Create a pool and connect to multiple relays
49
+ const pool = new RelayPool();
50
+ const relays = ["wss://relay1.example.com", "wss://relay2.example.com"];
51
+
52
+ // Subscribe to events from multiple relays
53
+ pool
54
+ .req(relays, {
55
+ kinds: [1],
56
+ limit: 10,
57
+ })
58
+ .subscribe((response) => {
59
+ if (response === "EOSE") {
60
+ console.log("End of stored events");
61
+ } else {
62
+ console.log("Received event:", response);
63
+ }
64
+ });
65
+
66
+ // Publish to multiple relays
67
+ pool.event(relays, event).subscribe((response) => {
68
+ console.log(`Published to ${response.from}:`, response.ok);
69
+ });
70
+ ```
71
+
72
+ ### Relay Group
73
+
74
+ ```typescript
75
+ import { RelayPool } from "./pool";
76
+
77
+ const pool = new RelayPool();
78
+ const relays = ["wss://relay1.example.com", "wss://relay2.example.com"];
79
+
80
+ // Create a group (automatically deduplicates events)
81
+ const group = pool.group(relays);
82
+
83
+ // Subscribe to events
84
+ group
85
+ .req({
86
+ kinds: [1],
87
+ limit: 10,
88
+ })
89
+ .subscribe((response) => {
90
+ console.log("Received:", response);
91
+ });
92
+
93
+ // Publish to all relays in group
94
+ group.event(event).subscribe((response) => {
95
+ console.log(`Published to ${response.from}:`, response.ok);
96
+ });
97
+ ```
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "./group.js";
2
2
  export * from "./pool.js";
3
3
  export * from "./relay.js";
4
+ export * from "./types.js";
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "./group.js";
2
2
  export * from "./pool.js";
3
3
  export * from "./relay.js";
4
+ export * from "./types.js";
package/dist/relay.d.ts CHANGED
@@ -9,14 +9,24 @@ export type RelayOptions = {
9
9
  export declare class Relay implements IRelay {
10
10
  url: string;
11
11
  protected log: typeof logger;
12
- socket$: WebSocketSubject<any>;
12
+ protected socket: WebSocketSubject<any>;
13
13
  connected$: BehaviorSubject<boolean>;
14
- challenge$: Observable<string>;
14
+ challenge$: BehaviorSubject<string | null>;
15
15
  authenticated$: BehaviorSubject<boolean>;
16
+ notices$: BehaviorSubject<string[]>;
17
+ /** An observable of all messages from the relay */
18
+ message$: Observable<any>;
19
+ /** An observable of NOTICE messages from the relay */
16
20
  notice$: Observable<string>;
21
+ get connected(): boolean;
22
+ get challenge(): string | null;
23
+ get notices(): string[];
24
+ get authenticated(): boolean;
17
25
  protected authRequiredForReq: BehaviorSubject<boolean>;
18
26
  protected authRequiredForPublish: BehaviorSubject<boolean>;
19
- protected reset(): void;
27
+ protected resetState(): void;
28
+ /** An internal observable that is responsible for watching all messages and updating state */
29
+ protected watchTower: Observable<never>;
20
30
  constructor(url: string, opts?: RelayOptions);
21
31
  protected waitForAuth<T extends unknown = unknown>(requireAuth: Observable<boolean>, observable: Observable<T>): Observable<T>;
22
32
  multiplex<T>(open: () => any, close: () => any, filter: (message: any) => boolean): Observable<T>;
package/dist/relay.js CHANGED
@@ -1,4 +1,4 @@
1
- import { BehaviorSubject, combineLatest, filter, map, merge, NEVER, of, shareReplay, switchMap, take, takeWhile, tap, timeout, } from "rxjs";
1
+ import { BehaviorSubject, combineLatest, filter, ignoreElements, map, merge, NEVER, of, scan, share, 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";
@@ -6,56 +6,95 @@ import { markFromRelay } from "./operators/mark-from-relay.js";
6
6
  export class Relay {
7
7
  url;
8
8
  log = logger.extend("Relay");
9
- socket$;
9
+ socket;
10
10
  connected$ = new BehaviorSubject(false);
11
- challenge$;
11
+ challenge$ = new BehaviorSubject(null);
12
12
  authenticated$ = new BehaviorSubject(false);
13
+ notices$ = new BehaviorSubject([]);
14
+ /** An observable of all messages from the relay */
15
+ message$;
16
+ /** An observable of NOTICE messages from the relay */
13
17
  notice$;
18
+ // sync state
19
+ get connected() {
20
+ return this.connected$.value;
21
+ }
22
+ get challenge() {
23
+ return this.challenge$.value;
24
+ }
25
+ get notices() {
26
+ return this.notices$.value;
27
+ }
28
+ get authenticated() {
29
+ return this.authenticated$.value;
30
+ }
14
31
  authRequiredForReq = new BehaviorSubject(false);
15
32
  authRequiredForPublish = new BehaviorSubject(false);
16
- reset() {
33
+ resetState() {
17
34
  // NOTE: only update the values if they need to be changed, otherwise this will cause an infinite loop
35
+ if (this.challenge$.value !== null)
36
+ this.challenge$.next(null);
18
37
  if (this.authenticated$.value)
19
38
  this.authenticated$.next(false);
39
+ if (this.notices$.value.length > 0)
40
+ this.notices$.next([]);
20
41
  if (this.authRequiredForReq.value)
21
42
  this.authRequiredForReq.next(false);
22
43
  if (this.authRequiredForPublish.value)
23
44
  this.authRequiredForPublish.next(false);
24
45
  }
46
+ /** An internal observable that is responsible for watching all messages and updating state */
47
+ watchTower;
25
48
  constructor(url, opts) {
26
49
  this.url = url;
27
50
  this.log = this.log.extend(url);
28
- this.socket$ = webSocket({
51
+ this.socket = webSocket({
29
52
  url,
30
53
  openObserver: {
31
54
  next: () => {
32
55
  this.log("Connected");
33
56
  this.connected$.next(true);
34
- this.reset();
57
+ this.resetState();
35
58
  },
36
59
  },
37
60
  closeObserver: {
38
61
  next: () => {
39
62
  this.log("Disconnected");
40
63
  this.connected$.next(false);
41
- this.reset();
64
+ this.resetState();
42
65
  },
43
66
  },
44
67
  WebSocketCtor: opts?.WebSocket,
45
68
  });
46
- // create an observable for listening for AUTH
47
- this.challenge$ = this.socket$.pipe(
48
- // listen for AUTH messages
49
- filter((message) => message[0] === "AUTH"),
50
- // pick the challenge string out
51
- map((m) => m[1]),
52
- // cache and share the challenge
53
- shareReplay(1));
54
- this.notice$ = this.socket$.pipe(
69
+ this.message$ = this.socket.asObservable();
70
+ this.notice$ = this.message$.pipe(
55
71
  // listen for NOTICE messages
56
72
  filter((m) => m[0] === "NOTICE"),
57
73
  // pick the string out of the message
58
74
  map((m) => m[1]));
75
+ // Update the notices state
76
+ const notice = this.notice$.pipe(
77
+ // Track all notices
78
+ scan((acc, notice) => [...acc, notice], []),
79
+ // Update the notices state
80
+ tap((notices) => this.notices$.next(notices)));
81
+ // Update the challenge state
82
+ const challenge = this.message$.pipe(
83
+ // listen for AUTH messages
84
+ filter((message) => message[0] === "AUTH"),
85
+ // pick the challenge string out
86
+ map((m) => m[1]),
87
+ // Update the challenge state
88
+ tap((challenge) => {
89
+ this.log("Received AUTH challenge", challenge);
90
+ this.challenge$.next(challenge);
91
+ }));
92
+ // Merge all watchers
93
+ this.watchTower = merge(notice, challenge).pipe(
94
+ // Never emit any values
95
+ ignoreElements(),
96
+ // There should only be a single watch tower
97
+ share());
59
98
  }
60
99
  waitForAuth(requireAuth, observable) {
61
100
  return combineLatest([requireAuth, this.authenticated$]).pipe(
@@ -68,15 +107,16 @@ export class Relay {
68
107
  }));
69
108
  }
70
109
  multiplex(open, close, filter) {
71
- return this.socket$.multiplex(open, close, filter);
110
+ return this.socket.multiplex(open, close, filter);
72
111
  }
73
112
  req(filters, id = nanoid()) {
74
- return this.waitForAuth(this.authRequiredForReq, this.socket$
113
+ const request = this.socket
75
114
  .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)
76
115
  .pipe(
77
116
  // listen for CLOSE auth-required
78
117
  tap((m) => {
79
118
  if (m[0] === "CLOSE" && m[1].startsWith("auth-required") && !this.authRequiredForReq.value) {
119
+ this.log("Auth required for REQ");
80
120
  this.authRequiredForReq.next(true);
81
121
  }
82
122
  }),
@@ -96,11 +136,13 @@ export class Relay {
96
136
  timeout({
97
137
  first: 10_000,
98
138
  with: () => merge(of("EOSE"), NEVER),
99
- })));
139
+ }));
140
+ // Wait for auth if required and make sure to start the watch tower
141
+ return this.waitForAuth(this.authRequiredForReq, merge(this.watchTower, request));
100
142
  }
101
143
  /** send an Event message */
102
144
  event(event, verb = "EVENT") {
103
- const observable = this.socket$
145
+ const observable = this.socket
104
146
  .multiplex(() => [verb, event], () => void 0, (m) => m[0] === "OK" && m[1] === event.id)
105
147
  .pipe(
106
148
  // format OK message
@@ -110,14 +152,16 @@ export class Relay {
110
152
  // listen for OK auth-required
111
153
  tap(({ ok, message }) => {
112
154
  if (ok === false && message.startsWith("auth-required") && !this.authRequiredForPublish.value) {
155
+ this.log("Auth required for publish");
113
156
  this.authRequiredForPublish.next(true);
114
157
  }
115
158
  }));
159
+ const withWatchTower = merge(this.watchTower, observable);
116
160
  // skip wait for auth if verb is AUTH
117
161
  if (verb === "AUTH")
118
- return observable;
162
+ return withWatchTower;
119
163
  else
120
- return this.waitForAuth(this.authRequiredForPublish, observable);
164
+ return this.waitForAuth(this.authRequiredForPublish, withWatchTower);
121
165
  }
122
166
  /** send and AUTH message */
123
167
  auth(event) {
package/dist/types.d.ts CHANGED
@@ -10,9 +10,9 @@ export type PublishResponse = {
10
10
  export type MultiplexWebSocket<T = any> = Pick<WebSocketSubject<T>, "multiplex">;
11
11
  export interface IRelayState {
12
12
  connected$: Observable<boolean>;
13
- challenge$: Observable<string>;
13
+ challenge$: Observable<string | null>;
14
14
  authenticated$: Observable<boolean>;
15
- notice$: Observable<string>;
15
+ notices$: Observable<string[]>;
16
16
  }
17
17
  export interface Nip01Actions {
18
18
  /** Send an EVENT message */
@@ -22,6 +22,12 @@ export interface Nip01Actions {
22
22
  }
23
23
  export interface IRelay extends MultiplexWebSocket, Nip01Actions, IRelayState {
24
24
  url: string;
25
+ message$: Observable<any>;
26
+ notice$: Observable<string>;
27
+ readonly connected: boolean;
28
+ readonly authenticated: boolean;
29
+ readonly challenge: string | null;
30
+ readonly notices: string[];
25
31
  /** Send an AUTH message */
26
32
  auth(event: NostrEvent): Observable<{
27
33
  ok: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-relay",
3
- "version": "0.0.0-next-20250330150216",
3
+ "version": "0.0.0-next-20250330153313",
4
4
  "description": "A collection of observable based loaders built on rx-nostr",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -21,6 +21,21 @@
21
21
  "require": "./dist/index.js",
22
22
  "types": "./dist/index.d.ts"
23
23
  },
24
+ "./pool": {
25
+ "import": "./dist/pool.js",
26
+ "require": "./dist/pool.js",
27
+ "types": "./dist/pool.d.ts"
28
+ },
29
+ "./relay": {
30
+ "import": "./dist/relay.js",
31
+ "require": "./dist/relay.js",
32
+ "types": "./dist/relay.d.ts"
33
+ },
34
+ "./types": {
35
+ "import": "./dist/types.js",
36
+ "require": "./dist/types.js",
37
+ "types": "./dist/types.d.ts"
38
+ },
24
39
  "./operators": {
25
40
  "import": "./dist/operators/index.js",
26
41
  "require": "./dist/operators/index.js",
@@ -33,7 +48,7 @@
33
48
  }
34
49
  },
35
50
  "dependencies": {
36
- "applesauce-core": "0.0.0-next-20250330150216",
51
+ "applesauce-core": "0.0.0-next-20250330153313",
37
52
  "nanoid": "^5.0.9",
38
53
  "nostr-tools": "^2.10.4",
39
54
  "rxjs": "^7.8.1"