applesauce-relay 5.2.0 → 6.0.2
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/dist/group.d.ts +29 -40
- package/dist/group.js +85 -56
- package/dist/liveness.d.ts +3 -3
- package/dist/operators/complete-on-eose.d.ts +4 -4
- package/dist/operators/complete-when.d.ts +7 -0
- package/dist/operators/complete-when.js +14 -0
- package/dist/operators/index.d.ts +0 -2
- package/dist/operators/index.js +0 -2
- package/dist/operators/only-events.d.ts +2 -2
- package/dist/operators/store-events.d.ts +2 -2
- package/dist/pool.d.ts +16 -16
- package/dist/pool.js +3 -2
- package/dist/relay.d.ts +46 -31
- package/dist/relay.js +266 -102
- package/dist/types.d.ts +104 -131
- package/package.json +3 -3
- package/dist/operators/mark-from-relay.d.ts +0 -4
- package/dist/operators/mark-from-relay.js +0 -9
- package/dist/operators/to-event-store.d.ts +0 -9
- package/dist/operators/to-event-store.js +0 -15
package/dist/relay.js
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
import { logger } from "applesauce-core";
|
|
2
|
+
import { addSeenRelay } from "applesauce-core/helpers";
|
|
2
3
|
import { ensureHttpURL } from "applesauce-core/helpers/url";
|
|
3
4
|
import { mapEventsToStore, simpleTimeout } from "applesauce-core/observable";
|
|
4
5
|
import { nanoid } from "nanoid";
|
|
5
6
|
import { makeAuthEvent } from "nostr-tools/nip42";
|
|
6
|
-
import { BehaviorSubject, catchError, combineLatest, defer, endWith, filter, finalize, firstValueFrom, from, identity, ignoreElements, isObservable, lastValueFrom, map, merge, mergeMap, mergeWith, NEVER, Observable, of, repeat, retry, scan, share, shareReplay, Subject, switchMap, take, takeUntil, tap, throwError, timeout, timer, } from "rxjs";
|
|
7
|
+
import { BehaviorSubject, catchError, combineLatest, defer, EMPTY, endWith, filter, finalize, firstValueFrom, from, identity, ignoreElements, isObservable, lastValueFrom, map, merge, mergeMap, mergeWith, NEVER, Observable, of, repeat, retry, scan, share, shareReplay, startWith, Subject, switchMap, take, takeUntil, takeWhile, tap, throwError, timeout, timer, } from "rxjs";
|
|
7
8
|
import { webSocket } from "rxjs/webSocket";
|
|
8
|
-
import {
|
|
9
|
-
import { markFromRelay } from "./operators/mark-from-relay.js";
|
|
9
|
+
import { completeWhen } from "./operators/complete-when.js";
|
|
10
10
|
const AUTH_REQUIRED_PREFIX = "auth-required:";
|
|
11
|
-
/** Default retry config for
|
|
11
|
+
/** Default reconnect/retry config for request, subscription, and publish. linear backoff */
|
|
12
12
|
const DEFAULT_RETRY_CONFIG = {
|
|
13
13
|
count: 3,
|
|
14
|
-
delay: 1000,
|
|
14
|
+
delay: (_err, count) => timer(count * 1000),
|
|
15
15
|
resetOnSuccess: true,
|
|
16
16
|
};
|
|
17
|
+
function normalizeRetryConfig(config) {
|
|
18
|
+
if (typeof config === "number")
|
|
19
|
+
return { ...DEFAULT_RETRY_CONFIG, count: config };
|
|
20
|
+
return { ...DEFAULT_RETRY_CONFIG, ...(config ?? {}) };
|
|
21
|
+
}
|
|
17
22
|
/** Flags for the negentropy sync type */
|
|
18
23
|
export var SyncDirection;
|
|
19
24
|
(function (SyncDirection) {
|
|
@@ -21,8 +26,45 @@ export var SyncDirection;
|
|
|
21
26
|
SyncDirection[SyncDirection["SEND"] = 2] = "SEND";
|
|
22
27
|
SyncDirection[SyncDirection["BOTH"] = 3] = "BOTH";
|
|
23
28
|
})(SyncDirection || (SyncDirection = {}));
|
|
24
|
-
/**
|
|
25
|
-
export class
|
|
29
|
+
/** Base error thrown when a relay closes a REQ or COUNT with a CLOSED message */
|
|
30
|
+
export class RelayClosedError extends Error {
|
|
31
|
+
reason;
|
|
32
|
+
constructor(reason) {
|
|
33
|
+
super(reason);
|
|
34
|
+
this.reason = reason;
|
|
35
|
+
this.name = "RelayClosedError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Thrown when the relay closes a subscription with an auth-required: prefix */
|
|
39
|
+
export class AuthRequiredError extends RelayClosedError {
|
|
40
|
+
constructor(reason) {
|
|
41
|
+
super(reason);
|
|
42
|
+
this.name = "AuthRequiredError";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** NIP-01 machine-readable prefixes that indicate an error condition on CLOSED/OK messages */
|
|
46
|
+
const CLOSED_ERROR_PREFIXES = {
|
|
47
|
+
"auth-required": AuthRequiredError,
|
|
48
|
+
unsupported: RelayClosedError,
|
|
49
|
+
error: RelayClosedError,
|
|
50
|
+
blocked: RelayClosedError,
|
|
51
|
+
restricted: RelayClosedError,
|
|
52
|
+
"rate-limited": RelayClosedError,
|
|
53
|
+
pow: RelayClosedError,
|
|
54
|
+
invalid: RelayClosedError,
|
|
55
|
+
duplicate: RelayClosedError,
|
|
56
|
+
mute: RelayClosedError,
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Parse a NIP-01 machine-readable CLOSED reason string into a typed error.
|
|
60
|
+
* Returns null if the reason has no recognized prefix — the relay closed gracefully
|
|
61
|
+
* and the observable should complete rather than error.
|
|
62
|
+
*/
|
|
63
|
+
function parseClosedError(reason) {
|
|
64
|
+
const ErrorClass = CLOSED_ERROR_PREFIXES[reason.split(":")[0]];
|
|
65
|
+
if (ErrorClass)
|
|
66
|
+
return new ErrorClass(reason);
|
|
67
|
+
return null;
|
|
26
68
|
}
|
|
27
69
|
/** A dummy filter that will return empty results */
|
|
28
70
|
const PING_FILTER = {
|
|
@@ -87,7 +129,7 @@ export class Relay {
|
|
|
87
129
|
open$ = new Subject();
|
|
88
130
|
/** An observable that emits when underlying websocket is closed */
|
|
89
131
|
close$ = new Subject();
|
|
90
|
-
/** An observable that emits when underlying websocket is closing due to
|
|
132
|
+
/** An observable that emits when underlying websocket is closing due to unsubscribe or complete */
|
|
91
133
|
closing$ = new Subject();
|
|
92
134
|
/** Tracks active req() operations by subscription ID */
|
|
93
135
|
reqs$ = new BehaviorSubject({});
|
|
@@ -125,8 +167,6 @@ export class Relay {
|
|
|
125
167
|
get reqs() {
|
|
126
168
|
return this.reqs$.value;
|
|
127
169
|
}
|
|
128
|
-
/** If an EOSE message is not seen in this time, emit one locally (default 10s) */
|
|
129
|
-
eoseTimeout = 10_000;
|
|
130
170
|
/** How long to wait for an OK message from the relay (default 10s) */
|
|
131
171
|
eventTimeout = 10_000;
|
|
132
172
|
/** How long to wait for a publish to complete (default 30s) */
|
|
@@ -139,9 +179,9 @@ export class Relay {
|
|
|
139
179
|
pingFrequency = 29_000;
|
|
140
180
|
/** How long to wait for EOSE response in milliseconds (default 20000) */
|
|
141
181
|
pingTimeout = 20_000;
|
|
142
|
-
/** Default retry config for subscription()
|
|
182
|
+
/** Default retry config for subscription() connection errors */
|
|
143
183
|
subscriptionReconnect;
|
|
144
|
-
/** Default retry config for request()
|
|
184
|
+
/** Default retry config for request() connection errors */
|
|
145
185
|
requestReconnect;
|
|
146
186
|
/** Default retry config for publish() method */
|
|
147
187
|
publishRetry;
|
|
@@ -174,8 +214,6 @@ export class Relay {
|
|
|
174
214
|
this.url = url;
|
|
175
215
|
this.log = this.log.extend(url);
|
|
176
216
|
// Set common options
|
|
177
|
-
if (opts?.eoseTimeout !== undefined)
|
|
178
|
-
this.eoseTimeout = opts.eoseTimeout;
|
|
179
217
|
if (opts?.eventTimeout !== undefined)
|
|
180
218
|
this.eventTimeout = opts.eventTimeout;
|
|
181
219
|
if (opts?.publishTimeout !== undefined)
|
|
@@ -191,8 +229,8 @@ export class Relay {
|
|
|
191
229
|
if (opts?.onUnresponsive !== undefined)
|
|
192
230
|
this.onUnresponsive = opts.onUnresponsive;
|
|
193
231
|
// Set retry configs
|
|
194
|
-
this.subscriptionReconnect =
|
|
195
|
-
this.requestReconnect =
|
|
232
|
+
this.subscriptionReconnect = normalizeRetryConfig(opts?.subscriptionReconnect);
|
|
233
|
+
this.requestReconnect = normalizeRetryConfig(opts?.requestReconnect);
|
|
196
234
|
this.publishRetry = { ...DEFAULT_RETRY_CONFIG, ...(opts?.publishRetry ?? {}) };
|
|
197
235
|
// Create an observable that tracks boolean authentication state
|
|
198
236
|
this.authenticated$ = this.authenticationResponse$.pipe(map((response) => response?.ok === true));
|
|
@@ -214,7 +252,7 @@ export class Relay {
|
|
|
214
252
|
this.log("Disconnected");
|
|
215
253
|
else
|
|
216
254
|
this.log("Failed to connect");
|
|
217
|
-
//
|
|
255
|
+
// Changed the connected state to false
|
|
218
256
|
if (this.connected$.value)
|
|
219
257
|
this.connected$.next(false);
|
|
220
258
|
// Increment the attempts counter
|
|
@@ -367,7 +405,7 @@ export class Relay {
|
|
|
367
405
|
if (!ready)
|
|
368
406
|
return NEVER;
|
|
369
407
|
// Only start the watch tower if the relay is ready
|
|
370
|
-
return merge(listenForAllMessages, listenForNotice, ListenForChallenge,
|
|
408
|
+
return merge(listenForAllMessages, listenForNotice, ListenForChallenge, pingHealthCheck).pipe(
|
|
371
409
|
// Never emit any values
|
|
372
410
|
ignoreElements(),
|
|
373
411
|
// Start the reconnect timer if the connection has an error
|
|
@@ -428,8 +466,15 @@ export class Relay {
|
|
|
428
466
|
send(message) {
|
|
429
467
|
this.socket.next(message);
|
|
430
468
|
}
|
|
431
|
-
/**
|
|
432
|
-
|
|
469
|
+
/**
|
|
470
|
+
* Create a REQ observable that emits OPEN, EVENT, EOSE, and CLOSED messages.
|
|
471
|
+
*
|
|
472
|
+
* `resubscribe` only repeats after the relay sends a clean CLOSED message for this REQ.
|
|
473
|
+
* `reconnect` only retries connection errors and does not retry relay CLOSED errors.
|
|
474
|
+
*/
|
|
475
|
+
req(filters, opts) {
|
|
476
|
+
const id = opts?.id ?? nanoid();
|
|
477
|
+
const waitForAuth = opts?.waitForAuth ?? true;
|
|
433
478
|
// Convert filters input into an observable, if its a normal value merge it with NEVER so it never completes
|
|
434
479
|
let input;
|
|
435
480
|
// Create input from filters input
|
|
@@ -441,84 +486,149 @@ export class Relay {
|
|
|
441
486
|
input = (isObservable(filters) ? filters : merge(of(filters), NEVER)).pipe(map((f) => (Array.isArray(f) ? f : [f])));
|
|
442
487
|
}
|
|
443
488
|
// Create an observable that completes when the upstream observable completes
|
|
444
|
-
const filtersComplete = input.pipe(ignoreElements(), endWith(
|
|
489
|
+
const filtersComplete = input.pipe(ignoreElements(), endWith(true));
|
|
490
|
+
// Track whether the relay already sent CLOSED so we skip the redundant client CLOSE
|
|
491
|
+
let relayClosedSub = false;
|
|
492
|
+
let shouldResubscribe = false;
|
|
445
493
|
// Create an observable that filters responses from the relay to just the ones for this REQ
|
|
446
494
|
const messages = this.socket.pipe(filter((m) => Array.isArray(m) && (m[0] === "EVENT" || m[0] === "CLOSED" || m[0] === "EOSE") && m[1] === id),
|
|
447
|
-
//
|
|
495
|
+
// Map NIP-01 messages to RelayReqMessage
|
|
496
|
+
map((m) => {
|
|
497
|
+
if (m[0] === "EVENT" && typeof m[2] === "object")
|
|
498
|
+
return { type: "EVENT", from: this.url, id: m[1], event: m[2] };
|
|
499
|
+
if (m[0] === "CLOSED")
|
|
500
|
+
return { type: "CLOSED", from: this.url, id: m[1], reason: m[2] ?? "" };
|
|
501
|
+
// EOSE
|
|
502
|
+
return { type: "EOSE", from: this.url, id: m[1] };
|
|
503
|
+
}),
|
|
504
|
+
// Throw typed errors for prefixed CLOSED; mark relay-closed before takeWhile sees it
|
|
505
|
+
tap((m) => {
|
|
506
|
+
if (m.type === "CLOSED") {
|
|
507
|
+
relayClosedSub = true;
|
|
508
|
+
const error = parseClosedError(m.reason);
|
|
509
|
+
if (error)
|
|
510
|
+
throw error;
|
|
511
|
+
shouldResubscribe = true;
|
|
512
|
+
}
|
|
513
|
+
}),
|
|
514
|
+
// Complete the stream on unprefixed CLOSED, emitting the CLOSED message last (inclusive)
|
|
515
|
+
takeWhile((m) => m.type !== "CLOSED", true),
|
|
516
|
+
// Singleton (prevents the .pipe() operator later from sending two REQ messages)
|
|
448
517
|
share());
|
|
449
518
|
// Create an observable that controls sending the filters and closing the REQ
|
|
450
519
|
const control = input.pipe(
|
|
451
520
|
// Send the filters when they change
|
|
452
|
-
|
|
521
|
+
map((filters) => {
|
|
522
|
+
// Reset closed flag on each new REQ (resubscribe cycles)
|
|
523
|
+
relayClosedSub = false;
|
|
524
|
+
shouldResubscribe = false;
|
|
453
525
|
this.socket.next(["REQ", id, ...filters]);
|
|
454
526
|
// Add to tracking when REQ is sent
|
|
455
527
|
this.reqs$.next({ ...this.reqs$.value, [id]: filters });
|
|
528
|
+
return { type: "OPEN", id, filters, from: this.url };
|
|
456
529
|
}),
|
|
457
|
-
// Send
|
|
530
|
+
// Send CLOSE when unsubscribed or input completes, but not if relay already sent CLOSED
|
|
458
531
|
finalize(() => {
|
|
459
|
-
|
|
532
|
+
if (!relayClosedSub)
|
|
533
|
+
this.socket.next(["CLOSE", id]);
|
|
460
534
|
// Remove from tracking when REQ closes
|
|
461
535
|
const { [id]: _, ...rest } = this.reqs$.value;
|
|
462
536
|
this.reqs$.next(rest);
|
|
463
537
|
}),
|
|
464
538
|
// Once filters have been sent, switch to listening for messages
|
|
465
|
-
switchMap(() => messages
|
|
539
|
+
switchMap((openMessage) => messages.pipe(
|
|
540
|
+
// Pass along the OPEN message for listeners
|
|
541
|
+
startWith(openMessage))));
|
|
466
542
|
// Start the watch tower with the observables
|
|
467
543
|
const observable = merge(this.watchTower, control).pipe(
|
|
468
|
-
// Complete
|
|
469
|
-
// This is to work around the fact that merge() waits for both observables to complete
|
|
544
|
+
// Complete when messages completes (e.g. unprefixed CLOSED = graceful relay close)
|
|
470
545
|
takeUntil(messages.pipe(ignoreElements(), endWith(true))),
|
|
471
546
|
// Complete the subscription when the input is completed
|
|
472
547
|
takeUntil(filtersComplete),
|
|
473
|
-
// Map the messages to events, EOSE, or throw an error
|
|
474
|
-
map((message) => {
|
|
475
|
-
if (message[0] === "EOSE")
|
|
476
|
-
return "EOSE";
|
|
477
|
-
else if (message[0] === "CLOSED")
|
|
478
|
-
throw new ReqCloseError(message[2]);
|
|
479
|
-
else
|
|
480
|
-
return message[2];
|
|
481
|
-
}), this.handleAuthRequiredForReq("REQ"),
|
|
482
548
|
// mark events as from relays
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
549
|
+
tap((message) => {
|
|
550
|
+
if (message.type === "EVENT")
|
|
551
|
+
addSeenRelay(message.event, this.url);
|
|
552
|
+
}),
|
|
553
|
+
// Only create one upstream subscription
|
|
554
|
+
share());
|
|
555
|
+
// Wait for auth only when enabled and make sure to start the watch tower
|
|
556
|
+
const reqWithAuthStrategy = waitForAuth ? this.waitForAuth(this.authRequiredForRead$, observable) : observable;
|
|
557
|
+
return defer(() => this.waitForReady(reqWithAuthStrategy)).pipe(
|
|
558
|
+
// Retry only auth-required errors, optionally waiting for authentication first
|
|
559
|
+
retry({
|
|
560
|
+
delay: (error) => {
|
|
561
|
+
// Re-throw non-auth-required errors
|
|
562
|
+
if (!(error instanceof AuthRequiredError))
|
|
563
|
+
return throwError(() => error);
|
|
564
|
+
// Flip the flag to indicate that auth is required
|
|
565
|
+
this.log(`Auth required for REQ`);
|
|
566
|
+
this.receivedAuthRequiredForReq.next(true);
|
|
567
|
+
// If not waiting for auth , re-throw the error
|
|
568
|
+
if (!waitForAuth)
|
|
569
|
+
return throwError(() => error);
|
|
570
|
+
// Wait for authentication before retrying
|
|
571
|
+
return this.authenticated$.pipe(filter((authenticated) => authenticated), take(1));
|
|
572
|
+
},
|
|
489
573
|
}),
|
|
574
|
+
// Retry connection errors independently from relay CLOSED errors
|
|
575
|
+
this.customConnectionRetryOperator(opts?.reconnect),
|
|
576
|
+
// Resubscribe only after the relay cleanly CLOSED this REQ
|
|
577
|
+
this.customRepeatOperator(opts?.resubscribe, () => shouldResubscribe),
|
|
490
578
|
// Only create one upstream subscription
|
|
491
579
|
share());
|
|
492
|
-
// Wait for auth if required and make sure to start the watch tower
|
|
493
|
-
return this.waitForReady(this.waitForAuth(this.authRequiredForRead$, observable));
|
|
494
580
|
}
|
|
495
581
|
/** Create a COUNT observable that emits a single count response */
|
|
496
582
|
count(filters, id = nanoid()) {
|
|
583
|
+
// Track whether the relay already sent CLOSED so we skip the redundant client CLOSE
|
|
584
|
+
let relayClosedSub = false;
|
|
497
585
|
// Create an observable that filters responses from the relay to just the ones for this COUNT
|
|
498
|
-
const messages = this.socket.pipe(filter((m) => Array.isArray(m) && (m[0] === "COUNT" || m[0] === "CLOSED") && m[1] === id)
|
|
586
|
+
const messages = this.socket.pipe(filter((m) => Array.isArray(m) && (m[0] === "COUNT" || m[0] === "CLOSED") && m[1] === id),
|
|
587
|
+
// Map to typed response, throwing on error-prefixed CLOSED
|
|
588
|
+
map((m) => {
|
|
589
|
+
if (m[0] === "COUNT")
|
|
590
|
+
return m[2];
|
|
591
|
+
else if (m[0] === "CLOSED") {
|
|
592
|
+
relayClosedSub = true;
|
|
593
|
+
const error = parseClosedError(m[2] ?? "");
|
|
594
|
+
if (error)
|
|
595
|
+
throw error;
|
|
596
|
+
}
|
|
597
|
+
return null;
|
|
598
|
+
}),
|
|
599
|
+
// Complete the stream on any CLOSED (including graceful close), emitting COUNT last
|
|
600
|
+
takeWhile((m) => m !== null, true), filter((m) => m !== null),
|
|
601
|
+
// Singleton
|
|
602
|
+
share());
|
|
499
603
|
// Send the COUNT message and listen for response
|
|
500
|
-
const
|
|
604
|
+
const control = defer(() => {
|
|
605
|
+
// Reset closed flag on each new COUNT
|
|
606
|
+
relayClosedSub = false;
|
|
501
607
|
// Send the COUNT message when subscription starts
|
|
502
608
|
this.socket.next(Array.isArray(filters) ? ["COUNT", id, ...filters] : ["COUNT", id, filters]);
|
|
503
|
-
|
|
504
|
-
return merge(this.watchTower, messages);
|
|
609
|
+
return messages;
|
|
505
610
|
}).pipe(
|
|
506
|
-
//
|
|
507
|
-
|
|
508
|
-
if (
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
611
|
+
// Send CLOSE when unsubscribed, but not if relay already sent CLOSED
|
|
612
|
+
finalize(() => {
|
|
613
|
+
if (!relayClosedSub)
|
|
614
|
+
this.socket.next(["CLOSE", id]);
|
|
615
|
+
}));
|
|
616
|
+
const observable = merge(this.watchTower, control).pipe(
|
|
617
|
+
// Complete when messages completes (unprefixed CLOSED = graceful relay close)
|
|
618
|
+
takeUntil(messages.pipe(ignoreElements(), endWith(true))),
|
|
513
619
|
// Complete on first value (COUNT responses are single-shot)
|
|
514
620
|
take(1),
|
|
515
|
-
//
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
621
|
+
// Set auth required flag when relay closes with auth-required
|
|
622
|
+
catchError((error) => {
|
|
623
|
+
if (error instanceof AuthRequiredError && !this.receivedAuthRequiredForReq.value) {
|
|
624
|
+
this.log(`Auth required for COUNT`);
|
|
625
|
+
this.receivedAuthRequiredForReq.next(true);
|
|
626
|
+
}
|
|
627
|
+
return throwError(() => error);
|
|
628
|
+
}),
|
|
629
|
+
// Throw an error if no COUNT response received within 10 seconds
|
|
630
|
+
timeout({ first: 10_000, with: () => throwError(() => new Error("COUNT timeout")) }));
|
|
520
631
|
// Start the watch tower and wait for auth if required
|
|
521
|
-
// Use share() to prevent multiple subscriptions from creating duplicate COUNT messages
|
|
522
632
|
return this.waitForReady(this.waitForAuth(this.authRequiredForRead$, observable)).pipe(share());
|
|
523
633
|
}
|
|
524
634
|
/** Send an EVENT or AUTH message and return an observable of PublishResponse that completes or errors */
|
|
@@ -546,7 +656,7 @@ export class Relay {
|
|
|
546
656
|
this.receivedAuthRequiredForEvent.next(true);
|
|
547
657
|
}
|
|
548
658
|
}),
|
|
549
|
-
// if no message is seen in 10s, emit
|
|
659
|
+
// if no message is seen in 10s, emit failed publish response
|
|
550
660
|
timeout({
|
|
551
661
|
first: this.eventTimeout,
|
|
552
662
|
with: () => of({ ok: false, from: this.url, message: "Timeout" }),
|
|
@@ -584,9 +694,9 @@ export class Relay {
|
|
|
584
694
|
const start = p instanceof Promise ? from(p) : of(p);
|
|
585
695
|
return lastValueFrom(start.pipe(switchMap((event) => this.auth(event))));
|
|
586
696
|
}
|
|
587
|
-
/** Internal operator for creating
|
|
697
|
+
/** Internal operator for creating a retry() operator */
|
|
588
698
|
customRetryOperator(times, base) {
|
|
589
|
-
if (times === false)
|
|
699
|
+
if (times === false || times === undefined)
|
|
590
700
|
return identity;
|
|
591
701
|
else if (typeof times === "number")
|
|
592
702
|
return retry({ ...base, count: times });
|
|
@@ -595,16 +705,45 @@ export class Relay {
|
|
|
595
705
|
else
|
|
596
706
|
return retry({ ...base, ...times });
|
|
597
707
|
}
|
|
598
|
-
/** Internal operator for
|
|
599
|
-
|
|
708
|
+
/** Internal operator for retrying connection failures without retrying relay CLOSED errors */
|
|
709
|
+
customConnectionRetryOperator(times, base) {
|
|
600
710
|
if (times === false || times === undefined)
|
|
601
711
|
return identity;
|
|
602
|
-
|
|
603
|
-
|
|
712
|
+
const config = typeof times === "number" ? { ...base, count: times } : times === true ? (base ?? {}) : { ...base, ...times };
|
|
713
|
+
return retry({
|
|
714
|
+
...config,
|
|
715
|
+
delay: (error, count) => {
|
|
716
|
+
if (error instanceof RelayClosedError)
|
|
717
|
+
return throwError(() => error);
|
|
718
|
+
if (typeof config.delay === "number")
|
|
719
|
+
return timer(config.delay);
|
|
720
|
+
if (typeof config.delay === "function")
|
|
721
|
+
return config.delay(error, count);
|
|
722
|
+
return of(null);
|
|
723
|
+
},
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
/** Internal operator for creating the repeat() operator, optionally gated by a condition */
|
|
727
|
+
customRepeatOperator(times, condition) {
|
|
728
|
+
if (times === false || times === undefined)
|
|
729
|
+
return identity;
|
|
730
|
+
const delay = (repeatCount) => {
|
|
731
|
+
if (condition && !condition())
|
|
732
|
+
return EMPTY;
|
|
733
|
+
if (typeof times === "object") {
|
|
734
|
+
if (typeof times.delay === "number")
|
|
735
|
+
return timer(times.delay);
|
|
736
|
+
if (typeof times.delay === "function")
|
|
737
|
+
return times.delay(repeatCount);
|
|
738
|
+
}
|
|
739
|
+
return of(null);
|
|
740
|
+
};
|
|
741
|
+
if (times === true)
|
|
742
|
+
return repeat({ delay });
|
|
604
743
|
else if (typeof times === "number")
|
|
605
|
-
return repeat(times);
|
|
744
|
+
return repeat({ count: times, delay });
|
|
606
745
|
else
|
|
607
|
-
return repeat(times);
|
|
746
|
+
return repeat({ ...times, delay });
|
|
608
747
|
}
|
|
609
748
|
/** Internal operator for creating the timeout() operator */
|
|
610
749
|
customTimeoutOperator(timeout, defaultTimeout) {
|
|
@@ -618,39 +757,36 @@ export class Relay {
|
|
|
618
757
|
else
|
|
619
758
|
return simpleTimeout(timeout ?? defaultTimeout);
|
|
620
759
|
}
|
|
621
|
-
/**
|
|
622
|
-
handleAuthRequiredForReq(operation) {
|
|
623
|
-
return catchError((error) => {
|
|
624
|
-
// Set auth required if the operation is closed with auth-required
|
|
625
|
-
if (error instanceof ReqCloseError &&
|
|
626
|
-
error.message.startsWith(AUTH_REQUIRED_PREFIX) &&
|
|
627
|
-
!this.receivedAuthRequiredForReq.value) {
|
|
628
|
-
this.log(`Auth required for ${operation}`);
|
|
629
|
-
this.receivedAuthRequiredForReq.next(true);
|
|
630
|
-
}
|
|
631
|
-
// Pass the error through
|
|
632
|
-
return throwError(() => error);
|
|
633
|
-
});
|
|
634
|
-
}
|
|
635
|
-
/** Creates a REQ that retries when relay errors ( default 3 retries ) */
|
|
760
|
+
/** Creates a persistent REQ that retries connection errors (default 3 retries) */
|
|
636
761
|
subscription(filters, opts) {
|
|
637
|
-
return this.req(filters,
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
762
|
+
return this.req(filters, {
|
|
763
|
+
...opts,
|
|
764
|
+
reconnect: opts?.reconnect ?? this.subscriptionReconnect,
|
|
765
|
+
}).pipe(
|
|
766
|
+
// Filter for EOSE messages
|
|
767
|
+
filter((message) => message.type === "EOSE" || message.type === "EVENT"),
|
|
768
|
+
// Extract EOSE messages
|
|
769
|
+
map((message) => (message.type === "EOSE" ? "EOSE" : message.event)),
|
|
642
770
|
// Single subscription
|
|
643
771
|
share());
|
|
644
772
|
}
|
|
645
|
-
/** Makes a single request that
|
|
773
|
+
/** Makes a single request that retries connection errors and completes on EOSE */
|
|
646
774
|
request(filters, opts) {
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
775
|
+
const req = this.req(filters, {
|
|
776
|
+
...opts,
|
|
777
|
+
reconnect: opts?.reconnect ?? this.requestReconnect,
|
|
778
|
+
});
|
|
779
|
+
return req.pipe(
|
|
780
|
+
// Add completion condition
|
|
781
|
+
opts?.complete ? completeWhen(opts?.complete) : identity,
|
|
782
|
+
// Apply request timeout
|
|
783
|
+
timeout({ first: opts?.timeout ?? 30_000 }),
|
|
652
784
|
// Complete when EOSE is received
|
|
653
|
-
|
|
785
|
+
takeWhile((message) => message.type !== "EOSE"),
|
|
786
|
+
// Filter only for event messages
|
|
787
|
+
filter((message) => message.type === "EVENT"),
|
|
788
|
+
// Extract event messages
|
|
789
|
+
map((message) => message.event),
|
|
654
790
|
// Single subscription
|
|
655
791
|
share());
|
|
656
792
|
}
|
|
@@ -659,7 +795,7 @@ export class Relay {
|
|
|
659
795
|
return lastValueFrom(this.event(event).pipe(mergeMap((result) => {
|
|
660
796
|
// If the relay responds with auth-required, throw an error for the retry operator to handle
|
|
661
797
|
if (result.ok === false && result.message?.startsWith(AUTH_REQUIRED_PREFIX))
|
|
662
|
-
return throwError(() => new
|
|
798
|
+
return throwError(() => new AuthRequiredError(result.message ?? ""));
|
|
663
799
|
return of(result);
|
|
664
800
|
}),
|
|
665
801
|
// Retry the publish until it succeeds or the number of retries is reached
|
|
@@ -668,7 +804,7 @@ export class Relay {
|
|
|
668
804
|
this.customTimeoutOperator(opts?.timeout, this.publishTimeout)));
|
|
669
805
|
}
|
|
670
806
|
/** Negentropy sync events with the relay and an event store */
|
|
671
|
-
sync(store,
|
|
807
|
+
sync(store, filters, direction = SyncDirection.RECEIVE) {
|
|
672
808
|
const getEvents = async (ids) => {
|
|
673
809
|
if (Array.isArray(store))
|
|
674
810
|
return store.filter((event) => ids.includes(event.id));
|
|
@@ -685,7 +821,7 @@ export class Relay {
|
|
|
685
821
|
controller.abort();
|
|
686
822
|
}
|
|
687
823
|
};
|
|
688
|
-
this.negentropy(store,
|
|
824
|
+
this.negentropy(store, filters, async (have, need) => {
|
|
689
825
|
// NOTE: it may be more efficient to sync all the events later in a single batch
|
|
690
826
|
// Send missing events to the relay
|
|
691
827
|
if (direction & SyncDirection.SEND && have.length > 0) {
|
|
@@ -697,7 +833,11 @@ export class Relay {
|
|
|
697
833
|
if (direction & SyncDirection.RECEIVE && need.length > 0) {
|
|
698
834
|
await lastValueFrom(this.req({ ids: need }).pipe(
|
|
699
835
|
// Complete when EOSE is received
|
|
700
|
-
|
|
836
|
+
takeWhile((message) => message.type !== "EOSE"),
|
|
837
|
+
// Filter only for event messages
|
|
838
|
+
filter((message) => message.type === "EVENT"),
|
|
839
|
+
// Extract event messages
|
|
840
|
+
map((message) => message.event),
|
|
701
841
|
// Add events to the store if its writable
|
|
702
842
|
Reflect.has(store, "add")
|
|
703
843
|
? mapEventsToStore(store)
|
|
@@ -756,4 +896,28 @@ export class Relay {
|
|
|
756
896
|
return timer(delay);
|
|
757
897
|
};
|
|
758
898
|
}
|
|
899
|
+
/** A complete condition that waits for the subscription to open */
|
|
900
|
+
static afterOpen(condition) {
|
|
901
|
+
return (source) => source.pipe(
|
|
902
|
+
// Wait for subscription open
|
|
903
|
+
filter((m) => m.type === "OPEN"),
|
|
904
|
+
// Switch to timer for complete
|
|
905
|
+
switchMap(() => source.pipe(condition)));
|
|
906
|
+
}
|
|
907
|
+
/** An OR complete condition, that completes when either condition is truthy */
|
|
908
|
+
static completeOr(...conditions) {
|
|
909
|
+
return (source) => combineLatest(conditions.map((condition) => source.pipe(condition, startWith(false)))).pipe(
|
|
910
|
+
// Return true if any condition is truthy
|
|
911
|
+
map((all) => all.some((v) => !!v)));
|
|
912
|
+
}
|
|
913
|
+
/** An AND complete condition, that completes when all conditions are truthy */
|
|
914
|
+
static completeAnd(...conditions) {
|
|
915
|
+
return (source) => combineLatest(conditions.map((condition) => source.pipe(condition, startWith(false)))).pipe(
|
|
916
|
+
// Return true if all conditions are truthy
|
|
917
|
+
map((all) => all.every((v) => !!v)));
|
|
918
|
+
}
|
|
919
|
+
/** A default complete condition that waits for the subscription to open and then completes after a timeout */
|
|
920
|
+
static defaultComplete(timeout, afterOpen = 10_000) {
|
|
921
|
+
return this.completeOr(() => timer(timeout), this.afterOpen(() => timer(afterOpen)));
|
|
922
|
+
}
|
|
759
923
|
}
|