applesauce-relay 0.0.0-next-20250930093922 → 0.0.0-next-20251020143053
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 +2 -3
- package/dist/group.d.ts +24 -8
- package/dist/group.js +140 -38
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/liveness.d.ts +123 -0
- package/dist/liveness.js +327 -0
- package/dist/operators/index.d.ts +2 -0
- package/dist/operators/index.js +2 -0
- package/dist/operators/liveness.d.ts +17 -0
- package/dist/operators/liveness.js +47 -0
- package/dist/operators/reverse-switch-map.d.ts +9 -0
- package/dist/operators/reverse-switch-map.js +46 -0
- package/dist/pool.d.ts +13 -14
- package/dist/pool.js +24 -38
- package/dist/relay.d.ts +7 -3
- package/dist/relay.js +61 -16
- package/dist/types.d.ts +35 -15
- package/package.json +3 -3
package/dist/liveness.js
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { logger } from "applesauce-core";
|
|
2
|
+
import { BehaviorSubject, map } from "rxjs";
|
|
3
|
+
/** Record and manage liveness reports for relays */
|
|
4
|
+
export class RelayLiveness {
|
|
5
|
+
log = logger.extend("RelayLiveness");
|
|
6
|
+
options;
|
|
7
|
+
states$ = new BehaviorSubject({});
|
|
8
|
+
/** Relays that have been seen this session. this should be used when checking dead relays for liveness */
|
|
9
|
+
seen = new Set();
|
|
10
|
+
/** Storage adapter for persistence */
|
|
11
|
+
storage;
|
|
12
|
+
/** An observable of all relays that are online */
|
|
13
|
+
online$;
|
|
14
|
+
/** An observable of all relays that are offline */
|
|
15
|
+
offline$;
|
|
16
|
+
/** An observable of all relays that are dead */
|
|
17
|
+
dead$;
|
|
18
|
+
/** An observable of all relays that are online or not in backoff */
|
|
19
|
+
healthy$;
|
|
20
|
+
/** An observable of all relays that are dead or in backoff */
|
|
21
|
+
unhealthy$;
|
|
22
|
+
/** Relays that are known to be online */
|
|
23
|
+
get online() {
|
|
24
|
+
return Object.keys(this.states$.value).filter((relay) => this.states$.value[relay].state === "online");
|
|
25
|
+
}
|
|
26
|
+
/** Relays that are known to be offline */
|
|
27
|
+
get offline() {
|
|
28
|
+
return Object.keys(this.states$.value).filter((relay) => this.states$.value[relay].state === "offline");
|
|
29
|
+
}
|
|
30
|
+
/** Relays that are known to be dead */
|
|
31
|
+
get dead() {
|
|
32
|
+
return Object.keys(this.states$.value).filter((relay) => this.states$.value[relay].state === "dead");
|
|
33
|
+
}
|
|
34
|
+
/** Relays that are online or not in backoff */
|
|
35
|
+
get healthy() {
|
|
36
|
+
return Object.keys(this.states$.value).filter((relay) => {
|
|
37
|
+
const state = this.states$.value[relay];
|
|
38
|
+
return state.state === "online" || (state.state === "offline" && !this.isInBackoff(relay));
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/** Relays that are dead or in backoff */
|
|
42
|
+
get unhealthy() {
|
|
43
|
+
return Object.keys(this.states$.value).filter((relay) => {
|
|
44
|
+
const state = this.states$.value[relay];
|
|
45
|
+
return state.state === "dead" || (state.state === "offline" && this.isInBackoff(relay));
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Create a new RelayLiveness instance
|
|
50
|
+
* @param options Configuration options for the liveness tracker
|
|
51
|
+
*/
|
|
52
|
+
constructor(options = {}) {
|
|
53
|
+
this.options = {
|
|
54
|
+
maxFailuresBeforeDead: options.maxFailuresBeforeDead ?? 5,
|
|
55
|
+
backoffBaseDelay: options.backoffBaseDelay ?? 30 * 1000, // 30 seconds
|
|
56
|
+
backoffMaxDelay: options.backoffMaxDelay ?? 5 * 60 * 1000, // 5 minutes
|
|
57
|
+
};
|
|
58
|
+
this.storage = options.storage;
|
|
59
|
+
// Create observable interfaces
|
|
60
|
+
this.online$ = this.states$.pipe(map((states) => Object.keys(states).filter((relay) => states[relay].state === "online")));
|
|
61
|
+
this.offline$ = this.states$.pipe(map((states) => Object.keys(states).filter((relay) => states[relay].state === "offline")));
|
|
62
|
+
this.dead$ = this.states$.pipe(map((states) => Object.keys(states).filter((relay) => states[relay].state === "dead")));
|
|
63
|
+
this.healthy$ = this.states$.pipe(map((states) => Object.keys(states).filter((relay) => {
|
|
64
|
+
const state = states[relay];
|
|
65
|
+
return state.state === "online" || (state.state === "offline" && !this.isInBackoff(relay));
|
|
66
|
+
})));
|
|
67
|
+
this.unhealthy$ = this.states$.pipe(map((states) => Object.keys(states).filter((relay) => {
|
|
68
|
+
const state = states[relay];
|
|
69
|
+
return state.state === "dead" || (state.state === "offline" && this.isInBackoff(relay));
|
|
70
|
+
})));
|
|
71
|
+
}
|
|
72
|
+
/** Load relay states from storage */
|
|
73
|
+
async load() {
|
|
74
|
+
if (!this.storage)
|
|
75
|
+
return;
|
|
76
|
+
const known = await this.storage.getItem("known");
|
|
77
|
+
if (!Array.isArray(known))
|
|
78
|
+
return;
|
|
79
|
+
this.log(`Loading states for ${known.length} known relays`);
|
|
80
|
+
const states = {};
|
|
81
|
+
for (const relay of known) {
|
|
82
|
+
try {
|
|
83
|
+
const state = await this.storage.getItem(relay);
|
|
84
|
+
if (state)
|
|
85
|
+
states[relay] = state;
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
// Ignore relay loading errors
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
this.states$.next(states);
|
|
92
|
+
}
|
|
93
|
+
/** Save all known relays and their states to storage */
|
|
94
|
+
async save() {
|
|
95
|
+
await this.saveKnownRelays();
|
|
96
|
+
await Promise.all(Object.entries(this.states$.value).map(([relay, state]) => this.saveRelayState(relay, state)));
|
|
97
|
+
this.log("Relay states saved to storage");
|
|
98
|
+
}
|
|
99
|
+
/** Filter relay list, removing dead relays and relays in backoff */
|
|
100
|
+
filter(relays) {
|
|
101
|
+
const results = [];
|
|
102
|
+
for (const relay of relays) {
|
|
103
|
+
// Track that this relay has been seen
|
|
104
|
+
this.seen.add(relay);
|
|
105
|
+
const state = this.getState(relay);
|
|
106
|
+
// Filter based on state and backoff
|
|
107
|
+
switch (state?.state) {
|
|
108
|
+
case undefined: // unknown state
|
|
109
|
+
case "online":
|
|
110
|
+
results.push(relay);
|
|
111
|
+
break;
|
|
112
|
+
case "offline":
|
|
113
|
+
// Only include if not in backoff
|
|
114
|
+
if (!this.isInBackoff(relay))
|
|
115
|
+
results.push(relay);
|
|
116
|
+
break;
|
|
117
|
+
case "dead":
|
|
118
|
+
default:
|
|
119
|
+
// Don't include dead relays
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return results;
|
|
124
|
+
}
|
|
125
|
+
/** Subscribe to a relays state */
|
|
126
|
+
state(relay) {
|
|
127
|
+
return this.states$.pipe(map((states) => states[relay]));
|
|
128
|
+
}
|
|
129
|
+
/** Revive a dead relay with the max backoff delay */
|
|
130
|
+
revive(relay) {
|
|
131
|
+
const state = this.getState(relay);
|
|
132
|
+
if (!state)
|
|
133
|
+
return;
|
|
134
|
+
this.updateRelayState(relay, {
|
|
135
|
+
state: "offline",
|
|
136
|
+
failureCount: 0,
|
|
137
|
+
lastFailureTime: 0,
|
|
138
|
+
lastSuccessTime: Date.now(),
|
|
139
|
+
backoffUntil: this.options.backoffMaxDelay,
|
|
140
|
+
});
|
|
141
|
+
this.log(`Relay ${relay} revived to offline state with max backoff delay`);
|
|
142
|
+
}
|
|
143
|
+
/** Get current relay health state for a relay */
|
|
144
|
+
getState(relay) {
|
|
145
|
+
return this.states$.value[relay];
|
|
146
|
+
}
|
|
147
|
+
/** Check if a relay is currently in backoff period */
|
|
148
|
+
isInBackoff(relay) {
|
|
149
|
+
const state = this.getState(relay);
|
|
150
|
+
if (!state?.backoffUntil)
|
|
151
|
+
return false;
|
|
152
|
+
return Date.now() < state.backoffUntil;
|
|
153
|
+
}
|
|
154
|
+
/** Get remaining backoff time for a relay (in ms) */
|
|
155
|
+
getBackoffRemaining(relay) {
|
|
156
|
+
const state = this.getState(relay);
|
|
157
|
+
if (!state?.backoffUntil)
|
|
158
|
+
return 0;
|
|
159
|
+
return Math.max(0, state.backoffUntil - Date.now());
|
|
160
|
+
}
|
|
161
|
+
/** Calculate backoff delay based on failure count */
|
|
162
|
+
calculateBackoffDelay(failureCount) {
|
|
163
|
+
const delay = this.options.backoffBaseDelay * Math.pow(2, failureCount - 1);
|
|
164
|
+
return Math.min(delay, this.options.backoffMaxDelay);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Record a successful connection
|
|
168
|
+
* @param relay The relay URL that succeeded
|
|
169
|
+
*/
|
|
170
|
+
recordSuccess(relay) {
|
|
171
|
+
const now = Date.now();
|
|
172
|
+
const state = this.getState(relay);
|
|
173
|
+
// Don't update dead relays
|
|
174
|
+
if (state?.state === "dead") {
|
|
175
|
+
this.log(`Ignoring success for dead relay ${relay}`);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
// Record new relays
|
|
179
|
+
if (state === undefined) {
|
|
180
|
+
this.seen.add(relay);
|
|
181
|
+
this.saveKnownRelays();
|
|
182
|
+
}
|
|
183
|
+
// TODO: resetting the state back to online might be too aggressive?
|
|
184
|
+
const newState = {
|
|
185
|
+
state: "online",
|
|
186
|
+
failureCount: 0,
|
|
187
|
+
lastFailureTime: 0,
|
|
188
|
+
lastSuccessTime: now,
|
|
189
|
+
};
|
|
190
|
+
this.updateRelayState(relay, newState);
|
|
191
|
+
// Log transition if it's not the first time we've seen the relay
|
|
192
|
+
if (state && state.state !== newState.state)
|
|
193
|
+
this.log(`Relay ${relay} transitioned ${state?.state} -> online`);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Record a failed connection
|
|
197
|
+
* @param relay The relay URL that failed
|
|
198
|
+
*/
|
|
199
|
+
recordFailure(relay) {
|
|
200
|
+
const state = this.getState(relay);
|
|
201
|
+
// Don't update dead relays
|
|
202
|
+
if (state?.state === "dead")
|
|
203
|
+
return;
|
|
204
|
+
// Ignore failures during backoff, this should help catch double reporting of failures
|
|
205
|
+
if (this.isInBackoff(relay))
|
|
206
|
+
return;
|
|
207
|
+
const now = Date.now();
|
|
208
|
+
const failureCount = (state?.failureCount || 0) + 1;
|
|
209
|
+
// Record new relays
|
|
210
|
+
if (state === undefined) {
|
|
211
|
+
this.seen.add(relay);
|
|
212
|
+
this.saveKnownRelays();
|
|
213
|
+
}
|
|
214
|
+
// Calculate backoff delay
|
|
215
|
+
const backoffDelay = this.calculateBackoffDelay(failureCount);
|
|
216
|
+
const newState = failureCount >= this.options.maxFailuresBeforeDead ? "dead" : "offline";
|
|
217
|
+
const relayState = {
|
|
218
|
+
state: newState,
|
|
219
|
+
failureCount,
|
|
220
|
+
lastFailureTime: now,
|
|
221
|
+
lastSuccessTime: state?.lastSuccessTime || 0,
|
|
222
|
+
backoffUntil: now + backoffDelay,
|
|
223
|
+
};
|
|
224
|
+
this.updateRelayState(relay, relayState);
|
|
225
|
+
// Log transition if it's not the first time we've seen the relay
|
|
226
|
+
if (newState !== state?.state)
|
|
227
|
+
this.log(`Relay ${relay} transitioned ${state?.state} -> ${newState}`);
|
|
228
|
+
// Set a timeout that will clear the backoff period
|
|
229
|
+
setTimeout(() => {
|
|
230
|
+
const state = this.getState(relay);
|
|
231
|
+
if (!state || state.backoffUntil === undefined)
|
|
232
|
+
return;
|
|
233
|
+
this.updateRelayState(relay, { ...state, backoffUntil: undefined });
|
|
234
|
+
}, backoffDelay);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Get all seen relays (for debugging/monitoring)
|
|
238
|
+
*/
|
|
239
|
+
getSeenRelays() {
|
|
240
|
+
return Array.from(this.seen);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Reset state for one or all relays
|
|
244
|
+
* @param relay Optional specific relay URL to reset, or reset all if not provided
|
|
245
|
+
*/
|
|
246
|
+
reset(relay) {
|
|
247
|
+
if (relay) {
|
|
248
|
+
const newStates = { ...this.states$.value };
|
|
249
|
+
delete newStates[relay];
|
|
250
|
+
this.states$.next(newStates);
|
|
251
|
+
this.seen.delete(relay);
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
// Reset all relays
|
|
255
|
+
this.states$.next({});
|
|
256
|
+
this.seen.clear();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// The connected pools and cleanup methods
|
|
260
|
+
connections = new Map();
|
|
261
|
+
/** Connect to a {@link RelayPool} instance and track relay connections */
|
|
262
|
+
connectToPool(pool) {
|
|
263
|
+
// Relay cleanup methods
|
|
264
|
+
const relays = new Map();
|
|
265
|
+
// Listen for relays being added
|
|
266
|
+
const add = pool.add$.subscribe((relay) => {
|
|
267
|
+
// Record seen relays
|
|
268
|
+
this.seen.add(relay.url);
|
|
269
|
+
const open = relay.open$.subscribe(() => {
|
|
270
|
+
this.recordSuccess(relay.url);
|
|
271
|
+
});
|
|
272
|
+
const close = relay.close$.subscribe((event) => {
|
|
273
|
+
if (event.wasClean === false)
|
|
274
|
+
this.recordFailure(relay.url);
|
|
275
|
+
});
|
|
276
|
+
// Register the cleanup method
|
|
277
|
+
relays.set(relay, () => {
|
|
278
|
+
open.unsubscribe();
|
|
279
|
+
close.unsubscribe();
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
// Listen for relays being removed
|
|
283
|
+
const remove = pool.remove$.subscribe((relay) => {
|
|
284
|
+
const cleanup = relays.get(relay);
|
|
285
|
+
if (cleanup)
|
|
286
|
+
cleanup();
|
|
287
|
+
relays.delete(relay);
|
|
288
|
+
});
|
|
289
|
+
// register the cleanup method
|
|
290
|
+
this.connections.set(pool, () => {
|
|
291
|
+
add.unsubscribe();
|
|
292
|
+
remove.unsubscribe();
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
/** Disconnect from a {@link RelayPool} instance */
|
|
296
|
+
disconnectFromPool(pool) {
|
|
297
|
+
const cleanup = this.connections.get(pool);
|
|
298
|
+
if (cleanup)
|
|
299
|
+
cleanup();
|
|
300
|
+
this.connections.delete(pool);
|
|
301
|
+
}
|
|
302
|
+
updateRelayState(relay, state) {
|
|
303
|
+
this.states$.next({ ...this.states$.value, [relay]: state });
|
|
304
|
+
// Auto-save to storage
|
|
305
|
+
this.saveRelayState(relay, state);
|
|
306
|
+
}
|
|
307
|
+
async saveKnownRelays() {
|
|
308
|
+
if (!this.storage)
|
|
309
|
+
return;
|
|
310
|
+
try {
|
|
311
|
+
await this.storage.setItem("known", Object.keys(this.states$.value));
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
// Ignore storage errors
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
async saveRelayState(relay, state) {
|
|
318
|
+
if (!this.storage)
|
|
319
|
+
return;
|
|
320
|
+
try {
|
|
321
|
+
await this.storage.setItem(relay, state);
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
// Ignore storage errors
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
package/dist/operators/index.js
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { MonoTypeOperatorFunction, Observable } from "rxjs";
|
|
2
|
+
type ILivenessTracker = {
|
|
3
|
+
unhealthy$: Observable<string[]>;
|
|
4
|
+
seen?: Set<string>;
|
|
5
|
+
};
|
|
6
|
+
/** Filters out unhealthy relays from an array of pointers */
|
|
7
|
+
export declare function ignoreUnhealthyRelaysOnPointers<T extends {
|
|
8
|
+
relays?: string[];
|
|
9
|
+
}, Tracker extends ILivenessTracker>(liveness: Tracker): MonoTypeOperatorFunction<T[]>;
|
|
10
|
+
/** Filters out unhealthy relays from the inboxes and outboxes */
|
|
11
|
+
export declare function ignoreUnhealthyMailboxes<T extends {
|
|
12
|
+
inboxes?: string[];
|
|
13
|
+
outboxes?: string[];
|
|
14
|
+
}, Tracker extends ILivenessTracker>(liveness: Tracker): MonoTypeOperatorFunction<T>;
|
|
15
|
+
/** Filters out unhealthy relays from an array of relays */
|
|
16
|
+
export declare function ignoreUnhealthyRelays<Tracker extends ILivenessTracker>(liveness: Tracker): MonoTypeOperatorFunction<string[]>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { combineLatestWith, map } from "rxjs";
|
|
2
|
+
function filterRelays(relays, unhealthy, liveness) {
|
|
3
|
+
return (relays &&
|
|
4
|
+
relays.filter((relay) => {
|
|
5
|
+
// Notify the liveness tracker that we've seen this relay
|
|
6
|
+
liveness.seen?.add(relay);
|
|
7
|
+
// Exclude unhealthy relays
|
|
8
|
+
return !unhealthy.includes(relay);
|
|
9
|
+
}));
|
|
10
|
+
}
|
|
11
|
+
/** Filters out unhealthy relays from an array of pointers */
|
|
12
|
+
export function ignoreUnhealthyRelaysOnPointers(liveness) {
|
|
13
|
+
return (source) => source.pipe(
|
|
14
|
+
// Combine with the liveness observable
|
|
15
|
+
combineLatestWith(liveness.unhealthy$),
|
|
16
|
+
// Filters out unhealthy relays from the pointers
|
|
17
|
+
map(([pointers, unhealthy]) => pointers.map((pointer) => {
|
|
18
|
+
if (!pointer.relays)
|
|
19
|
+
return pointer;
|
|
20
|
+
// Exclude unhealthy relays
|
|
21
|
+
return { ...pointer, relays: filterRelays(pointer.relays, unhealthy, liveness) };
|
|
22
|
+
})));
|
|
23
|
+
}
|
|
24
|
+
/** Filters out unhealthy relays from the inboxes and outboxes */
|
|
25
|
+
export function ignoreUnhealthyMailboxes(liveness) {
|
|
26
|
+
return (source) => source.pipe(
|
|
27
|
+
// Combine with the liveness observable
|
|
28
|
+
combineLatestWith(liveness.unhealthy$),
|
|
29
|
+
// Filters out unhealthy relays from the inboxes and outboxes
|
|
30
|
+
map(([mailboxes, unhealthy]) => {
|
|
31
|
+
if (!mailboxes.inboxes && !mailboxes.outboxes)
|
|
32
|
+
return mailboxes;
|
|
33
|
+
return {
|
|
34
|
+
...mailboxes,
|
|
35
|
+
inboxes: filterRelays(mailboxes.inboxes, unhealthy, liveness),
|
|
36
|
+
outboxes: filterRelays(mailboxes.outboxes, unhealthy, liveness),
|
|
37
|
+
};
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
/** Filters out unhealthy relays from an array of relays */
|
|
41
|
+
export function ignoreUnhealthyRelays(liveness) {
|
|
42
|
+
return (source) => source.pipe(
|
|
43
|
+
// Combine with the liveness observable
|
|
44
|
+
combineLatestWith(liveness.unhealthy$),
|
|
45
|
+
// Filters out unhealthy relays from the array
|
|
46
|
+
map(([relays, unhealthy]) => filterRelays(relays, unhealthy, liveness)));
|
|
47
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { OperatorFunction, ObservableInput, ObservedValueOf } from "rxjs";
|
|
2
|
+
/**
|
|
3
|
+
* Like switchMap, but subscribes to the new observable before unsubscribing from the old one.
|
|
4
|
+
* This prevents gaps in subscription coverage.
|
|
5
|
+
*
|
|
6
|
+
* @param project A function that, when applied to an item emitted by the source Observable,
|
|
7
|
+
* returns an Observable.
|
|
8
|
+
*/
|
|
9
|
+
export declare function reverseSwitchMap<T, O extends ObservableInput<any>>(project: (value: T, index: number) => O): OperatorFunction<T, ObservedValueOf<O>>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { from } from "rxjs";
|
|
2
|
+
import { createOperatorSubscriber } from "rxjs/internal/operators/OperatorSubscriber";
|
|
3
|
+
import { operate } from "rxjs/internal/util/lift";
|
|
4
|
+
/**
|
|
5
|
+
* Like switchMap, but subscribes to the new observable before unsubscribing from the old one.
|
|
6
|
+
* This prevents gaps in subscription coverage.
|
|
7
|
+
*
|
|
8
|
+
* @param project A function that, when applied to an item emitted by the source Observable,
|
|
9
|
+
* returns an Observable.
|
|
10
|
+
*/
|
|
11
|
+
export function reverseSwitchMap(project) {
|
|
12
|
+
return operate((source, subscriber) => {
|
|
13
|
+
let innerSubscriber = null;
|
|
14
|
+
let index = 0;
|
|
15
|
+
// Whether or not the source subscription has completed
|
|
16
|
+
let isComplete = false;
|
|
17
|
+
// We only complete the result if the source is complete AND we don't have an active inner subscription.
|
|
18
|
+
// This is called both when the source completes and when the inners complete.
|
|
19
|
+
const checkComplete = () => {
|
|
20
|
+
if (isComplete && !innerSubscriber)
|
|
21
|
+
subscriber.complete();
|
|
22
|
+
};
|
|
23
|
+
source.subscribe(createOperatorSubscriber(subscriber, (value) => {
|
|
24
|
+
const outerIndex = index++;
|
|
25
|
+
const oldSubscriber = innerSubscriber;
|
|
26
|
+
// Create the new inner subscription FIRST
|
|
27
|
+
// Immediately assign the new subscriber because observables can emit and complete synchronously
|
|
28
|
+
const self = (innerSubscriber = createOperatorSubscriber(subscriber, (innerValue) => subscriber.next(innerValue), () => {
|
|
29
|
+
// The inner has completed. Null out the inner subscriber to
|
|
30
|
+
// free up memory and to signal that we have no inner subscription
|
|
31
|
+
// currently. Only do this if this is still the active inner subscriber.
|
|
32
|
+
if (innerSubscriber === self || innerSubscriber === null) {
|
|
33
|
+
innerSubscriber = null;
|
|
34
|
+
checkComplete();
|
|
35
|
+
}
|
|
36
|
+
}));
|
|
37
|
+
// Subscribe to the new observable FIRST
|
|
38
|
+
from(project(value, outerIndex)).subscribe(innerSubscriber);
|
|
39
|
+
// THEN unsubscribe from the previous inner subscription
|
|
40
|
+
oldSubscriber?.unsubscribe();
|
|
41
|
+
}, () => {
|
|
42
|
+
isComplete = true;
|
|
43
|
+
checkComplete();
|
|
44
|
+
}));
|
|
45
|
+
});
|
|
46
|
+
}
|
package/dist/pool.d.ts
CHANGED
|
@@ -4,39 +4,38 @@ import { BehaviorSubject, Observable, Subject } from "rxjs";
|
|
|
4
4
|
import { RelayGroup } from "./group.js";
|
|
5
5
|
import type { NegentropySyncOptions, ReconcileFunction } from "./negentropy.js";
|
|
6
6
|
import { Relay, SyncDirection, type RelayOptions } from "./relay.js";
|
|
7
|
-
import type { FilterInput, IPool, IRelay, PublishResponse, SubscriptionResponse } from "./types.js";
|
|
7
|
+
import type { CountResponse, FilterInput, IPool, IPoolRelayInput, IRelay, PublishResponse, SubscriptionResponse } from "./types.js";
|
|
8
8
|
export declare class RelayPool implements IPool {
|
|
9
9
|
options?: RelayOptions | undefined;
|
|
10
|
-
groups$: BehaviorSubject<Map<string, RelayGroup>>;
|
|
11
|
-
get groups(): Map<string, RelayGroup>;
|
|
12
10
|
relays$: BehaviorSubject<Map<string, Relay>>;
|
|
13
11
|
get relays(): Map<string, Relay>;
|
|
14
12
|
/** A signal when a relay is added */
|
|
15
13
|
add$: Subject<IRelay>;
|
|
16
14
|
/** A signal when a relay is removed */
|
|
17
15
|
remove$: Subject<IRelay>;
|
|
18
|
-
/** An array of relays to never connect to */
|
|
19
|
-
blacklist: Set<string>;
|
|
20
16
|
constructor(options?: RelayOptions | undefined);
|
|
21
|
-
protected filterBlacklist(urls: string[]): string[];
|
|
22
17
|
/** Get or create a new relay connection */
|
|
23
18
|
relay(url: string): Relay;
|
|
24
19
|
/** Create a group of relays */
|
|
25
|
-
group(relays:
|
|
20
|
+
group(relays: IPoolRelayInput): RelayGroup;
|
|
26
21
|
/** Removes a relay from the pool and defaults to closing the connection */
|
|
27
22
|
remove(relay: string | IRelay, close?: boolean): void;
|
|
28
23
|
/** Make a REQ to multiple relays that does not deduplicate events */
|
|
29
|
-
req(relays:
|
|
24
|
+
req(relays: IPoolRelayInput, filters: FilterInput, id?: string): Observable<SubscriptionResponse>;
|
|
30
25
|
/** Send an EVENT message to multiple relays */
|
|
31
|
-
event(relays:
|
|
26
|
+
event(relays: IPoolRelayInput, event: NostrEvent): Observable<PublishResponse>;
|
|
32
27
|
/** Negentropy sync event ids with the relays and an event store */
|
|
33
|
-
negentropy(relays:
|
|
28
|
+
negentropy(relays: IPoolRelayInput, store: IEventStoreRead | IAsyncEventStoreRead | NostrEvent[], filter: Filter, reconcile: ReconcileFunction, opts?: NegentropySyncOptions): Promise<boolean>;
|
|
34
29
|
/** Publish an event to multiple relays */
|
|
35
|
-
publish(relays:
|
|
30
|
+
publish(relays: IPoolRelayInput, event: Parameters<RelayGroup["publish"]>[0], opts?: Parameters<RelayGroup["publish"]>[1]): Promise<PublishResponse[]>;
|
|
36
31
|
/** Request events from multiple relays */
|
|
37
|
-
request(relays:
|
|
32
|
+
request(relays: IPoolRelayInput, filters: FilterInput, opts?: Parameters<RelayGroup["request"]>[1]): Observable<NostrEvent>;
|
|
38
33
|
/** Open a subscription to multiple relays */
|
|
39
|
-
subscription(relays:
|
|
34
|
+
subscription(relays: IPoolRelayInput, filters: FilterInput, options?: Parameters<RelayGroup["subscription"]>[1]): Observable<SubscriptionResponse>;
|
|
35
|
+
/** Open a subscription for a map of relays and filters */
|
|
36
|
+
subscriptionMap(relays: Record<string, Filter | Filter[]> | Observable<Record<string, Filter | Filter[]>>, options?: Parameters<RelayGroup["subscription"]>[1]): Observable<SubscriptionResponse>;
|
|
37
|
+
/** Count events on multiple relays */
|
|
38
|
+
count(relays: IPoolRelayInput, filters: Filter | Filter[], id?: string): Observable<Record<string, CountResponse>>;
|
|
40
39
|
/** Negentropy sync events with the relays and an event store */
|
|
41
|
-
sync(relays:
|
|
40
|
+
sync(relays: IPoolRelayInput, store: IEventStoreRead | IAsyncEventStoreRead | NostrEvent[], filter: Filter, direction?: SyncDirection): Observable<NostrEvent>;
|
|
42
41
|
}
|
package/dist/pool.js
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
import { normalizeURL } from "applesauce-core/helpers";
|
|
2
|
-
import { BehaviorSubject, Subject } from "rxjs";
|
|
1
|
+
import { isFilterEqual, normalizeURL } from "applesauce-core/helpers";
|
|
2
|
+
import { BehaviorSubject, distinctUntilChanged, isObservable, map, of, Subject } from "rxjs";
|
|
3
3
|
import { RelayGroup } from "./group.js";
|
|
4
4
|
import { Relay } from "./relay.js";
|
|
5
5
|
export class RelayPool {
|
|
6
6
|
options;
|
|
7
|
-
groups$ = new BehaviorSubject(new Map());
|
|
8
|
-
get groups() {
|
|
9
|
-
return this.groups$.value;
|
|
10
|
-
}
|
|
11
7
|
relays$ = new BehaviorSubject(new Map());
|
|
12
8
|
get relays() {
|
|
13
9
|
return this.relays$.value;
|
|
@@ -16,34 +12,13 @@ export class RelayPool {
|
|
|
16
12
|
add$ = new Subject();
|
|
17
13
|
/** A signal when a relay is removed */
|
|
18
14
|
remove$ = new Subject();
|
|
19
|
-
/** An array of relays to never connect to */
|
|
20
|
-
blacklist = new Set();
|
|
21
15
|
constructor(options) {
|
|
22
16
|
this.options = options;
|
|
23
|
-
// Listen for relays being added and removed to emit connect / disconnect signals
|
|
24
|
-
// const listeners = new Map<IRelay, Subscription>();
|
|
25
|
-
// this.add$.subscribe((relay) =>
|
|
26
|
-
// listeners.set(
|
|
27
|
-
// relay,
|
|
28
|
-
// relay.connected$.subscribe((conn) => (conn ? this.connect$.next(relay) : this.disconnect$.next(relay))),
|
|
29
|
-
// ),
|
|
30
|
-
// );
|
|
31
|
-
// this.remove$.subscribe((relay) => {
|
|
32
|
-
// const listener = listeners.get(relay);
|
|
33
|
-
// if (listener) listener.unsubscribe();
|
|
34
|
-
// listeners.delete(relay);
|
|
35
|
-
// });
|
|
36
|
-
}
|
|
37
|
-
filterBlacklist(urls) {
|
|
38
|
-
return urls.filter((url) => !this.blacklist.has(url));
|
|
39
17
|
}
|
|
40
18
|
/** Get or create a new relay connection */
|
|
41
19
|
relay(url) {
|
|
42
20
|
// Normalize the url
|
|
43
21
|
url = normalizeURL(url);
|
|
44
|
-
// Check if the url is blacklisted
|
|
45
|
-
if (this.blacklist.has(url))
|
|
46
|
-
throw new Error("Relay is on blacklist");
|
|
47
22
|
// Check if the relay already exists
|
|
48
23
|
let relay = this.relays.get(url);
|
|
49
24
|
if (relay)
|
|
@@ -57,17 +32,9 @@ export class RelayPool {
|
|
|
57
32
|
}
|
|
58
33
|
/** Create a group of relays */
|
|
59
34
|
group(relays) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
relays = this.filterBlacklist(relays);
|
|
64
|
-
const key = relays.sort().join(",");
|
|
65
|
-
let group = this.groups.get(key);
|
|
66
|
-
if (group)
|
|
67
|
-
return group;
|
|
68
|
-
group = new RelayGroup(relays.map((url) => this.relay(url)));
|
|
69
|
-
this.groups$.next(this.groups.set(key, group));
|
|
70
|
-
return group;
|
|
35
|
+
return new RelayGroup(Array.isArray(relays)
|
|
36
|
+
? relays.map((url) => this.relay(url))
|
|
37
|
+
: relays.pipe(map((urls) => urls.map((url) => this.relay(url)))));
|
|
71
38
|
}
|
|
72
39
|
/** Removes a relay from the pool and defaults to closing the connection */
|
|
73
40
|
remove(relay, close = true) {
|
|
@@ -112,6 +79,25 @@ export class RelayPool {
|
|
|
112
79
|
subscription(relays, filters, options) {
|
|
113
80
|
return this.group(relays).subscription(filters, options);
|
|
114
81
|
}
|
|
82
|
+
/** Open a subscription for a map of relays and filters */
|
|
83
|
+
subscriptionMap(relays, options) {
|
|
84
|
+
// Convert input to observable
|
|
85
|
+
const relays$ = isObservable(relays) ? relays : of(relays);
|
|
86
|
+
return this.group(
|
|
87
|
+
// Create a group with an observable of dynamic relay urls
|
|
88
|
+
relays$.pipe(map((dir) => Object.keys(dir)))).subscription((relay) => {
|
|
89
|
+
// Return observable to subscribe to the relays unique filters
|
|
90
|
+
return relays$.pipe(
|
|
91
|
+
// Select the relays filters
|
|
92
|
+
map((dir) => dir[relay.url]),
|
|
93
|
+
// Don't send duplicate filters
|
|
94
|
+
distinctUntilChanged(isFilterEqual));
|
|
95
|
+
}, options);
|
|
96
|
+
}
|
|
97
|
+
/** Count events on multiple relays */
|
|
98
|
+
count(relays, filters, id) {
|
|
99
|
+
return this.group(relays).count(filters, id);
|
|
100
|
+
}
|
|
115
101
|
/** Negentropy sync events with the relays and an event store */
|
|
116
102
|
sync(relays, store, filter, direction) {
|
|
117
103
|
return this.group(relays).sync(store, filter, direction);
|
package/dist/relay.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { RelayInformation } from "nostr-tools/nip11";
|
|
|
4
4
|
import { BehaviorSubject, MonoTypeOperatorFunction, Observable, RepeatConfig, RetryConfig, Subject } from "rxjs";
|
|
5
5
|
import { WebSocketSubject, WebSocketSubjectConfig } from "rxjs/webSocket";
|
|
6
6
|
import { type NegentropySyncOptions, type ReconcileFunction } from "./negentropy.js";
|
|
7
|
-
import { AuthSigner, FilterInput, IRelay, PublishOptions, PublishResponse, RequestOptions, SubscriptionOptions, SubscriptionResponse } from "./types.js";
|
|
7
|
+
import { AuthSigner, CountResponse, FilterInput, IRelay, PublishOptions, PublishResponse, RequestOptions, SubscriptionOptions, SubscriptionResponse } from "./types.js";
|
|
8
8
|
/** Flags for the negentropy sync type */
|
|
9
9
|
export declare enum SyncDirection {
|
|
10
10
|
RECEIVE = 1,
|
|
@@ -104,6 +104,8 @@ export declare class Relay implements IRelay {
|
|
|
104
104
|
send(message: any): void;
|
|
105
105
|
/** Create a REQ observable that emits events or "EOSE" or errors */
|
|
106
106
|
req(filters: FilterInput, id?: string): Observable<SubscriptionResponse>;
|
|
107
|
+
/** Create a COUNT observable that emits a single count response */
|
|
108
|
+
count(filters: Filter | Filter[], id?: string): Observable<CountResponse>;
|
|
107
109
|
/** Send an EVENT or AUTH message and return an observable of PublishResponse that completes or errors */
|
|
108
110
|
event(event: NostrEvent, verb?: "EVENT" | "AUTH"): Observable<PublishResponse>;
|
|
109
111
|
/** send and AUTH message */
|
|
@@ -118,10 +120,12 @@ export declare class Relay implements IRelay {
|
|
|
118
120
|
protected customRepeatOperator<T extends unknown = unknown>(times: undefined | boolean | number | RepeatConfig | undefined): MonoTypeOperatorFunction<T>;
|
|
119
121
|
/** Internal operator for creating the timeout() operator */
|
|
120
122
|
protected customTimeoutOperator<T extends unknown = unknown>(timeout: undefined | boolean | number, defaultTimeout: number): MonoTypeOperatorFunction<T>;
|
|
123
|
+
/** Internal operator for handling auth-required errors from REQ/COUNT operations */
|
|
124
|
+
protected handleAuthRequiredForReq(operation: "REQ" | "COUNT"): MonoTypeOperatorFunction<any>;
|
|
121
125
|
/** Creates a REQ that retries when relay errors ( default 3 retries ) */
|
|
122
|
-
subscription(filters:
|
|
126
|
+
subscription(filters: FilterInput, opts?: SubscriptionOptions): Observable<SubscriptionResponse>;
|
|
123
127
|
/** Makes a single request that retires on errors and completes on EOSE */
|
|
124
|
-
request(filters:
|
|
128
|
+
request(filters: FilterInput, opts?: RequestOptions): Observable<NostrEvent>;
|
|
125
129
|
/** Publishes an event to the relay and retries when relay errors or responds with auth-required ( default 3 retries ) */
|
|
126
130
|
publish(event: NostrEvent, opts?: PublishOptions): Promise<PublishResponse>;
|
|
127
131
|
/** Negentropy sync events with the relay and an event store */
|