applesauce-relay 0.12.0 → 1.0.1
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 +115 -0
- package/dist/__tests__/auth.test.d.ts +1 -0
- package/dist/__tests__/auth.test.js +111 -0
- package/dist/__tests__/group.test.d.ts +1 -0
- package/dist/__tests__/group.test.js +106 -0
- package/dist/__tests__/pool.test.d.ts +1 -0
- package/dist/__tests__/pool.test.js +81 -0
- package/dist/__tests__/relay.test.d.ts +1 -0
- package/dist/__tests__/relay.test.js +561 -0
- package/dist/group.d.ts +19 -0
- package/dist/group.js +54 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/lib/negentropy.d.ts +61 -0
- package/dist/lib/negentropy.js +533 -0
- package/dist/negentropy.d.ts +15 -0
- package/dist/negentropy.js +68 -0
- package/dist/operators/complete-on-eose.d.ts +6 -0
- package/dist/operators/complete-on-eose.js +7 -0
- package/dist/operators/index.d.ts +4 -1
- package/dist/operators/index.js +4 -1
- package/dist/operators/mark-from-relay.d.ts +1 -1
- package/dist/operators/only-events.d.ts +1 -1
- package/dist/operators/store-events.d.ts +5 -0
- package/dist/operators/store-events.js +7 -0
- package/dist/operators/to-event-store.d.ts +6 -0
- package/dist/operators/to-event-store.js +19 -0
- package/dist/pool.d.ts +18 -5
- package/dist/pool.js +33 -23
- package/dist/relay.d.ts +73 -22
- package/dist/relay.js +278 -59
- package/dist/types.d.ts +104 -0
- package/dist/types.js +1 -0
- package/package.json +28 -6
package/dist/relay.js
CHANGED
|
@@ -1,115 +1,283 @@
|
|
|
1
|
-
import { BehaviorSubject, combineLatest, EMPTY, filter, map, merge, NEVER, of, shareReplay, switchMap, take, takeWhile, tap, timeout, } from "rxjs";
|
|
2
|
-
import { webSocket } from "rxjs/webSocket";
|
|
3
|
-
import { nanoid } from "nanoid";
|
|
4
1
|
import { logger } from "applesauce-core";
|
|
2
|
+
import { simpleTimeout } from "applesauce-core/observable";
|
|
3
|
+
import { nanoid } from "nanoid";
|
|
4
|
+
import { nip42 } from "nostr-tools";
|
|
5
|
+
import { BehaviorSubject, catchError, combineLatest, defer, filter, from, ignoreElements, map, merge, mergeMap, NEVER, of, retry, scan, share, shareReplay, switchMap, take, tap, throwError, timeout, timer, } from "rxjs";
|
|
6
|
+
import { webSocket } from "rxjs/webSocket";
|
|
7
|
+
import { completeOnEose } from "./operators/complete-on-eose.js";
|
|
5
8
|
import { markFromRelay } from "./operators/mark-from-relay.js";
|
|
9
|
+
/** An error that is thrown when a REQ is closed from the relay side */
|
|
10
|
+
export class ReqCloseError extends Error {
|
|
11
|
+
}
|
|
6
12
|
export class Relay {
|
|
7
13
|
url;
|
|
8
|
-
log = logger.extend("
|
|
9
|
-
socket
|
|
14
|
+
log = logger.extend("Relay");
|
|
15
|
+
socket;
|
|
16
|
+
/** Whether the relay is ready for subscriptions or event publishing. setting this to false will cause all .req and .event observables to hang until the relay is ready */
|
|
17
|
+
ready$ = new BehaviorSubject(true);
|
|
18
|
+
/** A method that returns an Observable that emits when the relay should reconnect */
|
|
19
|
+
reconnectTimer;
|
|
20
|
+
/** How many times the relay has tried to reconnect */
|
|
21
|
+
attempts$ = new BehaviorSubject(0);
|
|
22
|
+
/** Whether the relay is connected */
|
|
10
23
|
connected$ = new BehaviorSubject(false);
|
|
11
|
-
challenge
|
|
24
|
+
/** The authentication challenge string from the relay */
|
|
25
|
+
challenge$ = new BehaviorSubject(null);
|
|
26
|
+
/** Whether the client is authenticated with the relay */
|
|
12
27
|
authenticated$ = new BehaviorSubject(false);
|
|
13
|
-
notices
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
28
|
+
/** The notices from the relay */
|
|
29
|
+
notices$ = new BehaviorSubject([]);
|
|
30
|
+
/** An observable that emits the NIP-11 information document for the relay */
|
|
31
|
+
information$;
|
|
32
|
+
_nip11 = null;
|
|
33
|
+
/** An observable that emits the limitations for the relay */
|
|
34
|
+
limitations$;
|
|
35
|
+
/** An observable of all messages from the relay */
|
|
36
|
+
message$;
|
|
37
|
+
/** An observable of NOTICE messages from the relay */
|
|
38
|
+
notice$;
|
|
39
|
+
// sync state
|
|
40
|
+
get connected() {
|
|
41
|
+
return this.connected$.value;
|
|
42
|
+
}
|
|
43
|
+
get challenge() {
|
|
44
|
+
return this.challenge$.value;
|
|
20
45
|
}
|
|
46
|
+
get notices() {
|
|
47
|
+
return this.notices$.value;
|
|
48
|
+
}
|
|
49
|
+
get authenticated() {
|
|
50
|
+
return this.authenticated$.value;
|
|
51
|
+
}
|
|
52
|
+
get information() {
|
|
53
|
+
return this._nip11;
|
|
54
|
+
}
|
|
55
|
+
/** If an EOSE message is not seen in this time, emit one locally */
|
|
56
|
+
eoseTimeout = 10_000;
|
|
57
|
+
/** How long to wait for an OK message from the relay */
|
|
58
|
+
eventTimeout = 10_000;
|
|
59
|
+
/** How long to keep the connection alive after nothing is subscribed */
|
|
60
|
+
keepAlive = 30_000;
|
|
61
|
+
// subjects that track if an "auth-required" message has been received for REQ or EVENT
|
|
62
|
+
receivedAuthRequiredForReq = new BehaviorSubject(false);
|
|
63
|
+
receivedAuthRequiredForEvent = new BehaviorSubject(false);
|
|
64
|
+
// Computed observables that track if auth is required for REQ or EVENT
|
|
65
|
+
authRequiredForReq;
|
|
66
|
+
authRequiredForEvent;
|
|
67
|
+
resetState() {
|
|
68
|
+
// NOTE: only update the values if they need to be changed, otherwise this will cause an infinite loop
|
|
69
|
+
if (this.challenge$.value !== null)
|
|
70
|
+
this.challenge$.next(null);
|
|
71
|
+
if (this.authenticated$.value)
|
|
72
|
+
this.authenticated$.next(false);
|
|
73
|
+
if (this.notices$.value.length > 0)
|
|
74
|
+
this.notices$.next([]);
|
|
75
|
+
if (this.receivedAuthRequiredForReq.value)
|
|
76
|
+
this.receivedAuthRequiredForReq.next(false);
|
|
77
|
+
if (this.receivedAuthRequiredForEvent.value)
|
|
78
|
+
this.receivedAuthRequiredForEvent.next(false);
|
|
79
|
+
}
|
|
80
|
+
/** An internal observable that is responsible for watching all messages and updating state */
|
|
81
|
+
watchTower;
|
|
21
82
|
constructor(url, opts) {
|
|
22
83
|
this.url = url;
|
|
23
84
|
this.log = this.log.extend(url);
|
|
24
|
-
this
|
|
85
|
+
/** Use the static method to create a new reconnect method for this relay */
|
|
86
|
+
this.reconnectTimer = Relay.createReconnectTimer(url);
|
|
87
|
+
this.socket = webSocket({
|
|
25
88
|
url,
|
|
26
89
|
openObserver: {
|
|
27
90
|
next: () => {
|
|
28
91
|
this.log("Connected");
|
|
29
92
|
this.connected$.next(true);
|
|
30
|
-
this.
|
|
93
|
+
this.attempts$.next(0);
|
|
94
|
+
this.resetState();
|
|
31
95
|
},
|
|
32
96
|
},
|
|
33
97
|
closeObserver: {
|
|
34
|
-
next: () => {
|
|
98
|
+
next: (event) => {
|
|
35
99
|
this.log("Disconnected");
|
|
36
100
|
this.connected$.next(false);
|
|
37
|
-
this.
|
|
101
|
+
this.attempts$.next(this.attempts$.value + 1);
|
|
102
|
+
this.resetState();
|
|
103
|
+
// Start the reconnect timer if the connection was not closed cleanly
|
|
104
|
+
if (!event.wasClean)
|
|
105
|
+
this.startReconnectTimer(event);
|
|
38
106
|
},
|
|
39
107
|
},
|
|
40
108
|
WebSocketCtor: opts?.WebSocket,
|
|
41
109
|
});
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
110
|
+
this.message$ = this.socket.asObservable();
|
|
111
|
+
// Create an observable to fetch the NIP-11 information document
|
|
112
|
+
this.information$ = defer(() => {
|
|
113
|
+
this.log("Fetching NIP-11 information document");
|
|
114
|
+
return Relay.fetchInformationDocument(this.url);
|
|
115
|
+
}).pipe(
|
|
116
|
+
// if the fetch fails, return null
|
|
117
|
+
catchError(() => of(null)),
|
|
118
|
+
// cache the result
|
|
119
|
+
shareReplay(1),
|
|
120
|
+
// update the internal state
|
|
121
|
+
tap((info) => (this._nip11 = info)));
|
|
122
|
+
this.limitations$ = this.information$.pipe(map((info) => info?.limitation));
|
|
123
|
+
// Create observables that track if auth is required for REQ or EVENT
|
|
124
|
+
this.authRequiredForReq = combineLatest([this.receivedAuthRequiredForReq, this.limitations$]).pipe(map(([received, limitations]) => received || limitations?.auth_required === true), tap((required) => required && this.log("Auth required for REQ")), shareReplay(1));
|
|
125
|
+
this.authRequiredForEvent = combineLatest([this.receivedAuthRequiredForEvent, this.limitations$]).pipe(map(([received, limitations]) => received || limitations?.auth_required === true), tap((required) => required && this.log("Auth required for EVENT")), shareReplay(1));
|
|
126
|
+
this.notice$ = this.message$.pipe(
|
|
51
127
|
// listen for NOTICE messages
|
|
52
128
|
filter((m) => m[0] === "NOTICE"),
|
|
53
129
|
// pick the string out of the message
|
|
54
130
|
map((m) => m[1]));
|
|
131
|
+
// Update the notices state
|
|
132
|
+
const notice = this.notice$.pipe(
|
|
133
|
+
// Track all notices
|
|
134
|
+
scan((acc, notice) => [...acc, notice], []),
|
|
135
|
+
// Update the notices state
|
|
136
|
+
tap((notices) => this.notices$.next(notices)));
|
|
137
|
+
// Update the challenge state
|
|
138
|
+
const challenge = this.message$.pipe(
|
|
139
|
+
// listen for AUTH messages
|
|
140
|
+
filter((message) => message[0] === "AUTH"),
|
|
141
|
+
// pick the challenge string out
|
|
142
|
+
map((m) => m[1]),
|
|
143
|
+
// Update the challenge state
|
|
144
|
+
tap((challenge) => {
|
|
145
|
+
this.log("Received AUTH challenge", challenge);
|
|
146
|
+
this.challenge$.next(challenge);
|
|
147
|
+
}));
|
|
148
|
+
// Merge all watchers
|
|
149
|
+
this.watchTower = this.ready$.pipe(switchMap((ready) => {
|
|
150
|
+
if (!ready)
|
|
151
|
+
return NEVER;
|
|
152
|
+
// Only start the watch tower if the relay is ready
|
|
153
|
+
return merge(notice, challenge, this.information$).pipe(
|
|
154
|
+
// Never emit any values
|
|
155
|
+
ignoreElements(),
|
|
156
|
+
// Start the reconnect timer if the connection has an error
|
|
157
|
+
catchError((error) => {
|
|
158
|
+
this.startReconnectTimer(error instanceof Error ? error : new Error("Connection error"));
|
|
159
|
+
return NEVER;
|
|
160
|
+
}),
|
|
161
|
+
// Add keep alive timer to the connection
|
|
162
|
+
share({ resetOnRefCountZero: () => timer(this.keepAlive) }));
|
|
163
|
+
}),
|
|
164
|
+
// There should only be a single watch tower
|
|
165
|
+
share());
|
|
55
166
|
}
|
|
56
|
-
|
|
167
|
+
/** Set ready = false and start the reconnect timer */
|
|
168
|
+
startReconnectTimer(error) {
|
|
169
|
+
if (!this.ready$.value)
|
|
170
|
+
return;
|
|
171
|
+
this.ready$.next(false);
|
|
172
|
+
this.reconnectTimer(error, this.attempts$.value)
|
|
173
|
+
.pipe(take(1))
|
|
174
|
+
.subscribe(() => this.ready$.next(true));
|
|
175
|
+
}
|
|
176
|
+
/** Wait for ready and authenticated */
|
|
177
|
+
waitForAuth(
|
|
178
|
+
// NOTE: require BehaviorSubject so it always has a value
|
|
179
|
+
requireAuth, observable) {
|
|
57
180
|
return combineLatest([requireAuth, this.authenticated$]).pipe(
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
181
|
+
// wait for auth not required or authenticated
|
|
182
|
+
filter(([required, authenticated]) => !required || authenticated),
|
|
183
|
+
// complete after the first value so this does not repeat
|
|
184
|
+
take(1),
|
|
185
|
+
// switch to the observable
|
|
186
|
+
switchMap(() => observable));
|
|
187
|
+
}
|
|
188
|
+
/** Wait for the relay to be ready to accept connections */
|
|
189
|
+
waitForReady(observable) {
|
|
190
|
+
// Don't wait if the relay is already ready
|
|
191
|
+
if (this.ready$.value)
|
|
192
|
+
return observable;
|
|
193
|
+
else
|
|
194
|
+
return this.ready$.pipe(
|
|
195
|
+
// wait for ready to be true
|
|
196
|
+
filter((ready) => ready),
|
|
197
|
+
// complete after the first value so this does not repeat
|
|
198
|
+
take(1),
|
|
199
|
+
// switch to the observable
|
|
200
|
+
switchMap(() => observable));
|
|
201
|
+
}
|
|
202
|
+
multiplex(open, close, filter) {
|
|
203
|
+
return this.socket.multiplex(open, close, filter);
|
|
204
|
+
}
|
|
205
|
+
/** Send a message to the relay */
|
|
206
|
+
next(message) {
|
|
207
|
+
this.socket.next(message);
|
|
65
208
|
}
|
|
209
|
+
/** Create a REQ observable that emits events or "EOSE" or errors */
|
|
66
210
|
req(filters, id = nanoid()) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (m[0] === "CLOSE" && m[1].startsWith("auth-required") && !this.authRequiredForReq.value) {
|
|
73
|
-
this.authRequiredForReq.next(true);
|
|
74
|
-
}
|
|
75
|
-
}),
|
|
76
|
-
// complete when CLOSE is sent
|
|
77
|
-
takeWhile((m) => m[0] !== "CLOSE"),
|
|
78
|
-
// pick event out of EVENT messages
|
|
211
|
+
const request = this.socket.multiplex(() => (Array.isArray(filters) ? ["REQ", id, ...filters] : ["REQ", id, filters]), () => ["CLOSE", id], (message) => (message[0] === "EVENT" || message[0] === "CLOSED" || message[0] === "EOSE") && message[1] === id);
|
|
212
|
+
// Start the watch tower with the observable
|
|
213
|
+
const withWatchTower = merge(this.watchTower, request);
|
|
214
|
+
const observable = withWatchTower.pipe(
|
|
215
|
+
// Map the messages to events, EOSE, or throw an error
|
|
79
216
|
map((message) => {
|
|
80
217
|
if (message[0] === "EOSE")
|
|
81
218
|
return "EOSE";
|
|
219
|
+
else if (message[0] === "CLOSED")
|
|
220
|
+
throw new ReqCloseError(message[2]);
|
|
82
221
|
else
|
|
83
222
|
return message[2];
|
|
223
|
+
}), catchError((error) => {
|
|
224
|
+
// Set REQ auth required if the REQ is closed with auth-required
|
|
225
|
+
if (error instanceof ReqCloseError &&
|
|
226
|
+
error.message.startsWith("auth-required") &&
|
|
227
|
+
!this.receivedAuthRequiredForReq.value) {
|
|
228
|
+
this.log("Auth required for REQ");
|
|
229
|
+
this.receivedAuthRequiredForReq.next(true);
|
|
230
|
+
}
|
|
231
|
+
// Pass the error through
|
|
232
|
+
return throwError(() => error);
|
|
84
233
|
}),
|
|
85
234
|
// mark events as from relays
|
|
86
235
|
markFromRelay(this.url),
|
|
87
236
|
// if no events are seen in 10s, emit EOSE
|
|
237
|
+
// TODO: this should emit EOSE event if events are seen, the timeout should be for only the EOSE message
|
|
88
238
|
timeout({
|
|
89
|
-
first:
|
|
239
|
+
first: this.eoseTimeout,
|
|
90
240
|
with: () => merge(of("EOSE"), NEVER),
|
|
91
|
-
})
|
|
241
|
+
}),
|
|
242
|
+
// Only create one upstream subscription
|
|
243
|
+
share());
|
|
244
|
+
// Wait for auth if required and make sure to start the watch tower
|
|
245
|
+
return this.waitForReady(this.waitForAuth(this.authRequiredForReq, observable));
|
|
92
246
|
}
|
|
93
|
-
/**
|
|
247
|
+
/** Send an EVENT or AUTH message and return an observable of PublishResponse that completes or errors */
|
|
94
248
|
event(event, verb = "EVENT") {
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
.
|
|
98
|
-
|
|
99
|
-
|
|
249
|
+
const base = defer(() => {
|
|
250
|
+
// Send event when subscription starts
|
|
251
|
+
this.socket.next([verb, event]);
|
|
252
|
+
return this.socket.pipe(filter((m) => m[0] === "OK" && m[1] === event.id),
|
|
253
|
+
// format OK message
|
|
254
|
+
map((m) => ({ ok: m[2], message: m[3], from: this.url })));
|
|
255
|
+
});
|
|
256
|
+
// Start the watch tower with the observable
|
|
257
|
+
const withWatchTower = merge(this.watchTower, base);
|
|
258
|
+
// Add complete operators
|
|
259
|
+
const observable = withWatchTower.pipe(
|
|
100
260
|
// complete on first value
|
|
101
261
|
take(1),
|
|
102
262
|
// listen for OK auth-required
|
|
103
263
|
tap(({ ok, message }) => {
|
|
104
|
-
if (ok === false && message
|
|
105
|
-
this.
|
|
264
|
+
if (ok === false && message?.startsWith("auth-required") && !this.receivedAuthRequiredForEvent.value) {
|
|
265
|
+
this.log("Auth required for publish");
|
|
266
|
+
this.receivedAuthRequiredForEvent.next(true);
|
|
106
267
|
}
|
|
107
|
-
})
|
|
268
|
+
}),
|
|
269
|
+
// if no message is seen in 10s, emit EOSE
|
|
270
|
+
timeout({
|
|
271
|
+
first: this.eventTimeout,
|
|
272
|
+
with: () => of({ ok: false, from: this.url, message: "Timeout" }),
|
|
273
|
+
}),
|
|
274
|
+
// Only create one upstream subscription
|
|
275
|
+
share());
|
|
108
276
|
// skip wait for auth if verb is AUTH
|
|
109
277
|
if (verb === "AUTH")
|
|
110
|
-
return observable;
|
|
278
|
+
return this.waitForReady(observable);
|
|
111
279
|
else
|
|
112
|
-
return this.waitForAuth(this.
|
|
280
|
+
return this.waitForReady(this.waitForAuth(this.authRequiredForEvent, observable));
|
|
113
281
|
}
|
|
114
282
|
/** send and AUTH message */
|
|
115
283
|
auth(event) {
|
|
@@ -117,4 +285,55 @@ export class Relay {
|
|
|
117
285
|
// update authenticated
|
|
118
286
|
tap((result) => this.authenticated$.next(result.ok)));
|
|
119
287
|
}
|
|
288
|
+
/** Authenticate with the relay using a signer */
|
|
289
|
+
authenticate(signer) {
|
|
290
|
+
if (!this.challenge)
|
|
291
|
+
throw new Error("Have not received authentication challenge");
|
|
292
|
+
const p = signer.signEvent(nip42.makeAuthEvent(this.url, this.challenge));
|
|
293
|
+
const start = p instanceof Promise ? from(p) : of(p);
|
|
294
|
+
return start.pipe(switchMap((event) => this.auth(event)));
|
|
295
|
+
}
|
|
296
|
+
/** Creates a REQ that retries when relay errors ( default 3 retries ) */
|
|
297
|
+
subscription(filters, opts) {
|
|
298
|
+
return this.req(filters, opts?.id).pipe(
|
|
299
|
+
// Retry on connection errors
|
|
300
|
+
retry({ count: opts?.retries ?? 3, resetOnSuccess: true }));
|
|
301
|
+
}
|
|
302
|
+
/** Makes a single request that retires on errors and completes on EOSE */
|
|
303
|
+
request(filters, opts) {
|
|
304
|
+
return this.req(filters, opts?.id).pipe(
|
|
305
|
+
// Retry on connection errors
|
|
306
|
+
retry(opts?.retries ?? 3),
|
|
307
|
+
// Complete when EOSE is received
|
|
308
|
+
completeOnEose());
|
|
309
|
+
}
|
|
310
|
+
/** Publishes an event to the relay and retries when relay errors or responds with auth-required ( default 3 retries ) */
|
|
311
|
+
publish(event, opts) {
|
|
312
|
+
return this.event(event).pipe(mergeMap((result) => {
|
|
313
|
+
// If the relay responds with auth-required, throw an error for the retry operator to handle
|
|
314
|
+
if (result.ok === false && result.message?.startsWith("auth-required:"))
|
|
315
|
+
return throwError(() => new Error(result.message));
|
|
316
|
+
return of(result);
|
|
317
|
+
}),
|
|
318
|
+
// Retry the publish until it succeeds or the number of retries is reached
|
|
319
|
+
retry(opts?.retries ?? 3));
|
|
320
|
+
}
|
|
321
|
+
/** Static method to fetch the NIP-11 information document for a relay */
|
|
322
|
+
static fetchInformationDocument(url) {
|
|
323
|
+
return from(fetch(url, { headers: { Accept: "application/nostr+json" } }).then((res) => res.json())).pipe(
|
|
324
|
+
// if the fetch fails, return null
|
|
325
|
+
catchError(() => of(null)),
|
|
326
|
+
// timeout after 10s
|
|
327
|
+
simpleTimeout(10_000));
|
|
328
|
+
}
|
|
329
|
+
/** Static method to create a reconnection method for each relay */
|
|
330
|
+
static createReconnectTimer(_relay) {
|
|
331
|
+
return (_error, tries = 0) => {
|
|
332
|
+
// Calculate delay with exponential backoff: 2^attempts * 1000ms
|
|
333
|
+
// with a maximum delay of 5 minutes (300000ms)
|
|
334
|
+
const delay = Math.min(Math.pow(1.5, tries) * 1000, 300000);
|
|
335
|
+
// Return a timer that will emit after the calculated delay
|
|
336
|
+
return timer(delay);
|
|
337
|
+
};
|
|
338
|
+
}
|
|
120
339
|
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { EventTemplate, Filter, NostrEvent } from "nostr-tools";
|
|
2
|
+
import { Observable } from "rxjs";
|
|
3
|
+
import { WebSocketSubject } from "rxjs/webSocket";
|
|
4
|
+
export type SubscriptionResponse = NostrEvent | "EOSE";
|
|
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 | null>;
|
|
14
|
+
authenticated$: Observable<boolean>;
|
|
15
|
+
notices$: Observable<string[]>;
|
|
16
|
+
}
|
|
17
|
+
export type PublishOptions = {
|
|
18
|
+
retries?: number;
|
|
19
|
+
};
|
|
20
|
+
export type RequestOptions = {
|
|
21
|
+
id?: string;
|
|
22
|
+
retries?: number;
|
|
23
|
+
};
|
|
24
|
+
export type SubscriptionOptions = {
|
|
25
|
+
id?: string;
|
|
26
|
+
retries?: number;
|
|
27
|
+
};
|
|
28
|
+
export type AuthSigner = {
|
|
29
|
+
signEvent: (event: EventTemplate) => NostrEvent | Promise<NostrEvent>;
|
|
30
|
+
};
|
|
31
|
+
export interface Nip01Actions {
|
|
32
|
+
/** Send an EVENT message */
|
|
33
|
+
event(event: NostrEvent): Observable<PublishResponse>;
|
|
34
|
+
/** Send a REQ message */
|
|
35
|
+
req(filters: Filter | Filter[], id?: string): Observable<SubscriptionResponse>;
|
|
36
|
+
}
|
|
37
|
+
export interface IRelay extends MultiplexWebSocket, Nip01Actions, IRelayState {
|
|
38
|
+
url: string;
|
|
39
|
+
message$: Observable<any>;
|
|
40
|
+
notice$: Observable<string>;
|
|
41
|
+
readonly connected: boolean;
|
|
42
|
+
readonly authenticated: boolean;
|
|
43
|
+
readonly challenge: string | null;
|
|
44
|
+
readonly notices: string[];
|
|
45
|
+
/** Send an AUTH message */
|
|
46
|
+
auth(event: NostrEvent): Observable<{
|
|
47
|
+
ok: boolean;
|
|
48
|
+
message?: string;
|
|
49
|
+
}>;
|
|
50
|
+
/** Send an EVENT message with retries */
|
|
51
|
+
publish(event: NostrEvent, opts?: {
|
|
52
|
+
retries?: number;
|
|
53
|
+
}): Observable<PublishResponse>;
|
|
54
|
+
/** Send a REQ message with retries */
|
|
55
|
+
request(filters: Filter | Filter[], opts?: {
|
|
56
|
+
id?: string;
|
|
57
|
+
retries?: number;
|
|
58
|
+
}): Observable<NostrEvent>;
|
|
59
|
+
/** Open a subscription with retries */
|
|
60
|
+
subscription(filters: Filter | Filter[], opts?: {
|
|
61
|
+
id?: string;
|
|
62
|
+
retries?: number;
|
|
63
|
+
}): Observable<SubscriptionResponse>;
|
|
64
|
+
}
|
|
65
|
+
export interface IGroup extends Nip01Actions {
|
|
66
|
+
/** Send an EVENT message with retries */
|
|
67
|
+
publish(event: NostrEvent, opts?: {
|
|
68
|
+
retries?: number;
|
|
69
|
+
}): Observable<PublishResponse[]>;
|
|
70
|
+
/** Send a REQ message with retries */
|
|
71
|
+
request(filters: Filter | Filter[], opts?: {
|
|
72
|
+
id?: string;
|
|
73
|
+
retries?: number;
|
|
74
|
+
}): Observable<NostrEvent>;
|
|
75
|
+
/** Open a subscription with retries */
|
|
76
|
+
subscription(filters: Filter | Filter[], opts?: {
|
|
77
|
+
id?: string;
|
|
78
|
+
retries?: number;
|
|
79
|
+
}): Observable<SubscriptionResponse>;
|
|
80
|
+
}
|
|
81
|
+
export interface IPool {
|
|
82
|
+
/** Send an EVENT message */
|
|
83
|
+
event(relays: string[], event: NostrEvent): Observable<PublishResponse>;
|
|
84
|
+
/** Send a REQ message */
|
|
85
|
+
req(relays: string[], filters: Filter | Filter[], id?: string): Observable<SubscriptionResponse>;
|
|
86
|
+
/** Get or create a relay */
|
|
87
|
+
relay(url: string): IRelay;
|
|
88
|
+
/** Create a relay group */
|
|
89
|
+
group(relays: string[]): IGroup;
|
|
90
|
+
/** Send an EVENT message to relays with retries */
|
|
91
|
+
publish(relays: string[], event: NostrEvent, opts?: {
|
|
92
|
+
retries?: number;
|
|
93
|
+
}): Observable<PublishResponse[]>;
|
|
94
|
+
/** Send a REQ message to relays with retries */
|
|
95
|
+
request(relays: string[], filters: Filter | Filter[], opts?: {
|
|
96
|
+
id?: string;
|
|
97
|
+
retries?: number;
|
|
98
|
+
}): Observable<NostrEvent>;
|
|
99
|
+
/** Open a subscription to relays with retries */
|
|
100
|
+
subscription(relays: string[], filters: Filter | Filter[], opts?: {
|
|
101
|
+
id?: string;
|
|
102
|
+
retries?: number;
|
|
103
|
+
}): Observable<SubscriptionResponse>;
|
|
104
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "applesauce-relay",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "nostr relay communication framework built on rxjs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -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",
|
|
@@ -30,19 +45,26 @@
|
|
|
30
45
|
"import": "./dist/operators/*.js",
|
|
31
46
|
"require": "./dist/operators/*.js",
|
|
32
47
|
"types": "./dist/operators/*.d.ts"
|
|
48
|
+
},
|
|
49
|
+
"./negentropy": {
|
|
50
|
+
"import": "./dist/negentropy.js",
|
|
51
|
+
"require": "./dist/negentropy.js",
|
|
52
|
+
"types": "./dist/negentropy.d.ts"
|
|
33
53
|
}
|
|
34
54
|
},
|
|
35
55
|
"dependencies": {
|
|
36
|
-
"
|
|
56
|
+
"@noble/hashes": "^1.7.1",
|
|
57
|
+
"applesauce-core": "^1.0.0",
|
|
37
58
|
"nanoid": "^5.0.9",
|
|
38
59
|
"nostr-tools": "^2.10.4",
|
|
39
60
|
"rxjs": "^7.8.1"
|
|
40
61
|
},
|
|
41
62
|
"devDependencies": {
|
|
63
|
+
"@hirez_io/observer-spy": "^2.2.0",
|
|
64
|
+
"@vitest/expect": "^3.1.1",
|
|
42
65
|
"typescript": "^5.7.3",
|
|
43
|
-
"vitest": "^3.
|
|
44
|
-
"vitest-
|
|
45
|
-
"vitest-websocket-mock": "^0.4.0"
|
|
66
|
+
"vitest": "^3.1.1",
|
|
67
|
+
"vitest-websocket-mock": "^0.5.0"
|
|
46
68
|
},
|
|
47
69
|
"funding": {
|
|
48
70
|
"type": "lightning",
|