applesauce-relay 3.0.0 → 4.0.0
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 +30 -5
- package/dist/group.js +49 -8
- package/dist/lib/negentropy.d.ts +2 -2
- package/dist/negentropy.d.ts +22 -6
- package/dist/negentropy.js +10 -3
- package/dist/pool.d.ts +17 -7
- package/dist/pool.js +31 -4
- package/dist/relay.d.ts +28 -3
- package/dist/relay.js +104 -18
- package/dist/types.d.ts +54 -16
- package/package.json +9 -10
package/dist/group.d.ts
CHANGED
|
@@ -1,19 +1,44 @@
|
|
|
1
|
-
import { type NostrEvent } from "nostr-tools";
|
|
1
|
+
import { Filter, type NostrEvent } from "nostr-tools";
|
|
2
2
|
import { Observable } from "rxjs";
|
|
3
|
+
import { IAsyncEventStoreActions, IAsyncEventStoreRead, IEventStoreActions, IEventStoreRead } from "applesauce-core";
|
|
4
|
+
import { NegentropySyncOptions, type ReconcileFunction } from "./negentropy.js";
|
|
3
5
|
import { FilterInput, IGroup, IRelay, PublishOptions, PublishResponse, RequestOptions, SubscriptionOptions, SubscriptionResponse } from "./types.js";
|
|
6
|
+
import { SyncDirection } from "./relay.js";
|
|
7
|
+
/** Options for negentropy sync on a group of relays */
|
|
8
|
+
export type GroupNegentropySyncOptions = NegentropySyncOptions & {
|
|
9
|
+
/** Whether to sync in parallel (default true) */
|
|
10
|
+
parallel?: boolean;
|
|
11
|
+
};
|
|
12
|
+
/** Options for a subscription on a group of relays */
|
|
13
|
+
export type GroupSubscriptionOptions = SubscriptionOptions & {
|
|
14
|
+
/** Deduplicate events with an event store (default is a temporary instance of EventMemory), null will disable deduplication */
|
|
15
|
+
eventStore?: IEventStoreActions | IAsyncEventStoreActions | null;
|
|
16
|
+
};
|
|
17
|
+
/** Options for a request on a group of relays */
|
|
18
|
+
export type GroupRequestOptions = RequestOptions & {
|
|
19
|
+
/** Deduplicate events with an event store (default is a temporary instance of EventMemory), null will disable deduplication */
|
|
20
|
+
eventStore?: IEventStoreActions | IAsyncEventStoreActions | null;
|
|
21
|
+
};
|
|
4
22
|
export declare class RelayGroup implements IGroup {
|
|
5
23
|
relays: IRelay[];
|
|
6
24
|
constructor(relays: IRelay[]);
|
|
7
25
|
/** Takes an array of observables and only emits EOSE when all observables have emitted EOSE */
|
|
8
|
-
protected mergeEOSE(
|
|
9
|
-
/**
|
|
26
|
+
protected mergeEOSE(requests: Observable<SubscriptionResponse>[], eventStore?: IEventStoreActions | IAsyncEventStoreActions | null): Observable<import("nostr-tools").Event | "EOSE">;
|
|
27
|
+
/**
|
|
28
|
+
* Make a request to all relays
|
|
29
|
+
* @note This does not deduplicate events
|
|
30
|
+
*/
|
|
10
31
|
req(filters: FilterInput, id?: string): Observable<SubscriptionResponse>;
|
|
11
32
|
/** Send an event to all relays */
|
|
12
33
|
event(event: NostrEvent): Observable<PublishResponse>;
|
|
34
|
+
/** Negentropy sync events with the relays and an event store */
|
|
35
|
+
negentropy(store: IEventStoreRead | IAsyncEventStoreRead | NostrEvent[], filter: Filter, reconcile: ReconcileFunction, opts?: GroupNegentropySyncOptions): Promise<boolean>;
|
|
13
36
|
/** Publish an event to all relays with retries ( default 3 retries ) */
|
|
14
37
|
publish(event: NostrEvent, opts?: PublishOptions): Promise<PublishResponse[]>;
|
|
15
38
|
/** Request events from all relays with retries ( default 3 retries ) */
|
|
16
|
-
request(filters: FilterInput, opts?:
|
|
39
|
+
request(filters: FilterInput, opts?: GroupRequestOptions): Observable<NostrEvent>;
|
|
17
40
|
/** Open a subscription to all relays with retries ( default 3 retries ) */
|
|
18
|
-
subscription(filters: FilterInput, opts?:
|
|
41
|
+
subscription(filters: FilterInput, opts?: GroupSubscriptionOptions): Observable<SubscriptionResponse>;
|
|
42
|
+
/** Negentropy sync events with the relays and an event store */
|
|
43
|
+
sync(store: IEventStoreRead | IAsyncEventStoreRead | NostrEvent[], filter: Filter, direction?: SyncDirection): Observable<NostrEvent>;
|
|
19
44
|
}
|
package/dist/group.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { nanoid } from "nanoid";
|
|
2
|
-
import { catchError, EMPTY, endWith, ignoreElements, merge, of } from "rxjs";
|
|
2
|
+
import { catchError, defer, EMPTY, endWith, identity, ignoreElements, merge, of, share, switchMap, } from "rxjs";
|
|
3
|
+
import { EventMemory, filterDuplicateEvents, } from "applesauce-core";
|
|
3
4
|
import { completeOnEose } from "./operators/complete-on-eose.js";
|
|
4
5
|
import { onlyEvents } from "./operators/only-events.js";
|
|
5
6
|
export class RelayGroup {
|
|
@@ -8,9 +9,13 @@ export class RelayGroup {
|
|
|
8
9
|
this.relays = relays;
|
|
9
10
|
}
|
|
10
11
|
/** Takes an array of observables and only emits EOSE when all observables have emitted EOSE */
|
|
11
|
-
mergeEOSE(
|
|
12
|
+
mergeEOSE(requests, eventStore = new EventMemory()) {
|
|
12
13
|
// Create stream of events only
|
|
13
|
-
const events = merge(...requests).pipe(
|
|
14
|
+
const events = merge(...requests).pipe(
|
|
15
|
+
// Ignore non event responses
|
|
16
|
+
onlyEvents(),
|
|
17
|
+
// If an event store is provided, filter duplicate events
|
|
18
|
+
eventStore ? filterDuplicateEvents(eventStore) : identity);
|
|
14
19
|
// Create stream that emits EOSE when all relays have sent EOSE
|
|
15
20
|
const eose = merge(
|
|
16
21
|
// Create a new map of requests that only emits EOSE
|
|
@@ -19,13 +24,16 @@ export class RelayGroup {
|
|
|
19
24
|
endWith("EOSE"));
|
|
20
25
|
return merge(events, eose);
|
|
21
26
|
}
|
|
22
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Make a request to all relays
|
|
29
|
+
* @note This does not deduplicate events
|
|
30
|
+
*/
|
|
23
31
|
req(filters, id = nanoid(8)) {
|
|
24
32
|
const requests = this.relays.map((relay) => relay.req(filters, id).pipe(
|
|
25
33
|
// Ignore connection errors
|
|
26
34
|
catchError(() => of("EOSE"))));
|
|
27
35
|
// Merge events and the single EOSE stream
|
|
28
|
-
return this.mergeEOSE(
|
|
36
|
+
return this.mergeEOSE(requests);
|
|
29
37
|
}
|
|
30
38
|
/** Send an event to all relays */
|
|
31
39
|
event(event) {
|
|
@@ -33,6 +41,20 @@ export class RelayGroup {
|
|
|
33
41
|
// Catch error and return as PublishResponse
|
|
34
42
|
catchError((err) => of({ ok: false, from: relay.url, message: err?.message || "Unknown error" })))));
|
|
35
43
|
}
|
|
44
|
+
/** Negentropy sync events with the relays and an event store */
|
|
45
|
+
async negentropy(store, filter, reconcile, opts) {
|
|
46
|
+
// Filter out relays that do not support NIP-77 negentropy sync
|
|
47
|
+
const supported = await Promise.all(this.relays.map(async (relay) => [relay, await relay.getSupported()]));
|
|
48
|
+
const relays = supported.filter(([_, supported]) => supported?.includes(77)).map(([relay]) => relay);
|
|
49
|
+
if (relays.length === 0)
|
|
50
|
+
throw new Error("No relays support NIP-77 negentropy sync");
|
|
51
|
+
// Non parallel sync is not supported yet
|
|
52
|
+
if (!opts?.parallel)
|
|
53
|
+
throw new Error("Negentropy sync must be parallel (for now)");
|
|
54
|
+
// Sync all the relays in parallel
|
|
55
|
+
await Promise.allSettled(relays.map((relay) => relay.negentropy(store, filter, reconcile, opts)));
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
36
58
|
/** Publish an event to all relays with retries ( default 3 retries ) */
|
|
37
59
|
publish(event, opts) {
|
|
38
60
|
return Promise.all(this.relays.map((relay) => relay.publish(event, opts).catch(
|
|
@@ -43,12 +65,31 @@ export class RelayGroup {
|
|
|
43
65
|
request(filters, opts) {
|
|
44
66
|
return merge(...this.relays.map((relay) => relay.request(filters, opts).pipe(
|
|
45
67
|
// Ignore individual connection errors
|
|
46
|
-
catchError(() => EMPTY))))
|
|
68
|
+
catchError(() => EMPTY)))).pipe(
|
|
69
|
+
// If an event store is provided, filter duplicate events
|
|
70
|
+
opts?.eventStore == null ? identity : filterDuplicateEvents(opts?.eventStore ?? new EventMemory()));
|
|
47
71
|
}
|
|
48
72
|
/** Open a subscription to all relays with retries ( default 3 retries ) */
|
|
49
73
|
subscription(filters, opts) {
|
|
50
|
-
return this.mergeEOSE(
|
|
74
|
+
return this.mergeEOSE(this.relays.map((relay) => relay.subscription(filters, opts).pipe(
|
|
51
75
|
// Ignore individual connection errors
|
|
52
|
-
catchError(() => EMPTY)))
|
|
76
|
+
catchError(() => EMPTY))),
|
|
77
|
+
// Pass event store so that duplicate events are removed
|
|
78
|
+
opts?.eventStore);
|
|
79
|
+
}
|
|
80
|
+
/** Negentropy sync events with the relays and an event store */
|
|
81
|
+
sync(store, filter, direction) {
|
|
82
|
+
// Get an array of relays that support NIP-77 negentropy sync
|
|
83
|
+
return defer(async () => {
|
|
84
|
+
const supported = await Promise.all(this.relays.map(async (relay) => [relay, await relay.getSupported()]));
|
|
85
|
+
const relays = supported.filter(([_, supported]) => supported?.includes(77)).map(([relay]) => relay);
|
|
86
|
+
if (relays.length === 0)
|
|
87
|
+
throw new Error("No relays support NIP-77 negentropy sync");
|
|
88
|
+
return relays;
|
|
89
|
+
}).pipe(
|
|
90
|
+
// Once relays are selected, sync all the relays in parallel
|
|
91
|
+
switchMap((relays) => merge(...relays.map((relay) => relay.sync(store, filter, direction)))),
|
|
92
|
+
// Only create one upstream subscription
|
|
93
|
+
share());
|
|
53
94
|
}
|
|
54
95
|
}
|
package/dist/lib/negentropy.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ declare class Negentropy {
|
|
|
38
38
|
constructor(storage: NegentropyStorageVector, frameSizeLimit?: number);
|
|
39
39
|
_bound(timestamp: number, id?: Uint8Array): {
|
|
40
40
|
timestamp: number;
|
|
41
|
-
id: Uint8Array<
|
|
41
|
+
id: Uint8Array<ArrayBufferLike>;
|
|
42
42
|
};
|
|
43
43
|
initiate<T extends Uint8Array | string>(): Promise<T>;
|
|
44
44
|
setInitiator(): void;
|
|
@@ -55,7 +55,7 @@ declare class Negentropy {
|
|
|
55
55
|
encodeBound(key: Item): WrappedBuffer;
|
|
56
56
|
getMinimalBound(prev: Item, curr: Item): {
|
|
57
57
|
timestamp: number;
|
|
58
|
-
id: Uint8Array<
|
|
58
|
+
id: Uint8Array<ArrayBufferLike>;
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
61
|
export { Negentropy, NegentropyStorageVector };
|
package/dist/negentropy.d.ts
CHANGED
|
@@ -1,15 +1,31 @@
|
|
|
1
|
-
import { IEventStoreRead } from "applesauce-core";
|
|
1
|
+
import { IAsyncEventStoreRead, IEventStoreRead } from "applesauce-core";
|
|
2
2
|
import { Filter } from "nostr-tools";
|
|
3
3
|
import { MultiplexWebSocket } from "./types.js";
|
|
4
4
|
import { NegentropyStorageVector } from "./lib/negentropy.js";
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* A function that reconciles the storage vectors with a remote relay
|
|
7
|
+
* @param have - The ids that the local storage has
|
|
8
|
+
* @param need - The ids that the remote relay has
|
|
9
|
+
* @returns A promise that resolves when the reconciliation is complete
|
|
10
|
+
*/
|
|
11
|
+
export type ReconcileFunction = (have: string[], need: string[]) => Promise<void>;
|
|
12
|
+
/** Options for the negentropy sync */
|
|
13
|
+
export type NegentropySyncOptions = {
|
|
14
|
+
frameSizeLimit?: number;
|
|
15
|
+
signal?: AbortSignal;
|
|
16
|
+
};
|
|
17
|
+
/** Creates a NegentropyStorageVector from an event store and filter */
|
|
18
|
+
export declare function buildStorageFromFilter(store: IEventStoreRead | IAsyncEventStoreRead, filter: Filter): Promise<NegentropyStorageVector>;
|
|
19
|
+
/** Creates a NegentropyStorageVector from an array of items */
|
|
6
20
|
export declare function buildStorageVector(items: {
|
|
7
21
|
id: string;
|
|
8
22
|
created_at: number;
|
|
9
23
|
}[]): NegentropyStorageVector;
|
|
24
|
+
/**
|
|
25
|
+
* Sync the storage vectors with a remote relay
|
|
26
|
+
* @throws {Error} if the sync fails
|
|
27
|
+
* @returns true if the sync was successful, false if the sync was aborted
|
|
28
|
+
*/
|
|
10
29
|
export declare function negentropySync(storage: NegentropyStorageVector, socket: MultiplexWebSocket & {
|
|
11
30
|
next: (msg: any) => void;
|
|
12
|
-
}, filter: Filter, reconcile:
|
|
13
|
-
frameSizeLimit?: number;
|
|
14
|
-
signal?: AbortSignal;
|
|
15
|
-
}): Promise<boolean>;
|
|
31
|
+
}, filter: Filter, reconcile: ReconcileFunction, opts?: NegentropySyncOptions): Promise<boolean>;
|
package/dist/negentropy.js
CHANGED
|
@@ -3,13 +3,15 @@ import { map, share, firstValueFrom } from "rxjs";
|
|
|
3
3
|
import { nanoid } from "nanoid";
|
|
4
4
|
import { Negentropy, NegentropyStorageVector } from "./lib/negentropy.js";
|
|
5
5
|
const log = logger.extend("negentropy");
|
|
6
|
-
|
|
6
|
+
/** Creates a NegentropyStorageVector from an event store and filter */
|
|
7
|
+
export async function buildStorageFromFilter(store, filter) {
|
|
7
8
|
const storage = new NegentropyStorageVector();
|
|
8
|
-
for (const event of store.getByFilters(filter))
|
|
9
|
+
for (const event of await store.getByFilters(filter))
|
|
9
10
|
storage.insert(event.created_at, event.id);
|
|
10
11
|
storage.seal();
|
|
11
12
|
return storage;
|
|
12
13
|
}
|
|
14
|
+
/** Creates a NegentropyStorageVector from an array of items */
|
|
13
15
|
export function buildStorageVector(items) {
|
|
14
16
|
const storage = new NegentropyStorageVector();
|
|
15
17
|
for (const item of items)
|
|
@@ -17,6 +19,11 @@ export function buildStorageVector(items) {
|
|
|
17
19
|
storage.seal();
|
|
18
20
|
return storage;
|
|
19
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Sync the storage vectors with a remote relay
|
|
24
|
+
* @throws {Error} if the sync fails
|
|
25
|
+
* @returns true if the sync was successful, false if the sync was aborted
|
|
26
|
+
*/
|
|
20
27
|
export async function negentropySync(storage, socket, filter, reconcile, opts) {
|
|
21
28
|
let id = nanoid();
|
|
22
29
|
let ne = new Negentropy(storage, opts?.frameSizeLimit);
|
|
@@ -46,7 +53,7 @@ export async function negentropySync(storage, socket, filter, reconcile, opts) {
|
|
|
46
53
|
return msg[2];
|
|
47
54
|
}), share());
|
|
48
55
|
// keep an additional subscription open while waiting for async operations
|
|
49
|
-
const sub = incoming.subscribe((m) =>
|
|
56
|
+
const sub = incoming.subscribe((m) => log(m));
|
|
50
57
|
try {
|
|
51
58
|
while (msg && opts?.signal?.aborted !== true) {
|
|
52
59
|
const received = await firstValueFrom(incoming);
|
package/dist/pool.d.ts
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import type { IAsyncEventStoreRead, IEventStoreRead } from "applesauce-core";
|
|
2
|
+
import { Filter, type NostrEvent } from "nostr-tools";
|
|
3
|
+
import { BehaviorSubject, Observable, Subject } from "rxjs";
|
|
3
4
|
import { RelayGroup } from "./group.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import type { NegentropySyncOptions, ReconcileFunction } from "./negentropy.js";
|
|
6
|
+
import { Relay, SyncDirection, type RelayOptions } from "./relay.js";
|
|
7
|
+
import type { FilterInput, IPool, IRelay, PublishResponse, SubscriptionResponse } from "./types.js";
|
|
6
8
|
export declare class RelayPool implements IPool {
|
|
7
9
|
options?: RelayOptions | undefined;
|
|
8
10
|
groups$: BehaviorSubject<Map<string, RelayGroup>>;
|
|
9
11
|
get groups(): Map<string, RelayGroup>;
|
|
10
12
|
relays$: BehaviorSubject<Map<string, Relay>>;
|
|
11
13
|
get relays(): Map<string, Relay>;
|
|
14
|
+
/** A signal when a relay is added */
|
|
15
|
+
add$: Subject<IRelay>;
|
|
16
|
+
/** A signal when a relay is removed */
|
|
17
|
+
remove$: Subject<IRelay>;
|
|
12
18
|
/** An array of relays to never connect to */
|
|
13
19
|
blacklist: Set<string>;
|
|
14
20
|
constructor(options?: RelayOptions | undefined);
|
|
@@ -23,10 +29,14 @@ export declare class RelayPool implements IPool {
|
|
|
23
29
|
req(relays: string[], filters: FilterInput, id?: string): Observable<SubscriptionResponse>;
|
|
24
30
|
/** Send an EVENT message to multiple relays */
|
|
25
31
|
event(relays: string[], event: NostrEvent): Observable<PublishResponse>;
|
|
32
|
+
/** Negentropy sync event ids with the relays and an event store */
|
|
33
|
+
negentropy(relays: string[], store: IEventStoreRead | IAsyncEventStoreRead | NostrEvent[], filter: Filter, reconcile: ReconcileFunction, opts?: NegentropySyncOptions): Promise<boolean>;
|
|
26
34
|
/** Publish an event to multiple relays */
|
|
27
|
-
publish(relays: string[], event:
|
|
35
|
+
publish(relays: string[], event: Parameters<RelayGroup["publish"]>[0], opts?: Parameters<RelayGroup["publish"]>[1]): Promise<PublishResponse[]>;
|
|
28
36
|
/** Request events from multiple relays */
|
|
29
|
-
request(relays: string[], filters:
|
|
37
|
+
request(relays: string[], filters: Parameters<RelayGroup["request"]>[0], opts?: Parameters<RelayGroup["request"]>[1]): Observable<NostrEvent>;
|
|
30
38
|
/** Open a subscription to multiple relays */
|
|
31
|
-
subscription(relays: string[], filters:
|
|
39
|
+
subscription(relays: string[], filters: Parameters<RelayGroup["subscription"]>[0], options?: Parameters<RelayGroup["subscription"]>[1]): Observable<SubscriptionResponse>;
|
|
40
|
+
/** Negentropy sync events with the relays and an event store */
|
|
41
|
+
sync(relays: string[], store: IEventStoreRead | IAsyncEventStoreRead | NostrEvent[], filter: Filter, direction?: SyncDirection): Observable<NostrEvent>;
|
|
32
42
|
}
|
package/dist/pool.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { normalizeURL } from "applesauce-core/helpers";
|
|
2
|
+
import { BehaviorSubject, Subject } from "rxjs";
|
|
2
3
|
import { RelayGroup } from "./group.js";
|
|
3
4
|
import { Relay } from "./relay.js";
|
|
4
|
-
import { normalizeURL } from "applesauce-core/helpers";
|
|
5
5
|
export class RelayPool {
|
|
6
6
|
options;
|
|
7
7
|
groups$ = new BehaviorSubject(new Map());
|
|
@@ -12,10 +12,27 @@ export class RelayPool {
|
|
|
12
12
|
get relays() {
|
|
13
13
|
return this.relays$.value;
|
|
14
14
|
}
|
|
15
|
+
/** A signal when a relay is added */
|
|
16
|
+
add$ = new Subject();
|
|
17
|
+
/** A signal when a relay is removed */
|
|
18
|
+
remove$ = new Subject();
|
|
15
19
|
/** An array of relays to never connect to */
|
|
16
20
|
blacklist = new Set();
|
|
17
21
|
constructor(options) {
|
|
18
22
|
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
|
+
// });
|
|
19
36
|
}
|
|
20
37
|
filterBlacklist(urls) {
|
|
21
38
|
return urls.filter((url) => !this.blacklist.has(url));
|
|
@@ -35,6 +52,7 @@ export class RelayPool {
|
|
|
35
52
|
relay = new Relay(url, this.options);
|
|
36
53
|
this.relays.set(url, relay);
|
|
37
54
|
this.relays$.next(this.relays);
|
|
55
|
+
this.add$.next(relay);
|
|
38
56
|
return relay;
|
|
39
57
|
}
|
|
40
58
|
/** Create a group of relays */
|
|
@@ -68,6 +86,7 @@ export class RelayPool {
|
|
|
68
86
|
instance?.close();
|
|
69
87
|
this.relays.delete(instance.url);
|
|
70
88
|
this.relays$.next(this.relays);
|
|
89
|
+
this.remove$.next(instance);
|
|
71
90
|
}
|
|
72
91
|
/** Make a REQ to multiple relays that does not deduplicate events */
|
|
73
92
|
req(relays, filters, id) {
|
|
@@ -77,6 +96,10 @@ export class RelayPool {
|
|
|
77
96
|
event(relays, event) {
|
|
78
97
|
return this.group(relays).event(event);
|
|
79
98
|
}
|
|
99
|
+
/** Negentropy sync event ids with the relays and an event store */
|
|
100
|
+
negentropy(relays, store, filter, reconcile, opts) {
|
|
101
|
+
return this.group(relays).negentropy(store, filter, reconcile, opts);
|
|
102
|
+
}
|
|
80
103
|
/** Publish an event to multiple relays */
|
|
81
104
|
publish(relays, event, opts) {
|
|
82
105
|
return this.group(relays).publish(event, opts);
|
|
@@ -86,7 +109,11 @@ export class RelayPool {
|
|
|
86
109
|
return this.group(relays).request(filters, opts);
|
|
87
110
|
}
|
|
88
111
|
/** Open a subscription to multiple relays */
|
|
89
|
-
subscription(relays, filters,
|
|
90
|
-
return this.group(relays).subscription(filters,
|
|
112
|
+
subscription(relays, filters, options) {
|
|
113
|
+
return this.group(relays).subscription(filters, options);
|
|
114
|
+
}
|
|
115
|
+
/** Negentropy sync events with the relays and an event store */
|
|
116
|
+
sync(relays, store, filter, direction) {
|
|
117
|
+
return this.group(relays).sync(store, filter, direction);
|
|
91
118
|
}
|
|
92
119
|
}
|
package/dist/relay.d.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import { logger } from "applesauce-core";
|
|
1
|
+
import { IAsyncEventStoreRead, IEventStoreRead, logger } from "applesauce-core";
|
|
2
2
|
import { type Filter, type NostrEvent } from "nostr-tools";
|
|
3
3
|
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
|
+
import { type NegentropySyncOptions, type ReconcileFunction } from "./negentropy.js";
|
|
6
7
|
import { AuthSigner, FilterInput, IRelay, PublishOptions, PublishResponse, RequestOptions, SubscriptionOptions, SubscriptionResponse } from "./types.js";
|
|
8
|
+
/** Flags for the negentropy sync type */
|
|
9
|
+
export declare enum SyncDirection {
|
|
10
|
+
RECEIVE = 1,
|
|
11
|
+
SEND = 2,
|
|
12
|
+
BOTH = 3
|
|
13
|
+
}
|
|
7
14
|
/** An error that is thrown when a REQ is closed from the relay side */
|
|
8
15
|
export declare class ReqCloseError extends Error {
|
|
9
16
|
}
|
|
@@ -14,6 +21,8 @@ export type RelayOptions = {
|
|
|
14
21
|
eoseTimeout?: number;
|
|
15
22
|
/** How long to wait for an OK message from the relay (default 10s) */
|
|
16
23
|
eventTimeout?: number;
|
|
24
|
+
/** How long to wait for a publish to complete (default 30s) */
|
|
25
|
+
publishTimeout?: number;
|
|
17
26
|
/** How long to keep the connection alive after nothing is subscribed (default 30s) */
|
|
18
27
|
keepAlive?: number;
|
|
19
28
|
};
|
|
@@ -54,6 +63,8 @@ export declare class Relay implements IRelay {
|
|
|
54
63
|
protected _nip11: RelayInformation | null;
|
|
55
64
|
/** An observable that emits the limitations for the relay */
|
|
56
65
|
limitations$: Observable<RelayInformation["limitation"] | null>;
|
|
66
|
+
/** An array of supported NIPs from the NIP-11 information document */
|
|
67
|
+
supported$: Observable<number[] | null>;
|
|
57
68
|
/** An observable that emits when underlying websocket is opened */
|
|
58
69
|
open$: Subject<Event>;
|
|
59
70
|
/** An observable that emits when underlying websocket is closed */
|
|
@@ -70,6 +81,8 @@ export declare class Relay implements IRelay {
|
|
|
70
81
|
eoseTimeout: number;
|
|
71
82
|
/** How long to wait for an OK message from the relay (default 10s) */
|
|
72
83
|
eventTimeout: number;
|
|
84
|
+
/** How long to wait for a publish to complete (default 30s) */
|
|
85
|
+
publishTimeout: number;
|
|
73
86
|
/** How long to keep the connection alive after nothing is subscribed (default 30s) */
|
|
74
87
|
keepAlive: number;
|
|
75
88
|
protected receivedAuthRequiredForReq: BehaviorSubject<boolean>;
|
|
@@ -95,20 +108,32 @@ export declare class Relay implements IRelay {
|
|
|
95
108
|
event(event: NostrEvent, verb?: "EVENT" | "AUTH"): Observable<PublishResponse>;
|
|
96
109
|
/** send and AUTH message */
|
|
97
110
|
auth(event: NostrEvent): Promise<PublishResponse>;
|
|
111
|
+
/** Negentropy sync event ids with the relay and an event store */
|
|
112
|
+
negentropy(store: IEventStoreRead | IAsyncEventStoreRead | NostrEvent[], filter: Filter, reconcile: ReconcileFunction, opts?: NegentropySyncOptions): Promise<boolean>;
|
|
98
113
|
/** Authenticate with the relay using a signer */
|
|
99
114
|
authenticate(signer: AuthSigner): Promise<PublishResponse>;
|
|
100
115
|
/** Internal operator for creating the retry() operator */
|
|
101
|
-
protected customRetryOperator<T extends unknown = unknown>(times: number | RetryConfig): MonoTypeOperatorFunction<T>;
|
|
116
|
+
protected customRetryOperator<T extends unknown = unknown>(times: undefined | boolean | number | RetryConfig, base?: RetryConfig): MonoTypeOperatorFunction<T>;
|
|
102
117
|
/** Internal operator for creating the repeat() operator */
|
|
103
|
-
protected customRepeatOperator<T extends unknown = unknown>(times: boolean | number | RepeatConfig | undefined): MonoTypeOperatorFunction<T>;
|
|
118
|
+
protected customRepeatOperator<T extends unknown = unknown>(times: undefined | boolean | number | RepeatConfig | undefined): MonoTypeOperatorFunction<T>;
|
|
119
|
+
/** Internal operator for creating the timeout() operator */
|
|
120
|
+
protected customTimeoutOperator<T extends unknown = unknown>(timeout: undefined | boolean | number, defaultTimeout: number): MonoTypeOperatorFunction<T>;
|
|
104
121
|
/** Creates a REQ that retries when relay errors ( default 3 retries ) */
|
|
105
122
|
subscription(filters: Filter | Filter[], opts?: SubscriptionOptions): Observable<SubscriptionResponse>;
|
|
106
123
|
/** Makes a single request that retires on errors and completes on EOSE */
|
|
107
124
|
request(filters: Filter | Filter[], opts?: RequestOptions): Observable<NostrEvent>;
|
|
108
125
|
/** Publishes an event to the relay and retries when relay errors or responds with auth-required ( default 3 retries ) */
|
|
109
126
|
publish(event: NostrEvent, opts?: PublishOptions): Promise<PublishResponse>;
|
|
127
|
+
/** Negentropy sync events with the relay and an event store */
|
|
128
|
+
sync(store: IEventStoreRead | IAsyncEventStoreRead | NostrEvent[], filter: Filter, direction?: SyncDirection): Observable<NostrEvent>;
|
|
110
129
|
/** Force close the connection */
|
|
111
130
|
close(): void;
|
|
131
|
+
/** An async method that returns the NIP-11 information document for the relay */
|
|
132
|
+
getInformation(): Promise<RelayInformation | null>;
|
|
133
|
+
/** An async method that returns the NIP-11 limitations for the relay */
|
|
134
|
+
getLimitations(): Promise<RelayInformation["limitation"] | null>;
|
|
135
|
+
/** An async method that returns the supported NIPs for the relay */
|
|
136
|
+
getSupported(): Promise<number[] | null>;
|
|
112
137
|
/** Static method to fetch the NIP-11 information document for a relay */
|
|
113
138
|
static fetchInformationDocument(url: string): Observable<RelayInformation | null>;
|
|
114
139
|
/** Static method to create a reconnection method for each relay */
|
package/dist/relay.js
CHANGED
|
@@ -3,10 +3,18 @@ import { ensureHttpURL } from "applesauce-core/helpers";
|
|
|
3
3
|
import { simpleTimeout } from "applesauce-core/observable";
|
|
4
4
|
import { nanoid } from "nanoid";
|
|
5
5
|
import { nip42 } from "nostr-tools";
|
|
6
|
-
import { BehaviorSubject, catchError, combineLatest, defer, endWith, filter, finalize, from, identity, ignoreElements, isObservable, lastValueFrom, map, merge, mergeMap, mergeWith, NEVER, of, repeat, retry, scan, share, shareReplay, Subject, switchMap, take, takeUntil, tap, throwError, timeout, timer, } from "rxjs";
|
|
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
7
|
import { webSocket } from "rxjs/webSocket";
|
|
8
8
|
import { completeOnEose } from "./operators/complete-on-eose.js";
|
|
9
9
|
import { markFromRelay } from "./operators/mark-from-relay.js";
|
|
10
|
+
const DEFAULT_RETRY_CONFIG = { count: 10, delay: 1000, resetOnSuccess: true };
|
|
11
|
+
/** Flags for the negentropy sync type */
|
|
12
|
+
export var SyncDirection;
|
|
13
|
+
(function (SyncDirection) {
|
|
14
|
+
SyncDirection[SyncDirection["RECEIVE"] = 1] = "RECEIVE";
|
|
15
|
+
SyncDirection[SyncDirection["SEND"] = 2] = "SEND";
|
|
16
|
+
SyncDirection[SyncDirection["BOTH"] = 3] = "BOTH";
|
|
17
|
+
})(SyncDirection || (SyncDirection = {}));
|
|
10
18
|
/** An error that is thrown when a REQ is closed from the relay side */
|
|
11
19
|
export class ReqCloseError extends Error {
|
|
12
20
|
}
|
|
@@ -47,6 +55,8 @@ export class Relay {
|
|
|
47
55
|
_nip11 = null;
|
|
48
56
|
/** An observable that emits the limitations for the relay */
|
|
49
57
|
limitations$;
|
|
58
|
+
/** An array of supported NIPs from the NIP-11 information document */
|
|
59
|
+
supported$;
|
|
50
60
|
/** An observable that emits when underlying websocket is opened */
|
|
51
61
|
open$ = new Subject();
|
|
52
62
|
/** An observable that emits when underlying websocket is closed */
|
|
@@ -76,6 +86,8 @@ export class Relay {
|
|
|
76
86
|
eoseTimeout = 10_000;
|
|
77
87
|
/** How long to wait for an OK message from the relay (default 10s) */
|
|
78
88
|
eventTimeout = 10_000;
|
|
89
|
+
/** How long to wait for a publish to complete (default 30s) */
|
|
90
|
+
publishTimeout = 30_000;
|
|
79
91
|
/** How long to keep the connection alive after nothing is subscribed (default 30s) */
|
|
80
92
|
keepAlive = 30_000;
|
|
81
93
|
// Subjects that track if an "auth-required" message has been received for REQ or EVENT
|
|
@@ -107,6 +119,8 @@ export class Relay {
|
|
|
107
119
|
this.eoseTimeout = opts.eoseTimeout;
|
|
108
120
|
if (opts?.eventTimeout !== undefined)
|
|
109
121
|
this.eventTimeout = opts.eventTimeout;
|
|
122
|
+
if (opts?.publishTimeout !== undefined)
|
|
123
|
+
this.publishTimeout = opts.publishTimeout;
|
|
110
124
|
if (opts?.keepAlive !== undefined)
|
|
111
125
|
this.keepAlive = opts.keepAlive;
|
|
112
126
|
// Create an observable that tracks boolean authentication state
|
|
@@ -144,11 +158,12 @@ export class Relay {
|
|
|
144
158
|
}).pipe(
|
|
145
159
|
// if the fetch fails, return null
|
|
146
160
|
catchError(() => of(null)),
|
|
147
|
-
// cache the result
|
|
148
|
-
shareReplay(1),
|
|
149
161
|
// update the internal state
|
|
150
|
-
tap((info) => (this._nip11 = info))
|
|
151
|
-
|
|
162
|
+
tap((info) => (this._nip11 = info)),
|
|
163
|
+
// cache the result
|
|
164
|
+
shareReplay(1));
|
|
165
|
+
this.limitations$ = this.information$.pipe(map((info) => (info ? info.limitation : null)));
|
|
166
|
+
this.supported$ = this.information$.pipe(map((info) => info && Array.isArray(info.supported_nips) ? info.supported_nips.filter((n) => typeof n === "number") : null));
|
|
152
167
|
// Create observables that track if auth is required for REQ or EVENT
|
|
153
168
|
this.authRequiredForRead$ = this.receivedAuthRequiredForReq.pipe(tap((required) => required && this.log("Auth required for REQ")), shareReplay(1));
|
|
154
169
|
this.authRequiredForPublish$ = this.receivedAuthRequiredForEvent.pipe(tap((required) => required && this.log("Auth required for EVENT")), shareReplay(1));
|
|
@@ -347,6 +362,16 @@ export class Relay {
|
|
|
347
362
|
// update authenticated
|
|
348
363
|
tap((result) => this.authenticationResponse$.next(result))));
|
|
349
364
|
}
|
|
365
|
+
/** Negentropy sync event ids with the relay and an event store */
|
|
366
|
+
async negentropy(store, filter, reconcile, opts) {
|
|
367
|
+
// Check relay supports NIP-77 sync
|
|
368
|
+
if ((await this.getSupported())?.includes(77) === false)
|
|
369
|
+
throw new Error("Relay does not support NIP-77");
|
|
370
|
+
// Import negentropy functions dynamically
|
|
371
|
+
const { buildStorageVector, buildStorageFromFilter, negentropySync } = await import("./negentropy.js");
|
|
372
|
+
const storage = Array.isArray(store) ? buildStorageVector(store) : await buildStorageFromFilter(store, filter);
|
|
373
|
+
return negentropySync(storage, this.socket, filter, reconcile, opts);
|
|
374
|
+
}
|
|
350
375
|
/** Authenticate with the relay using a signer */
|
|
351
376
|
authenticate(signer) {
|
|
352
377
|
if (!this.challenge)
|
|
@@ -356,11 +381,15 @@ export class Relay {
|
|
|
356
381
|
return lastValueFrom(start.pipe(switchMap((event) => this.auth(event))));
|
|
357
382
|
}
|
|
358
383
|
/** Internal operator for creating the retry() operator */
|
|
359
|
-
customRetryOperator(times) {
|
|
360
|
-
if (
|
|
361
|
-
return
|
|
384
|
+
customRetryOperator(times, base) {
|
|
385
|
+
if (times === false)
|
|
386
|
+
return identity;
|
|
387
|
+
else if (typeof times === "number")
|
|
388
|
+
return retry({ ...base, count: times });
|
|
389
|
+
else if (times === true)
|
|
390
|
+
return base ? retry(base) : retry();
|
|
362
391
|
else
|
|
363
|
-
return retry(times);
|
|
392
|
+
return retry({ ...base, ...times });
|
|
364
393
|
}
|
|
365
394
|
/** Internal operator for creating the repeat() operator */
|
|
366
395
|
customRepeatOperator(times) {
|
|
@@ -373,13 +402,25 @@ export class Relay {
|
|
|
373
402
|
else
|
|
374
403
|
return repeat(times);
|
|
375
404
|
}
|
|
405
|
+
/** Internal operator for creating the timeout() operator */
|
|
406
|
+
customTimeoutOperator(timeout, defaultTimeout) {
|
|
407
|
+
// Do nothing if disabled
|
|
408
|
+
if (timeout === false)
|
|
409
|
+
return identity;
|
|
410
|
+
// If true default to 30 seconds
|
|
411
|
+
else if (timeout === true)
|
|
412
|
+
return simpleTimeout(defaultTimeout);
|
|
413
|
+
// Otherwise use the timeout value or default to 30 seconds
|
|
414
|
+
else
|
|
415
|
+
return simpleTimeout(timeout ?? defaultTimeout);
|
|
416
|
+
}
|
|
376
417
|
/** Creates a REQ that retries when relay errors ( default 3 retries ) */
|
|
377
418
|
subscription(filters, opts) {
|
|
378
419
|
return this.req(filters, opts?.id).pipe(
|
|
379
420
|
// Retry on connection errors
|
|
380
|
-
this.customRetryOperator(opts?.retries ??
|
|
381
|
-
// Create
|
|
382
|
-
this.customRepeatOperator(opts?.
|
|
421
|
+
this.customRetryOperator(opts?.retries ?? opts?.reconnect ?? true, DEFAULT_RETRY_CONFIG),
|
|
422
|
+
// Create resubscribe logic (repeat operator)
|
|
423
|
+
this.customRepeatOperator(opts?.resubscribe),
|
|
383
424
|
// Single subscription
|
|
384
425
|
share());
|
|
385
426
|
}
|
|
@@ -387,9 +428,9 @@ export class Relay {
|
|
|
387
428
|
request(filters, opts) {
|
|
388
429
|
return this.req(filters, opts?.id).pipe(
|
|
389
430
|
// Retry on connection errors
|
|
390
|
-
this.customRetryOperator(opts?.retries ??
|
|
391
|
-
// Create
|
|
392
|
-
this.customRepeatOperator(opts?.
|
|
431
|
+
this.customRetryOperator(opts?.retries ?? opts?.reconnect ?? true, DEFAULT_RETRY_CONFIG),
|
|
432
|
+
// Create resubscribe logic (repeat operator)
|
|
433
|
+
this.customRepeatOperator(opts?.resubscribe),
|
|
393
434
|
// Complete when EOSE is received
|
|
394
435
|
completeOnEose(),
|
|
395
436
|
// Single subscription
|
|
@@ -404,16 +445,61 @@ export class Relay {
|
|
|
404
445
|
return of(result);
|
|
405
446
|
}),
|
|
406
447
|
// Retry the publish until it succeeds or the number of retries is reached
|
|
407
|
-
this.customRetryOperator(opts?.retries ??
|
|
408
|
-
//
|
|
409
|
-
this.
|
|
448
|
+
this.customRetryOperator(opts?.retries ?? opts?.reconnect ?? true, DEFAULT_RETRY_CONFIG),
|
|
449
|
+
// Add timeout for publishing
|
|
450
|
+
this.customTimeoutOperator(opts?.timeout, this.publishTimeout),
|
|
410
451
|
// Single subscription
|
|
411
452
|
share()));
|
|
412
453
|
}
|
|
454
|
+
/** Negentropy sync events with the relay and an event store */
|
|
455
|
+
sync(store, filter, direction = SyncDirection.RECEIVE) {
|
|
456
|
+
const getEvents = async (ids) => {
|
|
457
|
+
if (Array.isArray(store))
|
|
458
|
+
return store.filter((event) => ids.includes(event.id));
|
|
459
|
+
else
|
|
460
|
+
return store.getByFilters({ ids });
|
|
461
|
+
};
|
|
462
|
+
return new Observable((observer) => {
|
|
463
|
+
const controller = new AbortController();
|
|
464
|
+
this.negentropy(store, filter, async (have, need) => {
|
|
465
|
+
// NOTE: it may be more efficient to sync all the events later in a single batch
|
|
466
|
+
// Send missing events to the relay
|
|
467
|
+
if (direction & SyncDirection.SEND && have.length > 0) {
|
|
468
|
+
const events = await getEvents(have);
|
|
469
|
+
// Send all events to the relay
|
|
470
|
+
await Promise.allSettled(events.map((event) => lastValueFrom(this.event(event))));
|
|
471
|
+
}
|
|
472
|
+
// Fetch missing events from the relay
|
|
473
|
+
if (direction & SyncDirection.RECEIVE && need.length > 0) {
|
|
474
|
+
await lastValueFrom(this.req({ ids: need }).pipe(completeOnEose(), tap((event) => observer.next(event))));
|
|
475
|
+
}
|
|
476
|
+
}, { signal: controller.signal })
|
|
477
|
+
// Complete the observable when the sync is complete
|
|
478
|
+
.then(() => observer.complete())
|
|
479
|
+
// Error the observable when the sync fails
|
|
480
|
+
.catch((err) => observer.error(err));
|
|
481
|
+
// Cancel the sync when the observable is unsubscribed
|
|
482
|
+
return () => controller.abort();
|
|
483
|
+
}).pipe(
|
|
484
|
+
// Only create one upstream subscription
|
|
485
|
+
share());
|
|
486
|
+
}
|
|
413
487
|
/** Force close the connection */
|
|
414
488
|
close() {
|
|
415
489
|
this.socket.unsubscribe();
|
|
416
490
|
}
|
|
491
|
+
/** An async method that returns the NIP-11 information document for the relay */
|
|
492
|
+
async getInformation() {
|
|
493
|
+
return firstValueFrom(this.information$);
|
|
494
|
+
}
|
|
495
|
+
/** An async method that returns the NIP-11 limitations for the relay */
|
|
496
|
+
async getLimitations() {
|
|
497
|
+
return firstValueFrom(this.limitations$);
|
|
498
|
+
}
|
|
499
|
+
/** An async method that returns the supported NIPs for the relay */
|
|
500
|
+
async getSupported() {
|
|
501
|
+
return firstValueFrom(this.supported$);
|
|
502
|
+
}
|
|
417
503
|
/** Static method to fetch the NIP-11 information document for a relay */
|
|
418
504
|
static fetchInformationDocument(url) {
|
|
419
505
|
return from(fetch(ensureHttpURL(url), { headers: { Accept: "application/nostr+json" } }).then((res) => res.json())).pipe(
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import type { EventTemplate, Filter, NostrEvent } from "nostr-tools";
|
|
2
|
+
import type { RelayInformation } from "nostr-tools/nip11";
|
|
3
|
+
import type { Observable, repeat, retry } from "rxjs";
|
|
4
|
+
import type { WebSocketSubject } from "rxjs/webSocket";
|
|
5
|
+
import type { NegentropySyncOptions, ReconcileFunction } from "./negentropy.js";
|
|
6
|
+
import type { IAsyncEventStoreRead, IEventStoreRead } from "applesauce-core";
|
|
7
|
+
import type { SyncDirection } from "./relay.js";
|
|
8
|
+
import type { GroupNegentropySyncOptions, GroupRequestOptions, GroupSubscriptionOptions } from "./group.js";
|
|
4
9
|
export type SubscriptionResponse = NostrEvent | "EOSE";
|
|
5
10
|
export type PublishResponse = {
|
|
6
11
|
ok: boolean;
|
|
@@ -11,15 +16,18 @@ export type MultiplexWebSocket<T = any> = Pick<WebSocketSubject<T>, "multiplex">
|
|
|
11
16
|
/** Options for the publish method on the pool and relay */
|
|
12
17
|
export type PublishOptions = {
|
|
13
18
|
/**
|
|
14
|
-
* Number of times to retry the publish. default is
|
|
19
|
+
* Number of times to retry the publish. default is 10
|
|
15
20
|
* @see https://rxjs.dev/api/index/function/retry
|
|
21
|
+
* @deprecated use `reconnect` instead
|
|
16
22
|
*/
|
|
17
23
|
retries?: number | Parameters<typeof retry>[0];
|
|
18
24
|
/**
|
|
19
|
-
* Whether to reconnect when socket
|
|
20
|
-
* @see https://rxjs.dev/api/index/function/
|
|
25
|
+
* Whether to reconnect when socket fails to connect. default is true (10 retries with 1 second delay)
|
|
26
|
+
* @see https://rxjs.dev/api/index/function/retry
|
|
21
27
|
*/
|
|
22
|
-
reconnect?: boolean | Parameters<typeof
|
|
28
|
+
reconnect?: boolean | number | Parameters<typeof retry>[0];
|
|
29
|
+
/** Timeout for publish in milliseconds (default 30 seconds) */
|
|
30
|
+
timeout?: number | boolean;
|
|
23
31
|
};
|
|
24
32
|
/** Options for the request method on the pool and relay */
|
|
25
33
|
export type RequestOptions = SubscriptionOptions;
|
|
@@ -28,15 +36,21 @@ export type SubscriptionOptions = {
|
|
|
28
36
|
/** Custom REQ id for the subscription */
|
|
29
37
|
id?: string;
|
|
30
38
|
/**
|
|
31
|
-
* Number of times to retry
|
|
39
|
+
* Number of times to retry the subscription if the relay fails to connect. default is 10
|
|
32
40
|
* @see https://rxjs.dev/api/index/function/retry
|
|
41
|
+
* @deprecated use `reconnect` instead
|
|
33
42
|
*/
|
|
34
43
|
retries?: number | Parameters<typeof retry>[0];
|
|
35
44
|
/**
|
|
36
|
-
* Whether to
|
|
45
|
+
* Whether to resubscribe if the subscription is closed by the relay. default is false
|
|
37
46
|
* @see https://rxjs.dev/api/index/function/repeat
|
|
38
47
|
*/
|
|
39
|
-
|
|
48
|
+
resubscribe?: boolean | number | Parameters<typeof repeat>[0];
|
|
49
|
+
/**
|
|
50
|
+
* Whether to reconnect when socket is closed. default is true (10 retries with 1 second delay)
|
|
51
|
+
* @see https://rxjs.dev/api/index/function/retry
|
|
52
|
+
*/
|
|
53
|
+
reconnect?: boolean | number | Parameters<typeof retry>[0];
|
|
40
54
|
};
|
|
41
55
|
export type AuthSigner = {
|
|
42
56
|
signEvent: (event: EventTemplate) => NostrEvent | Promise<NostrEvent>;
|
|
@@ -51,6 +65,7 @@ export interface IRelay extends MultiplexWebSocket {
|
|
|
51
65
|
challenge$: Observable<string | null>;
|
|
52
66
|
authenticated$: Observable<boolean>;
|
|
53
67
|
notices$: Observable<string[]>;
|
|
68
|
+
error$: Observable<Error | null>;
|
|
54
69
|
readonly connected: boolean;
|
|
55
70
|
readonly authenticated: boolean;
|
|
56
71
|
readonly challenge: string | null;
|
|
@@ -63,6 +78,8 @@ export interface IRelay extends MultiplexWebSocket {
|
|
|
63
78
|
event(event: NostrEvent): Observable<PublishResponse>;
|
|
64
79
|
/** Send an AUTH message */
|
|
65
80
|
auth(event: NostrEvent): Promise<PublishResponse>;
|
|
81
|
+
/** Negentropy sync event ids with the relay and an event store */
|
|
82
|
+
negentropy(store: IEventStoreRead | IAsyncEventStoreRead | NostrEvent[], filter: Filter, reconcile: ReconcileFunction, opts?: NegentropySyncOptions): Promise<boolean>;
|
|
66
83
|
/** Authenticate with the relay using a signer */
|
|
67
84
|
authenticate(signer: AuthSigner): Promise<PublishResponse>;
|
|
68
85
|
/** Send an EVENT message with retries */
|
|
@@ -71,20 +88,37 @@ export interface IRelay extends MultiplexWebSocket {
|
|
|
71
88
|
request(filters: FilterInput, opts?: RequestOptions): Observable<NostrEvent>;
|
|
72
89
|
/** Open a subscription with retries */
|
|
73
90
|
subscription(filters: FilterInput, opts?: SubscriptionOptions): Observable<SubscriptionResponse>;
|
|
91
|
+
/** Negentropy sync events with the relay and an event store */
|
|
92
|
+
sync(store: IEventStoreRead | IAsyncEventStoreRead | NostrEvent[], filter: Filter, direction?: SyncDirection): Observable<NostrEvent>;
|
|
93
|
+
/** Get the NIP-11 information document for the relay */
|
|
94
|
+
getInformation(): Promise<RelayInformation | null>;
|
|
95
|
+
/** Get the limitations for the relay */
|
|
96
|
+
getLimitations(): Promise<RelayInformation["limitation"] | null>;
|
|
97
|
+
/** Get the supported NIPs for the relay */
|
|
98
|
+
getSupported(): Promise<number[] | null>;
|
|
74
99
|
}
|
|
75
100
|
export interface IGroup {
|
|
76
101
|
/** Send a REQ message */
|
|
77
102
|
req(filters: FilterInput, id?: string): Observable<SubscriptionResponse>;
|
|
78
103
|
/** Send an EVENT message */
|
|
79
104
|
event(event: NostrEvent): Observable<PublishResponse>;
|
|
105
|
+
/** Negentropy sync event ids with the relays and an event store */
|
|
106
|
+
negentropy(store: IEventStoreRead | IAsyncEventStoreRead | NostrEvent[], filter: Filter, reconcile: ReconcileFunction, opts?: NegentropySyncOptions): Promise<boolean>;
|
|
80
107
|
/** Send an EVENT message with retries */
|
|
81
108
|
publish(event: NostrEvent, opts?: PublishOptions): Promise<PublishResponse[]>;
|
|
82
109
|
/** Send a REQ message with retries */
|
|
83
|
-
request(filters: FilterInput, opts?:
|
|
110
|
+
request(filters: FilterInput, opts?: GroupRequestOptions): Observable<NostrEvent>;
|
|
84
111
|
/** Open a subscription with retries */
|
|
85
|
-
subscription(filters: FilterInput, opts?:
|
|
112
|
+
subscription(filters: FilterInput, opts?: GroupSubscriptionOptions): Observable<SubscriptionResponse>;
|
|
113
|
+
/** Negentropy sync events with the relay and an event store */
|
|
114
|
+
sync(store: IEventStoreRead | IAsyncEventStoreRead | NostrEvent[], filter: Filter, direction?: SyncDirection): Observable<NostrEvent>;
|
|
115
|
+
}
|
|
116
|
+
/** Signals emitted by the pool */
|
|
117
|
+
export interface IPoolSignals {
|
|
118
|
+
add$: Observable<IRelay>;
|
|
119
|
+
remove$: Observable<IRelay>;
|
|
86
120
|
}
|
|
87
|
-
export interface IPool {
|
|
121
|
+
export interface IPool extends IPoolSignals {
|
|
88
122
|
/** Get or create a relay */
|
|
89
123
|
relay(url: string): IRelay;
|
|
90
124
|
/** Create a relay group */
|
|
@@ -95,10 +129,14 @@ export interface IPool {
|
|
|
95
129
|
req(relays: string[], filters: FilterInput, id?: string): Observable<SubscriptionResponse>;
|
|
96
130
|
/** Send an EVENT message */
|
|
97
131
|
event(relays: string[], event: NostrEvent): Observable<PublishResponse>;
|
|
132
|
+
/** Negentropy sync event ids with the relays and an event store */
|
|
133
|
+
negentropy(relays: string[], store: IEventStoreRead | IAsyncEventStoreRead | NostrEvent[], filter: Filter, reconcile: ReconcileFunction, opts?: GroupNegentropySyncOptions): Promise<boolean>;
|
|
98
134
|
/** Send an EVENT message to relays with retries */
|
|
99
|
-
publish(relays: string[], event:
|
|
135
|
+
publish(relays: string[], event: Parameters<IGroup["publish"]>[0], opts?: Parameters<IGroup["publish"]>[1]): Promise<PublishResponse[]>;
|
|
100
136
|
/** Send a REQ message to relays with retries */
|
|
101
|
-
request(relays: string[], filters:
|
|
137
|
+
request(relays: string[], filters: Parameters<IGroup["request"]>[0], opts?: Parameters<IGroup["request"]>[1]): Observable<NostrEvent>;
|
|
102
138
|
/** Open a subscription to relays with retries */
|
|
103
|
-
subscription(relays: string[], filters:
|
|
139
|
+
subscription(relays: string[], filters: Parameters<IGroup["subscription"]>[0], opts?: Parameters<IGroup["subscription"]>[1]): Observable<SubscriptionResponse>;
|
|
140
|
+
/** Negentropy sync events with the relay and an event store */
|
|
141
|
+
sync(relays: string[], store: IEventStoreRead | IAsyncEventStoreRead | NostrEvent[], filter: Filter, direction?: SyncDirection): Observable<NostrEvent>;
|
|
104
142
|
}
|
package/package.json
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "applesauce-relay",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
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",
|
|
8
8
|
"keywords": [
|
|
9
|
-
"nostr"
|
|
10
|
-
"applesauce"
|
|
9
|
+
"nostr"
|
|
11
10
|
],
|
|
12
11
|
"author": "hzrd149",
|
|
13
12
|
"license": "MIT",
|
|
14
13
|
"files": [
|
|
15
|
-
"dist"
|
|
16
|
-
"applesauce"
|
|
14
|
+
"dist"
|
|
17
15
|
],
|
|
18
16
|
"exports": {
|
|
19
17
|
".": {
|
|
@@ -54,17 +52,17 @@
|
|
|
54
52
|
},
|
|
55
53
|
"dependencies": {
|
|
56
54
|
"@noble/hashes": "^1.7.1",
|
|
57
|
-
"applesauce-core": "^
|
|
55
|
+
"applesauce-core": "^4.0.0",
|
|
58
56
|
"nanoid": "^5.0.9",
|
|
59
|
-
"nostr-tools": "
|
|
57
|
+
"nostr-tools": "~2.17",
|
|
60
58
|
"rxjs": "^7.8.1"
|
|
61
59
|
},
|
|
62
60
|
"devDependencies": {
|
|
63
61
|
"@hirez_io/observer-spy": "^2.2.0",
|
|
64
|
-
"applesauce-signers": "^
|
|
65
|
-
"
|
|
62
|
+
"applesauce-signers": "^4.0.0",
|
|
63
|
+
"rimraf": "^6.0.1",
|
|
66
64
|
"typescript": "^5.7.3",
|
|
67
|
-
"vitest": "^3.2.
|
|
65
|
+
"vitest": "^3.2.4",
|
|
68
66
|
"vitest-websocket-mock": "^0.5.0"
|
|
69
67
|
},
|
|
70
68
|
"funding": {
|
|
@@ -72,6 +70,7 @@
|
|
|
72
70
|
"url": "lightning:nostrudel@geyser.fund"
|
|
73
71
|
},
|
|
74
72
|
"scripts": {
|
|
73
|
+
"prebuild": "rimraf dist",
|
|
75
74
|
"build": "tsc",
|
|
76
75
|
"watch:build": "tsc --watch > /dev/null",
|
|
77
76
|
"test": "vitest run --passWithNoTests",
|