applesauce-relay 0.0.0-next-20251020143053 → 0.0.0-next-20251030142514

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.
@@ -1,5 +1,5 @@
1
1
  import { logger } from "applesauce-core";
2
- import { map, share, firstValueFrom } from "rxjs";
2
+ import { map, share, firstValueFrom, race, Observable } from "rxjs";
3
3
  import { nanoid } from "nanoid";
4
4
  import { Negentropy, NegentropyStorageVector } from "./lib/negentropy.js";
5
5
  const log = logger.extend("negentropy");
@@ -52,16 +52,49 @@ export async function negentropySync(storage, socket, filter, reconcile, opts) {
52
52
  throw new Error(msg[2]);
53
53
  return msg[2];
54
54
  }), share());
55
+ // Check if already aborted before starting sync
56
+ if (opts?.signal?.aborted)
57
+ return false;
58
+ // Create an observable that emits when abort signal is triggered
59
+ const abortSignal$ = new Observable((observer) => {
60
+ if (opts?.signal?.aborted) {
61
+ observer.next("abort");
62
+ observer.complete();
63
+ return;
64
+ }
65
+ const onAbort = () => {
66
+ observer.next("abort");
67
+ observer.complete();
68
+ };
69
+ opts?.signal?.addEventListener("abort", onAbort);
70
+ return () => opts?.signal?.removeEventListener("abort", onAbort);
71
+ });
55
72
  // keep an additional subscription open while waiting for async operations
56
- const sub = incoming.subscribe((m) => log(m));
73
+ const sub = incoming.subscribe({
74
+ next: (m) => log(m),
75
+ error: () => { }, // Ignore errors here, they'll be caught by firstValueFrom
76
+ });
57
77
  try {
58
78
  while (msg && opts?.signal?.aborted !== true) {
59
- const received = await firstValueFrom(incoming);
60
- if (opts?.signal?.aborted)
61
- return false;
62
- const [newMsg, have, need] = await ne.reconcile(received);
63
- await reconcile(have, need);
64
- msg = newMsg;
79
+ // Race between incoming message and abort signal
80
+ try {
81
+ const received = await firstValueFrom(race(incoming.pipe(map((m) => ({ type: "message", data: m }))), abortSignal$.pipe(map(() => ({ type: "abort" })))));
82
+ if (received.type === "abort" || opts?.signal?.aborted) {
83
+ sub.unsubscribe();
84
+ return false;
85
+ }
86
+ const [newMsg, have, need] = await ne.reconcile(received.data);
87
+ await reconcile(have, need);
88
+ msg = newMsg;
89
+ }
90
+ catch (err) {
91
+ // Check if aborted during reconcile or message processing
92
+ if (opts?.signal?.aborted) {
93
+ sub.unsubscribe();
94
+ return false;
95
+ }
96
+ throw err;
97
+ }
65
98
  }
66
99
  }
67
100
  catch (err) {
package/dist/pool.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { IAsyncEventStoreRead, IEventStoreRead } from "applesauce-core";
2
+ import { FilterMap, OutboxMap } from "applesauce-core/helpers";
2
3
  import { Filter, type NostrEvent } from "nostr-tools";
3
4
  import { BehaviorSubject, Observable, Subject } from "rxjs";
4
5
  import { RelayGroup } from "./group.js";
@@ -33,7 +34,9 @@ export declare class RelayPool implements IPool {
33
34
  /** Open a subscription to multiple relays */
34
35
  subscription(relays: IPoolRelayInput, filters: FilterInput, options?: Parameters<RelayGroup["subscription"]>[1]): Observable<SubscriptionResponse>;
35
36
  /** 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
+ subscriptionMap(relays: FilterMap | Observable<FilterMap>, options?: Parameters<RelayGroup["subscription"]>[1]): Observable<SubscriptionResponse>;
38
+ /** Open a subscription for an {@link OutboxMap} and filter */
39
+ outboxSubscription(outboxes: OutboxMap | Observable<OutboxMap>, filter: Omit<Filter, "authors">, options?: Parameters<RelayGroup["subscription"]>[1]): Observable<SubscriptionResponse>;
37
40
  /** Count events on multiple relays */
38
41
  count(relays: IPoolRelayInput, filters: Filter | Filter[], id?: string): Observable<Record<string, CountResponse>>;
39
42
  /** Negentropy sync events with the relays and an event store */
package/dist/pool.js CHANGED
@@ -1,4 +1,4 @@
1
- import { isFilterEqual, normalizeURL } from "applesauce-core/helpers";
1
+ import { createFilterMap, isFilterEqual, normalizeURL } from "applesauce-core/helpers";
2
2
  import { BehaviorSubject, distinctUntilChanged, isObservable, map, of, Subject } from "rxjs";
3
3
  import { RelayGroup } from "./group.js";
4
4
  import { Relay } from "./relay.js";
@@ -94,6 +94,15 @@ export class RelayPool {
94
94
  distinctUntilChanged(isFilterEqual));
95
95
  }, options);
96
96
  }
97
+ /** Open a subscription for an {@link OutboxMap} and filter */
98
+ outboxSubscription(outboxes, filter, options) {
99
+ const filterMap = isObservable(outboxes)
100
+ ? outboxes.pipe(
101
+ // Project outbox map to filter map
102
+ map((outboxes) => createFilterMap(outboxes, filter)))
103
+ : createFilterMap(outboxes, filter);
104
+ return this.subscriptionMap(filterMap, options);
105
+ }
97
106
  /** Count events on multiple relays */
98
107
  count(relays, filters, id) {
99
108
  return this.group(relays).count(filters, id);
package/dist/relay.js CHANGED
@@ -506,6 +506,14 @@ export class Relay {
506
506
  };
507
507
  return new Observable((observer) => {
508
508
  const controller = new AbortController();
509
+ let cleanupCalled = false;
510
+ // Store reference to cleanup the negentropy properly
511
+ const cleanup = () => {
512
+ if (!cleanupCalled) {
513
+ cleanupCalled = true;
514
+ controller.abort();
515
+ }
516
+ };
509
517
  this.negentropy(store, filter, async (have, need) => {
510
518
  // NOTE: it may be more efficient to sync all the events later in a single batch
511
519
  // Send missing events to the relay
@@ -520,11 +528,17 @@ export class Relay {
520
528
  }
521
529
  }, { signal: controller.signal })
522
530
  // Complete the observable when the sync is complete
523
- .then(() => observer.complete())
531
+ .then(() => {
532
+ if (!cleanupCalled)
533
+ observer.complete();
534
+ })
524
535
  // Error the observable when the sync fails
525
- .catch((err) => observer.error(err));
536
+ .catch((err) => {
537
+ if (!cleanupCalled)
538
+ observer.error(err);
539
+ });
526
540
  // Cancel the sync when the observable is unsubscribed
527
- return () => controller.abort();
541
+ return cleanup;
528
542
  }).pipe(
529
543
  // Only create one upstream subscription
530
544
  share());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-relay",
3
- "version": "0.0.0-next-20251020143053",
3
+ "version": "0.0.0-next-20251030142514",
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-20251020143053",
55
+ "applesauce-core": "0.0.0-next-20251030142514",
56
56
  "nanoid": "^5.0.9",
57
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": "^4.1.0",
62
+ "applesauce-signers": "0.0.0-next-20251030142514",
63
63
  "rimraf": "^6.0.1",
64
64
  "typescript": "^5.7.3",
65
65
  "vitest": "^3.2.4",