applesauce-relay 0.0.0-next-20250919114711 → 0.0.0-next-20250930093922

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 CHANGED
@@ -1,12 +1,29 @@
1
- import { type NostrEvent } from "nostr-tools";
1
+ import { Filter, type NostrEvent } from "nostr-tools";
2
2
  import { Observable } from "rxjs";
3
- import { IAsyncEventStoreActions, IEventStoreActions } from "applesauce-core";
3
+ import { IAsyncEventStoreActions, IAsyncEventStoreRead, IEventStoreActions, IEventStoreRead } from "applesauce-core";
4
+ import { NegentropySyncOptions, type ReconcileFunction } from "./negentropy.js";
4
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
+ };
5
22
  export declare class RelayGroup implements IGroup {
6
23
  relays: IRelay[];
7
24
  constructor(relays: IRelay[]);
8
25
  /** Takes an array of observables and only emits EOSE when all observables have emitted EOSE */
9
- protected mergeEOSE(requests: Observable<SubscriptionResponse>[], eventStore?: IEventStoreActions | IAsyncEventStoreActions): Observable<import("nostr-tools").Event | "EOSE">;
26
+ protected mergeEOSE(requests: Observable<SubscriptionResponse>[], eventStore?: IEventStoreActions | IAsyncEventStoreActions | null): Observable<import("nostr-tools").Event | "EOSE">;
10
27
  /**
11
28
  * Make a request to all relays
12
29
  * @note This does not deduplicate events
@@ -14,14 +31,14 @@ export declare class RelayGroup implements IGroup {
14
31
  req(filters: FilterInput, id?: string): Observable<SubscriptionResponse>;
15
32
  /** Send an event to all relays */
16
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>;
17
36
  /** Publish an event to all relays with retries ( default 3 retries ) */
18
37
  publish(event: NostrEvent, opts?: PublishOptions): Promise<PublishResponse[]>;
19
38
  /** Request events from all relays with retries ( default 3 retries ) */
20
- request(filters: FilterInput, opts?: RequestOptions & {
21
- eventStore?: IEventStoreActions;
22
- }): Observable<NostrEvent>;
39
+ request(filters: FilterInput, opts?: GroupRequestOptions): Observable<NostrEvent>;
23
40
  /** Open a subscription to all relays with retries ( default 3 retries ) */
24
- subscription(filters: FilterInput, opts?: SubscriptionOptions & {
25
- eventStore?: IEventStoreActions;
26
- }): Observable<SubscriptionResponse>;
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>;
27
44
  }
package/dist/group.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { nanoid } from "nanoid";
2
- import { catchError, EMPTY, endWith, identity, ignoreElements, merge, of } from "rxjs";
3
- import { filterDuplicateEvents } from "applesauce-core";
2
+ import { catchError, defer, EMPTY, endWith, identity, ignoreElements, merge, of, share, switchMap, } from "rxjs";
3
+ import { EventMemory, filterDuplicateEvents, } from "applesauce-core";
4
4
  import { completeOnEose } from "./operators/complete-on-eose.js";
5
5
  import { onlyEvents } from "./operators/only-events.js";
6
6
  export class RelayGroup {
@@ -9,7 +9,7 @@ export class RelayGroup {
9
9
  this.relays = relays;
10
10
  }
11
11
  /** Takes an array of observables and only emits EOSE when all observables have emitted EOSE */
12
- mergeEOSE(requests, eventStore) {
12
+ mergeEOSE(requests, eventStore = new EventMemory()) {
13
13
  // Create stream of events only
14
14
  const events = merge(...requests).pipe(
15
15
  // Ignore non event responses
@@ -41,6 +41,20 @@ export class RelayGroup {
41
41
  // Catch error and return as PublishResponse
42
42
  catchError((err) => of({ ok: false, from: relay.url, message: err?.message || "Unknown error" })))));
43
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
+ }
44
58
  /** Publish an event to all relays with retries ( default 3 retries ) */
45
59
  publish(event, opts) {
46
60
  return Promise.all(this.relays.map((relay) => relay.publish(event, opts).catch(
@@ -53,7 +67,7 @@ export class RelayGroup {
53
67
  // Ignore individual connection errors
54
68
  catchError(() => EMPTY)))).pipe(
55
69
  // If an event store is provided, filter duplicate events
56
- opts?.eventStore ? filterDuplicateEvents(opts.eventStore) : identity);
70
+ opts?.eventStore == null ? identity : filterDuplicateEvents(opts?.eventStore ?? new EventMemory()));
57
71
  }
58
72
  /** Open a subscription to all relays with retries ( default 3 retries ) */
59
73
  subscription(filters, opts) {
@@ -63,4 +77,19 @@ export class RelayGroup {
63
77
  // Pass event store so that duplicate events are removed
64
78
  opts?.eventStore);
65
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());
94
+ }
66
95
  }
@@ -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<ArrayBuffer>;
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<ArrayBuffer>;
58
+ id: Uint8Array<ArrayBufferLike>;
59
59
  };
60
60
  }
61
61
  export { Negentropy, NegentropyStorageVector };
@@ -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
- export declare function buildStorageFromFilter(store: IEventStoreRead, filter: Filter): NegentropyStorageVector;
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: (have: string[], need: string[]) => Promise<void>, opts?: {
13
- frameSizeLimit?: number;
14
- signal?: AbortSignal;
15
- }): Promise<boolean>;
31
+ }, filter: Filter, reconcile: ReconcileFunction, opts?: NegentropySyncOptions): Promise<boolean>;
@@ -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
- export function buildStorageFromFilter(store, filter) {
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) => console.log(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,8 +1,10 @@
1
- import { type NostrEvent } from "nostr-tools";
1
+ import type { IAsyncEventStoreRead, IEventStoreRead } from "applesauce-core";
2
+ import { Filter, type NostrEvent } from "nostr-tools";
2
3
  import { BehaviorSubject, Observable, Subject } from "rxjs";
3
4
  import { RelayGroup } from "./group.js";
4
- import { Relay, RelayOptions } from "./relay.js";
5
- import { FilterInput, IPool, IRelay, PublishResponse, SubscriptionResponse } from "./types.js";
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>>;
@@ -27,10 +29,14 @@ export declare class RelayPool implements IPool {
27
29
  req(relays: string[], filters: FilterInput, id?: string): Observable<SubscriptionResponse>;
28
30
  /** Send an EVENT message to multiple relays */
29
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>;
30
34
  /** Publish an event to multiple relays */
31
35
  publish(relays: string[], event: Parameters<RelayGroup["publish"]>[0], opts?: Parameters<RelayGroup["publish"]>[1]): Promise<PublishResponse[]>;
32
36
  /** Request events from multiple relays */
33
37
  request(relays: string[], filters: Parameters<RelayGroup["request"]>[0], opts?: Parameters<RelayGroup["request"]>[1]): Observable<NostrEvent>;
34
38
  /** Open a subscription to multiple relays */
35
- subscription(relays: string[], filters: Parameters<RelayGroup["subscription"]>[0], opts?: Parameters<RelayGroup["subscription"]>[1]): Observable<SubscriptionResponse>;
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>;
36
42
  }
package/dist/pool.js CHANGED
@@ -1,5 +1,5 @@
1
- import { BehaviorSubject, Subject } from "rxjs";
2
1
  import { normalizeURL } from "applesauce-core/helpers";
2
+ import { BehaviorSubject, Subject } from "rxjs";
3
3
  import { RelayGroup } from "./group.js";
4
4
  import { Relay } from "./relay.js";
5
5
  export class RelayPool {
@@ -96,6 +96,10 @@ export class RelayPool {
96
96
  event(relays, event) {
97
97
  return this.group(relays).event(event);
98
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
+ }
99
103
  /** Publish an event to multiple relays */
100
104
  publish(relays, event, opts) {
101
105
  return this.group(relays).publish(event, opts);
@@ -105,7 +109,11 @@ export class RelayPool {
105
109
  return this.group(relays).request(filters, opts);
106
110
  }
107
111
  /** Open a subscription to multiple relays */
108
- subscription(relays, filters, opts) {
109
- return this.group(relays).subscription(filters, opts);
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);
110
118
  }
111
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
  }
@@ -56,6 +63,8 @@ export declare class Relay implements IRelay {
56
63
  protected _nip11: RelayInformation | null;
57
64
  /** An observable that emits the limitations for the relay */
58
65
  limitations$: Observable<RelayInformation["limitation"] | null>;
66
+ /** An array of supported NIPs from the NIP-11 information document */
67
+ supported$: Observable<number[] | null>;
59
68
  /** An observable that emits when underlying websocket is opened */
60
69
  open$: Subject<Event>;
61
70
  /** An observable that emits when underlying websocket is closed */
@@ -99,6 +108,8 @@ export declare class Relay implements IRelay {
99
108
  event(event: NostrEvent, verb?: "EVENT" | "AUTH"): Observable<PublishResponse>;
100
109
  /** send and AUTH message */
101
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>;
102
113
  /** Authenticate with the relay using a signer */
103
114
  authenticate(signer: AuthSigner): Promise<PublishResponse>;
104
115
  /** Internal operator for creating the retry() operator */
@@ -113,12 +124,16 @@ export declare class Relay implements IRelay {
113
124
  request(filters: Filter | Filter[], opts?: RequestOptions): Observable<NostrEvent>;
114
125
  /** Publishes an event to the relay and retries when relay errors or responds with auth-required ( default 3 retries ) */
115
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>;
116
129
  /** Force close the connection */
117
130
  close(): void;
118
131
  /** An async method that returns the NIP-11 information document for the relay */
119
132
  getInformation(): Promise<RelayInformation | null>;
120
133
  /** An async method that returns the NIP-11 limitations for the relay */
121
134
  getLimitations(): Promise<RelayInformation["limitation"] | null>;
135
+ /** An async method that returns the supported NIPs for the relay */
136
+ getSupported(): Promise<number[] | null>;
122
137
  /** Static method to fetch the NIP-11 information document for a relay */
123
138
  static fetchInformationDocument(url: string): Observable<RelayInformation | null>;
124
139
  /** Static method to create a reconnection method for each relay */
package/dist/relay.js CHANGED
@@ -3,11 +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, firstValueFrom, 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
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 = {}));
11
18
  /** An error that is thrown when a REQ is closed from the relay side */
12
19
  export class ReqCloseError extends Error {
13
20
  }
@@ -48,6 +55,8 @@ export class Relay {
48
55
  _nip11 = null;
49
56
  /** An observable that emits the limitations for the relay */
50
57
  limitations$;
58
+ /** An array of supported NIPs from the NIP-11 information document */
59
+ supported$;
51
60
  /** An observable that emits when underlying websocket is opened */
52
61
  open$ = new Subject();
53
62
  /** An observable that emits when underlying websocket is closed */
@@ -153,7 +162,8 @@ export class Relay {
153
162
  tap((info) => (this._nip11 = info)),
154
163
  // cache the result
155
164
  shareReplay(1));
156
- this.limitations$ = this.information$.pipe(map((info) => info?.limitation));
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));
157
167
  // Create observables that track if auth is required for REQ or EVENT
158
168
  this.authRequiredForRead$ = this.receivedAuthRequiredForReq.pipe(tap((required) => required && this.log("Auth required for REQ")), shareReplay(1));
159
169
  this.authRequiredForPublish$ = this.receivedAuthRequiredForEvent.pipe(tap((required) => required && this.log("Auth required for EVENT")), shareReplay(1));
@@ -352,6 +362,16 @@ export class Relay {
352
362
  // update authenticated
353
363
  tap((result) => this.authenticationResponse$.next(result))));
354
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
+ }
355
375
  /** Authenticate with the relay using a signer */
356
376
  authenticate(signer) {
357
377
  if (!this.challenge)
@@ -431,6 +451,39 @@ export class Relay {
431
451
  // Single subscription
432
452
  share()));
433
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
+ }
434
487
  /** Force close the connection */
435
488
  close() {
436
489
  this.socket.unsubscribe();
@@ -443,6 +496,10 @@ export class Relay {
443
496
  async getLimitations() {
444
497
  return firstValueFrom(this.limitations$);
445
498
  }
499
+ /** An async method that returns the supported NIPs for the relay */
500
+ async getSupported() {
501
+ return firstValueFrom(this.supported$);
502
+ }
446
503
  /** Static method to fetch the NIP-11 information document for a relay */
447
504
  static fetchInformationDocument(url) {
448
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 { type EventTemplate, type Filter, type NostrEvent } from "nostr-tools";
2
- import { Observable, repeat, retry } from "rxjs";
3
- import { WebSocketSubject } from "rxjs/webSocket";
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;
@@ -73,6 +78,8 @@ export interface IRelay extends MultiplexWebSocket {
73
78
  event(event: NostrEvent): Observable<PublishResponse>;
74
79
  /** Send an AUTH message */
75
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>;
76
83
  /** Authenticate with the relay using a signer */
77
84
  authenticate(signer: AuthSigner): Promise<PublishResponse>;
78
85
  /** Send an EVENT message with retries */
@@ -81,18 +88,30 @@ export interface IRelay extends MultiplexWebSocket {
81
88
  request(filters: FilterInput, opts?: RequestOptions): Observable<NostrEvent>;
82
89
  /** Open a subscription with retries */
83
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>;
84
99
  }
85
100
  export interface IGroup {
86
101
  /** Send a REQ message */
87
102
  req(filters: FilterInput, id?: string): Observable<SubscriptionResponse>;
88
103
  /** Send an EVENT message */
89
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>;
90
107
  /** Send an EVENT message with retries */
91
108
  publish(event: NostrEvent, opts?: PublishOptions): Promise<PublishResponse[]>;
92
109
  /** Send a REQ message with retries */
93
- request(filters: FilterInput, opts?: RequestOptions): Observable<NostrEvent>;
110
+ request(filters: FilterInput, opts?: GroupRequestOptions): Observable<NostrEvent>;
94
111
  /** Open a subscription with retries */
95
- subscription(filters: FilterInput, opts?: SubscriptionOptions): Observable<SubscriptionResponse>;
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>;
96
115
  }
97
116
  /** Signals emitted by the pool */
98
117
  export interface IPoolSignals {
@@ -110,10 +129,14 @@ export interface IPool extends IPoolSignals {
110
129
  req(relays: string[], filters: FilterInput, id?: string): Observable<SubscriptionResponse>;
111
130
  /** Send an EVENT message */
112
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>;
113
134
  /** Send an EVENT message to relays with retries */
114
135
  publish(relays: string[], event: Parameters<IGroup["publish"]>[0], opts?: Parameters<IGroup["publish"]>[1]): Promise<PublishResponse[]>;
115
136
  /** Send a REQ message to relays with retries */
116
137
  request(relays: string[], filters: Parameters<IGroup["request"]>[0], opts?: Parameters<IGroup["request"]>[1]): Observable<NostrEvent>;
117
138
  /** Open a subscription to relays with retries */
118
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>;
119
142
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-relay",
3
- "version": "0.0.0-next-20250919114711",
3
+ "version": "0.0.0-next-20250930093922",
4
4
  "description": "nostr relay communication framework built on rxjs",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -52,14 +52,14 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "@noble/hashes": "^1.7.1",
55
- "applesauce-core": "0.0.0-next-20250919114711",
55
+ "applesauce-core": "0.0.0-next-20250930093922",
56
56
  "nanoid": "^5.0.9",
57
- "nostr-tools": "~2.15",
57
+ "nostr-tools": "~2.17",
58
58
  "rxjs": "^7.8.1"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@hirez_io/observer-spy": "^2.2.0",
62
- "applesauce-signers": "0.0.0-next-20250919114711",
62
+ "applesauce-signers": "0.0.0-next-20250930093922",
63
63
  "rimraf": "^6.0.1",
64
64
  "typescript": "^5.7.3",
65
65
  "vitest": "^3.2.4",