applesauce-loaders 2.0.0 → 3.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.
@@ -1,16 +1,6 @@
1
1
  import { AddressPointerWithoutD } from "applesauce-core/helpers";
2
2
  import { Filter } from "nostr-tools";
3
3
  import { AddressPointer } from "nostr-tools/nip19";
4
- export type LoadableAddressPointer = {
5
- kind: number;
6
- pubkey: string;
7
- /** Optional "d" tag for paramaritized replaceable */
8
- identifier?: string;
9
- /** Relays to load from */
10
- relays?: string[];
11
- /** Ignore all forms of caching */
12
- force?: boolean;
13
- };
14
4
  /** Converts an array of address pointers to a filter */
15
5
  export declare function createFilterFromAddressPointers(pointers: AddressPointerWithoutD[] | AddressPointer[]): Filter;
16
6
  /** Takes a set of address pointers, groups them, then returns filters for the groups */
@@ -23,7 +13,3 @@ export declare function groupAddressPointersByKind(pointers: AddressPointerWitho
23
13
  export declare function groupAddressPointersByPubkey(pointers: AddressPointerWithoutD[]): Map<string, AddressPointerWithoutD[]>;
24
14
  /** Groups address pointers by kind or pubkey depending on which is most optimal */
25
15
  export declare function groupAddressPointersByPubkeyOrKind(pointers: AddressPointerWithoutD[]): Map<string, AddressPointerWithoutD[]> | Map<number, AddressPointerWithoutD[]>;
26
- /** @deprecated use mergeRelaySets instead */
27
- export declare function getRelaysFromPointers(pointers: AddressPointerWithoutD[]): Set<string>;
28
- /** deduplicates an array of address pointers and merges their relays array */
29
- export declare function consolidateAddressPointers(pointers: LoadableAddressPointer[]): LoadableAddressPointer[];
@@ -1,4 +1,3 @@
1
- import { createReplaceableAddress, mergeRelaySets } from "applesauce-core/helpers";
2
1
  import { isAddressableKind, isReplaceableKind } from "nostr-tools/kinds";
3
2
  import { unique } from "./array.js";
4
3
  /** Converts an array of address pointers to a filter */
@@ -14,15 +13,15 @@ export function createFilterFromAddressPointers(pointers) {
14
13
  /** Takes a set of address pointers, groups them, then returns filters for the groups */
15
14
  export function createFiltersFromAddressPointers(pointers) {
16
15
  // split the points in to two groups so they they don't mix in the filters
17
- const parameterizedReplaceable = pointers.filter((p) => isAddressableKind(p.kind));
18
16
  const replaceable = pointers.filter((p) => isReplaceableKind(p.kind));
17
+ const addressable = pointers.filter((p) => isAddressableKind(p.kind));
19
18
  const filters = [];
20
19
  if (replaceable.length > 0) {
21
20
  const groups = groupAddressPointersByPubkeyOrKind(replaceable);
22
21
  filters.push(...Array.from(groups.values()).map(createFilterFromAddressPointers));
23
22
  }
24
- if (parameterizedReplaceable.length > 0) {
25
- const groups = groupAddressPointersByPubkeyOrKind(parameterizedReplaceable);
23
+ if (addressable.length > 0) {
24
+ const groups = groupAddressPointersByPubkeyOrKind(addressable);
26
25
  filters.push(...Array.from(groups.values()).map(createFilterFromAddressPointers));
27
26
  }
28
27
  return filters;
@@ -62,48 +61,3 @@ export function groupAddressPointersByPubkeyOrKind(pointers) {
62
61
  const pubkeys = new Set(pointers.map((p) => p.pubkey));
63
62
  return pubkeys.size < kinds.size ? groupAddressPointersByPubkey(pointers) : groupAddressPointersByKind(pointers);
64
63
  }
65
- /** @deprecated use mergeRelaySets instead */
66
- export function getRelaysFromPointers(pointers) {
67
- const relays = new Set();
68
- for (const pointer of pointers) {
69
- if (!pointer.relays)
70
- continue;
71
- for (const relay of pointer.relays) {
72
- relays.add(relay);
73
- }
74
- }
75
- return relays;
76
- }
77
- /** deep clone a loadable pointer to ensure its safe to modify */
78
- function cloneLoadablePointer(pointer) {
79
- const clone = { ...pointer };
80
- if (pointer.relays)
81
- clone.relays = [...pointer.relays];
82
- return clone;
83
- }
84
- /** deduplicates an array of address pointers and merges their relays array */
85
- export function consolidateAddressPointers(pointers) {
86
- const byAddress = new Map();
87
- for (const pointer of pointers) {
88
- const addr = createReplaceableAddress(pointer.kind, pointer.pubkey, pointer.identifier);
89
- if (byAddress.has(addr)) {
90
- // duplicate, merge pointers
91
- const current = byAddress.get(addr);
92
- // merge relays
93
- if (pointer.relays) {
94
- if (current.relays)
95
- current.relays = mergeRelaySets(current.relays, pointer.relays);
96
- else
97
- current.relays = pointer.relays;
98
- }
99
- // merge force flag
100
- if (pointer.force !== undefined) {
101
- current.force = current.force || pointer.force;
102
- }
103
- }
104
- else
105
- byAddress.set(addr, cloneLoadablePointer(pointer));
106
- }
107
- // return consolidated pointers
108
- return Array.from(byAddress.values());
109
- }
@@ -1,9 +1,7 @@
1
1
  import { Observable } from "rxjs";
2
2
  import { Filter, NostrEvent } from "nostr-tools";
3
- import { CacheRequest, FilterRequest } from "../types.js";
3
+ import { CacheRequest } from "../types.js";
4
4
  /** Calls the cache request and converts the reponse into an observable */
5
5
  export declare function unwrapCacheRequest(request: CacheRequest, filters: Filter[]): Observable<NostrEvent>;
6
6
  /** Calls a cache request method with filters and marks all returned events as being from the cache */
7
7
  export declare function makeCacheRequest(request: CacheRequest, filters: Filter[]): Observable<NostrEvent>;
8
- /** Wraps a cache request method and returns a FilterRequest */
9
- export declare function wrapCacheRequest(request: CacheRequest): FilterRequest;
@@ -16,7 +16,3 @@ export function unwrapCacheRequest(request, filters) {
16
16
  export function makeCacheRequest(request, filters) {
17
17
  return unwrapCacheRequest(request, filters).pipe(tap((e) => markFromCache(e)));
18
18
  }
19
- /** Wraps a cache request method and returns a FilterRequest */
20
- export function wrapCacheRequest(request) {
21
- return (filters) => makeCacheRequest(request, filters);
22
- }
@@ -1,3 +1,8 @@
1
1
  import { MonoTypeOperatorFunction, Observable, OperatorFunction } from "rxjs";
2
- /** Creates a loader that takes a single value and batches the requests to an upstream loader */
2
+ /** Takes a value optionally wrapped in an observable and unwraps it */
3
+ export declare function unwrap<T, R>(value: T | Observable<T>, next: (value: T) => Observable<R>): Observable<R>;
4
+ /**
5
+ * Creates a loader that takes a single value and batches the requests to an upstream loader
6
+ * IMPORTANT: the buffer operator MUST NOT filter values. its important that every input creates a new upstream request
7
+ */
3
8
  export declare function batchLoader<Input extends unknown = unknown, Output extends unknown = unknown>(buffer: OperatorFunction<Input, Input[]>, upstream: (input: Input[]) => Observable<Output>, matcher: (input: Input, output: Output) => boolean, output?: MonoTypeOperatorFunction<Output>): (value: Input) => Observable<Output>;
@@ -1,18 +1,33 @@
1
- import { filter, identity, map, mergeAll, Observable, share, Subject, take, } from "rxjs";
2
- /** Creates a loader that takes a single value and batches the requests to an upstream loader */
1
+ import { filter, identity, isObservable, mergeAll, Observable, share, Subject, switchMap, take, } from "rxjs";
2
+ /** Takes a value optionally wrapped in an observable and unwraps it */
3
+ export function unwrap(value, next) {
4
+ return isObservable(value) ? value.pipe(take(1), switchMap(next)) : next(value);
5
+ }
6
+ /**
7
+ * Creates a loader that takes a single value and batches the requests to an upstream loader
8
+ * IMPORTANT: the buffer operator MUST NOT filter values. its important that every input creates a new upstream request
9
+ */
3
10
  export function batchLoader(buffer, upstream, matcher, output) {
4
11
  const queue = new Subject();
5
- const requests = queue.pipe(buffer,
6
- // ignore empty buffers
7
- filter((buffer) => buffer.length > 0),
8
- // create a new upstream request for each buffer and make sure only one is created
9
- map((v) => upstream(v).pipe(share())),
10
- // Make sure that only a single upstream buffer is created
11
- share());
12
+ const next = new Subject();
13
+ // Every "buffer" make a new upstream request
14
+ queue.pipe(buffer).subscribe((buffer) => {
15
+ // If there is nothing in the buffer, dont make a request
16
+ if (buffer.length === 0)
17
+ return;
18
+ return next.next(upstream(buffer).pipe(
19
+ // Never reset the upstream request
20
+ share({ resetOnRefCountZero: false, resetOnComplete: false, resetOnError: false })));
21
+ });
12
22
  return (input) => new Observable((observer) => {
13
23
  // Add the pointer to the queue when observable is subscribed
14
- setTimeout(() => queue.next(input), 0);
15
- return requests
24
+ // NOTE: do not use setTimeout here, FF has a strange bug where it will delay the queue.next until after the buffer
25
+ /*
26
+ Adding the value to the queue before the request subscribes to the next batch may cause it to miss the next batch
27
+ but it should work the majority of the time because buffers use setTimeout internally which always runs next tick
28
+ */
29
+ queue.next(input);
30
+ return next
16
31
  .pipe(
17
32
  // wait for the next batch to run
18
33
  take(1),
@@ -1,8 +1,17 @@
1
1
  import { IEventStore } from "applesauce-core";
2
2
  import { NostrEvent } from "nostr-tools";
3
3
  import { Observable } from "rxjs";
4
- import { LoadableAddressPointer } from "../helpers/address-pointer.js";
5
4
  import { CacheRequest, NostrRequest, UpstreamPool } from "../types.js";
5
+ export type LoadableAddressPointer = {
6
+ kind: number;
7
+ pubkey: string;
8
+ /** Optional "d" tag for paramaritized replaceable */
9
+ identifier?: string;
10
+ /** Relays to load from */
11
+ relays?: string[];
12
+ /** Whether to ignore the cache */
13
+ cache?: boolean;
14
+ };
6
15
  /** A method that takes address pointers and returns an observable of events */
7
16
  export type AddressPointersLoader = (pointers: LoadableAddressPointer[]) => Observable<NostrEvent>;
8
17
  export type AddressPointerLoader = (pointer: LoadableAddressPointer) => Observable<NostrEvent>;
@@ -17,6 +26,8 @@ export declare function relayHintsAddressPointersLoader(request: NostrRequest):
17
26
  export declare function relaysAddressPointersLoader(request: NostrRequest, relays: Observable<string[]> | string[]): AddressPointersLoader;
18
27
  /** Creates a loader that loads all event pointers based on their relays */
19
28
  export declare function addressPointerLoadingSequence(...loaders: (AddressPointersLoader | undefined)[]): AddressPointersLoader;
29
+ /** deduplicates an array of address pointers and merges their relays array */
30
+ export declare function consolidateAddressPointers(pointers: LoadableAddressPointer[]): LoadableAddressPointer[];
20
31
  export type AddressLoaderOptions = Partial<{
21
32
  /** Time interval to buffer requests in ms ( default 1000 ) */
22
33
  bufferTime: number;
@@ -28,10 +39,10 @@ export type AddressLoaderOptions = Partial<{
28
39
  cacheRequest: CacheRequest;
29
40
  /** Whether to follow relay hints ( default true ) */
30
41
  followRelayHints: boolean;
31
- /** Fallback lookup relays to check when event cant be found */
32
- lookupRelays: string[] | Observable<string[]>;
33
42
  /** An array of relays to always fetch from */
34
43
  extraRelays: string[] | Observable<string[]>;
44
+ /** Fallback lookup relays to check when event cant be found */
45
+ lookupRelays: string[] | Observable<string[]>;
35
46
  }>;
36
47
  /** Create a pre-built address pointer loader that supports batching, caching, and lookup relays */
37
48
  export declare function createAddressLoader(pool: UpstreamPool, opts?: AddressLoaderOptions): AddressPointerLoader;
@@ -1,19 +1,23 @@
1
1
  import { mapEventsToStore } from "applesauce-core";
2
2
  import { createReplaceableAddress, getReplaceableAddress, getReplaceableIdentifier, isReplaceable, mergeRelaySets, } from "applesauce-core/helpers";
3
- import { bufferTime, catchError, EMPTY, filter, isObservable, map, of, pipe, switchMap, take } from "rxjs";
4
- import { consolidateAddressPointers, createFiltersFromAddressPointers, isLoadableAddressPointer, } from "../helpers/address-pointer.js";
5
- import { makeCacheRequest, wrapCacheRequest } from "../helpers/cache.js";
6
- import { batchLoader } from "../helpers/loaders.js";
7
- import { wrapGeneratorFunction } from "../operators/generator.js";
3
+ import { bufferTime, catchError, EMPTY } from "rxjs";
4
+ import { createFiltersFromAddressPointers, isLoadableAddressPointer } from "../helpers/address-pointer.js";
5
+ import { makeCacheRequest } from "../helpers/cache.js";
6
+ import { batchLoader, unwrap } from "../helpers/loaders.js";
8
7
  import { wrapUpstreamPool } from "../helpers/upstream.js";
8
+ import { wrapGeneratorFunction } from "../operators/generator.js";
9
9
  /**
10
10
  * Loads address pointers from an async cache
11
11
  * @note ignores pointers with force=true
12
12
  */
13
13
  export function cacheAddressPointersLoader(request) {
14
- return (pointers) => makeCacheRequest(request, createFiltersFromAddressPointers(pointers
15
- // Ignore pointers that want to skip cache
16
- .filter((p) => p.force !== true)));
14
+ return (pointers) => {
15
+ pointers = pointers.filter((p) => p.cache !== false);
16
+ // Skip if there are no pointers to load from cache
17
+ if (pointers.length === 0)
18
+ return EMPTY;
19
+ return makeCacheRequest(request, createFiltersFromAddressPointers(pointers));
20
+ };
17
21
  }
18
22
  /** Loads address pointers from the relay hints */
19
23
  export function relayHintsAddressPointersLoader(request) {
@@ -27,22 +31,22 @@ export function relayHintsAddressPointersLoader(request) {
27
31
  }
28
32
  /** Loads address pointers from an array of relays */
29
33
  export function relaysAddressPointersLoader(request, relays) {
30
- return (pointers) =>
31
- // Resolve the relays as an observable
32
- (isObservable(relays) ? relays : of(relays)).pipe(
33
- // Only take the first value
34
- take(1),
35
- // Make the request
36
- switchMap((relays) => {
34
+ return (pointers) => unwrap(relays, (relays) => {
37
35
  if (relays.length === 0)
38
36
  return EMPTY;
39
37
  const filters = createFiltersFromAddressPointers(pointers);
40
38
  return request(relays, filters);
41
- }));
39
+ });
42
40
  }
43
41
  /** Creates a loader that loads all event pointers based on their relays */
44
42
  export function addressPointerLoadingSequence(...loaders) {
45
43
  return wrapGeneratorFunction(function* (pointers) {
44
+ // Filter out invalid pointers and consolidate
45
+ pointers = consolidateAddressPointers(pointers.filter(isLoadableAddressPointer));
46
+ // Skip if there are no pointers
47
+ if (pointers.length === 0)
48
+ return;
49
+ // Keep track of remaining pointers to load
46
50
  let remaining = Array.from(pointers);
47
51
  for (const loader of loaders) {
48
52
  if (loader === undefined)
@@ -53,32 +57,55 @@ export function addressPointerLoadingSequence(...loaders) {
53
57
  // Get set of addresses loaded
54
58
  const addresses = new Set(results.filter((e) => isReplaceable(e.kind)).map((event) => getReplaceableAddress(event)));
55
59
  // Remove the pointers that were loaded
56
- remaining = remaining.filter((p) => !addresses.has(createReplaceableAddress(p.kind, p.pubkey, p.identifier)) || p.force === true);
60
+ remaining = remaining.filter((p) => !addresses.has(createReplaceableAddress(p.kind, p.pubkey, p.identifier)));
57
61
  // If there are no remaining pointers, complete
58
62
  if (remaining.length === 0)
59
63
  return;
60
64
  }
61
65
  });
62
66
  }
67
+ /** deep clone a loadable pointer to ensure its safe to modify */
68
+ function cloneLoadablePointer(pointer) {
69
+ const clone = { ...pointer };
70
+ if (pointer.relays)
71
+ clone.relays = [...pointer.relays];
72
+ return clone;
73
+ }
74
+ /** deduplicates an array of address pointers and merges their relays array */
75
+ export function consolidateAddressPointers(pointers) {
76
+ const byAddress = new Map();
77
+ for (const pointer of pointers) {
78
+ const addr = createReplaceableAddress(pointer.kind, pointer.pubkey, pointer.identifier);
79
+ if (byAddress.has(addr)) {
80
+ // duplicate, merge pointers
81
+ const current = byAddress.get(addr);
82
+ // merge relays
83
+ if (pointer.relays && pointer.relays.length > 0) {
84
+ if (current.relays)
85
+ current.relays = mergeRelaySets(current.relays, pointer.relays);
86
+ else
87
+ current.relays = pointer.relays;
88
+ }
89
+ // merge cache flag
90
+ if (pointer.cache === false)
91
+ current.cache = false;
92
+ }
93
+ else
94
+ byAddress.set(addr, cloneLoadablePointer(pointer));
95
+ }
96
+ // return consolidated pointers
97
+ return Array.from(byAddress.values());
98
+ }
63
99
  /** Create a pre-built address pointer loader that supports batching, caching, and lookup relays */
64
100
  export function createAddressLoader(pool, opts) {
65
101
  const request = wrapUpstreamPool(pool);
66
- const cacheRequest = opts?.cacheRequest ? wrapCacheRequest(opts.cacheRequest) : undefined;
67
102
  return batchLoader(
68
- // Create batching sequence
69
- pipe(
70
- // filter out invalid pointers
71
- filter(isLoadableAddressPointer),
72
103
  // buffer requests by time or size
73
104
  bufferTime(opts?.bufferTime ?? 1000, undefined, opts?.bufferSize ?? 200),
74
- // Ingore empty buffers
75
- filter((b) => b.length > 0),
76
- // consolidate buffered pointers
77
- map(consolidateAddressPointers)),
78
105
  // Create a loader for batching
79
106
  addressPointerLoadingSequence(
80
107
  // Step 1. load from cache if available
81
- cacheRequest ? cacheAddressPointersLoader(cacheRequest) : undefined,
108
+ opts?.cacheRequest ? cacheAddressPointersLoader(opts.cacheRequest) : undefined,
82
109
  // Step 2. load from relay hints on pointers
83
110
  opts?.followRelayHints !== false ? relayHintsAddressPointersLoader(request) : undefined,
84
111
  // Step 3. load from extra relays
@@ -3,8 +3,11 @@ import { NostrEvent } from "nostr-tools";
3
3
  import { EventPointer } from "nostr-tools/nip19";
4
4
  import { Observable } from "rxjs";
5
5
  import { CacheRequest, NostrRequest, UpstreamPool } from "../types.js";
6
- export type EventPointerLoader = (pointer: EventPointer) => Observable<NostrEvent>;
7
- export type createEventLoader = (pointers: EventPointer[]) => Observable<NostrEvent>;
6
+ export type LoadableEventPointer = EventPointer & {
7
+ cache?: boolean;
8
+ };
9
+ export type EventPointerLoader = (pointer: LoadableEventPointer) => Observable<NostrEvent>;
10
+ export type createEventLoader = (pointers: LoadableEventPointer[]) => Observable<NostrEvent>;
8
11
  /** Creates a loader that gets a single event from the cache */
9
12
  export declare function cacheEventsLoader(request: CacheRequest): createEventLoader;
10
13
  /** Creates a loader that gets an array of events from a list of relays */
@@ -1,24 +1,23 @@
1
1
  import { mapEventsToStore } from "applesauce-core";
2
- import { bufferTime, catchError, EMPTY, filter, isObservable, map, merge, of, pipe, switchMap, take, tap, } from "rxjs";
3
- import { makeCacheRequest, wrapCacheRequest } from "../helpers/cache.js";
2
+ import { bufferTime, catchError, EMPTY, merge, tap } from "rxjs";
3
+ import { makeCacheRequest } from "../helpers/cache.js";
4
4
  import { consolidateEventPointers } from "../helpers/event-pointer.js";
5
- import { batchLoader } from "../helpers/loaders.js";
5
+ import { batchLoader, unwrap } from "../helpers/loaders.js";
6
6
  import { groupByRelay } from "../helpers/pointer.js";
7
- import { wrapGeneratorFunction } from "../operators/generator.js";
8
7
  import { wrapUpstreamPool } from "../helpers/upstream.js";
8
+ import { wrapGeneratorFunction } from "../operators/generator.js";
9
9
  /** Creates a loader that gets a single event from the cache */
10
10
  export function cacheEventsLoader(request) {
11
- return (pointers) => makeCacheRequest(request, [{ ids: pointers.map((p) => p.id) }]);
11
+ return (pointers) => {
12
+ pointers = pointers.filter((p) => p.cache !== false);
13
+ if (pointers.length === 0)
14
+ return EMPTY;
15
+ return makeCacheRequest(request, [{ ids: pointers.map((p) => p.id) }]);
16
+ };
12
17
  }
13
18
  /** Creates a loader that gets an array of events from a list of relays */
14
19
  export function relaysEventsLoader(request, relays) {
15
- return (pointers) =>
16
- // unwrap relays observable
17
- (isObservable(relays) ? relays : of(relays)).pipe(
18
- // take the first value
19
- take(1),
20
- // Request events from relays
21
- switchMap((relays) => request(relays, [{ ids: pointers.map((p) => p.id) }])));
20
+ return (pointers) => unwrap(relays, (relays) => request(relays, [{ ids: pointers.map((p) => p.id) }]));
22
21
  }
23
22
  /** Creates a loader that gets an array of events from a single relay */
24
23
  export function relayEventsLoader(request, relay) {
@@ -46,6 +45,11 @@ export function relayHintsEventsLoader(request, upstream = relayEventsLoader) {
46
45
  /** Creates a loader that tries to load events from a list of loaders in order */
47
46
  export function eventLoadingSequence(...loaders) {
48
47
  return wrapGeneratorFunction(function* (pointers) {
48
+ // Filter out invalid pointers and consolidate
49
+ pointers = consolidateEventPointers(pointers);
50
+ // Skip if there are no pointers
51
+ if (pointers.length === 0)
52
+ return;
49
53
  const found = new Set();
50
54
  let remaining = Array.from(pointers);
51
55
  for (const loader of loaders) {
@@ -67,20 +71,14 @@ export function eventLoadingSequence(...loaders) {
67
71
  /** Create a pre-built address pointer loader that supports batching, caching, and lookup relays */
68
72
  export function createEventLoader(pool, opts) {
69
73
  const request = wrapUpstreamPool(pool);
70
- const cacheRequest = opts?.cacheRequest ? wrapCacheRequest(opts.cacheRequest) : undefined;
71
74
  return batchLoader(
72
75
  // Create batching sequence
73
- pipe(
74
76
  // buffer requests by time or size
75
77
  bufferTime(opts?.bufferTime ?? 1000, undefined, opts?.bufferSize ?? 200),
76
- // Ingore empty buffers
77
- filter((b) => b.length > 0),
78
- // consolidate buffered pointers
79
- map(consolidateEventPointers)),
80
78
  // Create a loader for batching
81
79
  eventLoadingSequence(
82
80
  // Step 1. load from cache if available
83
- cacheRequest ? cacheEventsLoader(cacheRequest) : undefined,
81
+ opts?.cacheRequest ? cacheEventsLoader(opts.cacheRequest) : undefined,
84
82
  // Step 2. load from relay hints on pointers
85
83
  opts?.followRelayHints !== false ? relayHintsEventsLoader(request) : undefined,
86
84
  // Step 3. load from extra relays
@@ -24,7 +24,7 @@ export type TagValueLoaderOptions = {
24
24
  /** Method used to load from the cache */
25
25
  cacheRequest?: CacheRequest;
26
26
  /** An array of relays to always fetch from */
27
- extraRelays?: string[];
27
+ extraRelays?: string[] | Observable<string[]>;
28
28
  /** An event store used to deduplicate events */
29
29
  eventStore?: IEventStore;
30
30
  };
@@ -1,10 +1,9 @@
1
1
  import { mapEventsToStore } from "applesauce-core";
2
2
  import { mergeRelaySets } from "applesauce-core/helpers";
3
- import { bufferTime, filter, merge, pipe } from "rxjs";
3
+ import { bufferTime, EMPTY, merge } from "rxjs";
4
4
  import { unique } from "../helpers/array.js";
5
- import { makeCacheRequest, wrapCacheRequest } from "../helpers/cache.js";
6
- import { batchLoader } from "../helpers/loaders.js";
7
- import { distinctRelaysBatch } from "../operators/distinct-relays.js";
5
+ import { makeCacheRequest } from "../helpers/cache.js";
6
+ import { batchLoader, unwrap } from "../helpers/loaders.js";
8
7
  import { wrapUpstreamPool } from "../helpers/upstream.js";
9
8
  /** Creates a loader that gets tag values from the cache */
10
9
  export function cacheTagValueLoader(request, tagName, opts) {
@@ -24,7 +23,7 @@ export function cacheTagValueLoader(request, tagName, opts) {
24
23
  /** Creates a loader that gets tag values from relays */
25
24
  export function relaysTagValueLoader(request, tagName, opts) {
26
25
  const filterTag = `#${tagName}`;
27
- return (pointers) => {
26
+ return (pointers) => unwrap(opts?.extraRelays, (extraRelays) => {
28
27
  const baseFilter = {};
29
28
  if (opts?.kinds)
30
29
  baseFilter.kinds = opts.kinds;
@@ -34,7 +33,7 @@ export function relaysTagValueLoader(request, tagName, opts) {
34
33
  baseFilter.since = opts.since;
35
34
  // build request map for relays
36
35
  const requestMap = pointers.reduce((map, pointer) => {
37
- const relays = mergeRelaySets(pointer.relays, opts?.extraRelays);
36
+ const relays = mergeRelaySets(pointer.relays, extraRelays);
38
37
  for (const relay of relays) {
39
38
  if (!map[relay]) {
40
39
  // create new filter for relay
@@ -49,27 +48,26 @@ export function relaysTagValueLoader(request, tagName, opts) {
49
48
  }, {});
50
49
  const requests = Object.entries(requestMap).map(([relay, filter]) => request([relay], [filter]));
51
50
  return merge(...requests);
52
- };
51
+ });
53
52
  }
54
53
  /** Create a pre-built tag value loader that supports batching, caching, and relay hints */
55
54
  export function createTagValueLoader(pool, tagName, opts) {
56
55
  const request = wrapUpstreamPool(pool);
57
- const cacheRequest = opts?.cacheRequest ? wrapCacheRequest(opts.cacheRequest) : undefined;
58
56
  return batchLoader(
59
- // Create batching sequence
60
- pipe(
61
57
  // buffer requests by time or size
62
58
  bufferTime(opts?.bufferTime ?? 1000, undefined, opts?.bufferSize ?? 200),
63
- // Ignore empty buffers
64
- filter((b) => b.length > 0),
65
- // Only request from each relay once
66
- distinctRelaysBatch((m) => m.value)),
67
59
  // Create a loader for batching
68
- (pointers) => merge(
69
- // Step 1. load from cache if available
70
- cacheRequest ? cacheTagValueLoader(cacheRequest, tagName, opts)(pointers) : [],
71
- // Step 2. load from relays
72
- relaysTagValueLoader(request, tagName, opts)(pointers)),
60
+ (pointers) => {
61
+ // Skip if there are no pointers
62
+ if (pointers.length === 0)
63
+ return EMPTY;
64
+ // Load from cache and relays in parallel
65
+ return merge(
66
+ // load from cache if available
67
+ opts?.cacheRequest ? cacheTagValueLoader(opts.cacheRequest, tagName, opts)(pointers) : [],
68
+ // load from relays
69
+ relaysTagValueLoader(request, tagName, opts)(pointers));
70
+ },
73
71
  // Filter results based on requests
74
72
  (pointer, event) => event.tags.some((tag) => tag[0] === tagName && tag[1] === pointer.value),
75
73
  // Pass all events through the store if defined
@@ -15,7 +15,7 @@ export type UserListsLoaderOptions = Partial<{
15
15
  /** A method used to load events from a local cache */
16
16
  cacheRequest?: CacheRequest;
17
17
  /** An array of extra relay to load from */
18
- extraRelays?: string[];
18
+ extraRelays?: string[] | Observable<string[]>;
19
19
  /** An event store used to deduplicate events */
20
20
  eventStore: IEventStore;
21
21
  }>;
@@ -2,7 +2,8 @@ import { mapEventsToStore } from "applesauce-core";
2
2
  import { mergeRelaySets } from "applesauce-core/helpers";
3
3
  import { kinds } from "nostr-tools";
4
4
  import { EMPTY, identity, merge } from "rxjs";
5
- import { makeCacheRequest, wrapCacheRequest } from "../helpers/cache.js";
5
+ import { makeCacheRequest } from "../helpers/cache.js";
6
+ import { unwrap } from "../helpers/loaders.js";
6
7
  import { wrapUpstreamPool } from "../helpers/upstream.js";
7
8
  /** A list of NIP-51 list kinds that most clients will use */
8
9
  export const COMMON_LIST_KINDS = [kinds.Contacts, kinds.Mutelist, kinds.Pinlist, kinds.BookmarkList];
@@ -14,20 +15,19 @@ export const COMMON_SET_KINDS = [kinds.Bookmarksets, kinds.Followsets];
14
15
  */
15
16
  export function createUserListsLoader(pool, opts) {
16
17
  const request = wrapUpstreamPool(pool);
17
- const cacheRequest = opts?.cacheRequest ? wrapCacheRequest(opts.cacheRequest) : undefined;
18
- return (user) => {
18
+ return (user) => unwrap(opts?.extraRelays, (extraRelays) => {
19
19
  const filter = {
20
20
  kinds: opts?.kinds || [...COMMON_LIST_KINDS, ...COMMON_SET_KINDS],
21
21
  authors: [user.pubkey],
22
22
  };
23
23
  // Merge extra relays with user relays
24
- const relays = opts?.extraRelays ? mergeRelaySets(user.relays, opts.extraRelays) : user.relays;
24
+ const relays = mergeRelaySets(user.relays, extraRelays);
25
25
  return merge(
26
26
  // Load from cache
27
- cacheRequest ? makeCacheRequest(cacheRequest, [filter]) : EMPTY,
27
+ opts?.cacheRequest ? makeCacheRequest(opts.cacheRequest, [filter]) : EMPTY,
28
28
  // Load from relays
29
29
  relays ? request(relays, [filter]) : EMPTY).pipe(
30
30
  // If event store is set, deduplicate events
31
31
  opts?.eventStore ? mapEventsToStore(opts.eventStore) : identity);
32
- };
32
+ });
33
33
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-loaders",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "description": "A collection of observable based loaders built on rx-nostr",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -53,14 +53,14 @@
53
53
  }
54
54
  },
55
55
  "dependencies": {
56
- "applesauce-core": "^2.0.0",
56
+ "applesauce-core": "^3.0.0",
57
57
  "nanoid": "^5.0.9",
58
58
  "nostr-tools": "^2.13",
59
59
  "rxjs": "^7.8.1"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@hirez_io/observer-spy": "^2.2.0",
63
- "applesauce-signers": "^2.0.0",
63
+ "applesauce-signers": "^3.0.0",
64
64
  "typescript": "^5.8.3",
65
65
  "vitest": "^3.2.3",
66
66
  "vitest-nostr": "^0.4.1",