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 +94 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/relay.d.ts +13 -3
- package/dist/relay.js +66 -22
- package/dist/types.d.ts +8 -2
- package/package.json +17 -2
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
package/dist/index.js
CHANGED
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
|
|
12
|
+
protected socket: WebSocketSubject<any>;
|
|
13
13
|
connected$: BehaviorSubject<boolean>;
|
|
14
|
-
challenge$:
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
64
|
+
this.resetState();
|
|
42
65
|
},
|
|
43
66
|
},
|
|
44
67
|
WebSocketCtor: opts?.WebSocket,
|
|
45
68
|
});
|
|
46
|
-
|
|
47
|
-
this.
|
|
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
|
|
110
|
+
return this.socket.multiplex(open, close, filter);
|
|
72
111
|
}
|
|
73
112
|
req(filters, id = nanoid()) {
|
|
74
|
-
|
|
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
|
|
162
|
+
return withWatchTower;
|
|
119
163
|
else
|
|
120
|
-
return this.waitForAuth(this.authRequiredForPublish,
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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"
|