applesauce-loaders 1.0.0 → 2.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.
Files changed (66) hide show
  1. package/README.md +204 -65
  2. package/dist/helpers/address-pointer.d.ts +2 -2
  3. package/dist/helpers/address-pointer.js +8 -10
  4. package/dist/helpers/cache.d.ts +9 -0
  5. package/dist/helpers/cache.js +22 -0
  6. package/dist/helpers/event-pointer.js +10 -10
  7. package/dist/helpers/index.d.ts +5 -0
  8. package/dist/helpers/index.js +5 -0
  9. package/dist/helpers/loaders.d.ts +3 -0
  10. package/dist/helpers/loaders.js +27 -0
  11. package/dist/helpers/pointer.d.ts +1 -0
  12. package/dist/helpers/pointer.js +1 -0
  13. package/dist/helpers/upstream.d.ts +7 -0
  14. package/dist/helpers/upstream.js +13 -0
  15. package/dist/index.d.ts +2 -1
  16. package/dist/index.js +2 -1
  17. package/dist/loaders/address-loader.d.ts +37 -0
  18. package/dist/loaders/address-loader.js +94 -0
  19. package/dist/loaders/event-loader.d.ts +33 -0
  20. package/dist/loaders/event-loader.js +92 -0
  21. package/dist/loaders/index.d.ts +8 -8
  22. package/dist/loaders/index.js +8 -8
  23. package/dist/loaders/reactions-loader.d.ts +12 -0
  24. package/dist/loaders/reactions-loader.js +18 -0
  25. package/dist/loaders/social-graph.d.ts +21 -0
  26. package/dist/loaders/social-graph.js +50 -0
  27. package/dist/loaders/tag-value-loader.d.ts +16 -18
  28. package/dist/loaders/tag-value-loader.js +72 -71
  29. package/dist/loaders/timeline-loader.d.ts +23 -21
  30. package/dist/loaders/timeline-loader.js +49 -55
  31. package/dist/loaders/user-lists-loader.d.ts +26 -0
  32. package/dist/loaders/user-lists-loader.js +33 -0
  33. package/dist/loaders/zaps-loader.d.ts +12 -0
  34. package/dist/loaders/zaps-loader.js +18 -0
  35. package/dist/operators/complete-on-eose.d.ts +4 -1
  36. package/dist/operators/complete-on-eose.js +4 -1
  37. package/dist/operators/generator.d.ts +13 -0
  38. package/dist/operators/generator.js +72 -0
  39. package/dist/operators/index.d.ts +1 -1
  40. package/dist/operators/index.js +1 -1
  41. package/dist/types.d.ts +14 -0
  42. package/package.json +6 -4
  43. package/dist/helpers/__tests__/address-pointer.test.js +0 -19
  44. package/dist/loaders/__tests__/dns-identity-loader.test.d.ts +0 -1
  45. package/dist/loaders/__tests__/dns-identity-loader.test.js +0 -59
  46. package/dist/loaders/__tests__/relay-timeline-loader.test.d.ts +0 -1
  47. package/dist/loaders/__tests__/relay-timeline-loader.test.js +0 -26
  48. package/dist/loaders/cache-timeline-loader.d.ts +0 -22
  49. package/dist/loaders/cache-timeline-loader.js +0 -61
  50. package/dist/loaders/loader.d.ts +0 -22
  51. package/dist/loaders/loader.js +0 -22
  52. package/dist/loaders/relay-timeline-loader.d.ts +0 -23
  53. package/dist/loaders/relay-timeline-loader.js +0 -71
  54. package/dist/loaders/replaceable-loader.d.ts +0 -27
  55. package/dist/loaders/replaceable-loader.js +0 -104
  56. package/dist/loaders/single-event-loader.d.ts +0 -32
  57. package/dist/loaders/single-event-loader.js +0 -74
  58. package/dist/loaders/user-sets-loader.d.ts +0 -33
  59. package/dist/loaders/user-sets-loader.js +0 -59
  60. package/dist/operators/__tests__/distinct-relays.test.d.ts +0 -1
  61. package/dist/operators/__tests__/distinct-relays.test.js +0 -75
  62. package/dist/operators/__tests__/generator-sequence.test.d.ts +0 -1
  63. package/dist/operators/__tests__/generator-sequence.test.js +0 -38
  64. package/dist/operators/generator-sequence.d.ts +0 -3
  65. package/dist/operators/generator-sequence.js +0 -53
  66. /package/dist/{helpers/__tests__/address-pointer.test.d.ts → types.js} +0 -0
@@ -0,0 +1,33 @@
1
+ import { IEventStore } from "applesauce-core";
2
+ import { NostrEvent } from "nostr-tools";
3
+ import { EventPointer } from "nostr-tools/nip19";
4
+ import { Observable } from "rxjs";
5
+ import { CacheRequest, NostrRequest, UpstreamPool } from "../types.js";
6
+ export type EventPointerLoader = (pointer: EventPointer) => Observable<NostrEvent>;
7
+ export type createEventLoader = (pointers: EventPointer[]) => Observable<NostrEvent>;
8
+ /** Creates a loader that gets a single event from the cache */
9
+ export declare function cacheEventsLoader(request: CacheRequest): createEventLoader;
10
+ /** Creates a loader that gets an array of events from a list of relays */
11
+ export declare function relaysEventsLoader(request: NostrRequest, relays: string[] | Observable<string[]>): createEventLoader;
12
+ /** Creates a loader that gets an array of events from a single relay */
13
+ export declare function relayEventsLoader(request: NostrRequest, relay: string): createEventLoader;
14
+ /** Creates a loader that creates a new loader for each relay hint and uses them to load events */
15
+ export declare function relayHintsEventsLoader(request: NostrRequest, upstream?: (request: NostrRequest, relay: string) => createEventLoader): createEventLoader;
16
+ /** Creates a loader that tries to load events from a list of loaders in order */
17
+ export declare function eventLoadingSequence(...loaders: (createEventLoader | undefined)[]): createEventLoader;
18
+ export type EventPointerLoaderOptions = Partial<{
19
+ /** Time interval to buffer requests in ms ( default 1000 ) */
20
+ bufferTime: number;
21
+ /** Max buffer size ( default 200 ) */
22
+ bufferSize: number;
23
+ /** An event store used to deduplicate events */
24
+ eventStore: IEventStore;
25
+ /** A method used to load events from a local cache */
26
+ cacheRequest: CacheRequest;
27
+ /** Whether to follow relay hints ( default true ) */
28
+ followRelayHints: boolean;
29
+ /** An array of relays to always fetch from */
30
+ extraRelays: string[] | Observable<string[]>;
31
+ }>;
32
+ /** Create a pre-built address pointer loader that supports batching, caching, and lookup relays */
33
+ export declare function createEventLoader(pool: UpstreamPool, opts?: EventPointerLoaderOptions): EventPointerLoader;
@@ -0,0 +1,92 @@
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";
4
+ import { consolidateEventPointers } from "../helpers/event-pointer.js";
5
+ import { batchLoader } from "../helpers/loaders.js";
6
+ import { groupByRelay } from "../helpers/pointer.js";
7
+ import { wrapGeneratorFunction } from "../operators/generator.js";
8
+ import { wrapUpstreamPool } from "../helpers/upstream.js";
9
+ /** Creates a loader that gets a single event from the cache */
10
+ export function cacheEventsLoader(request) {
11
+ return (pointers) => makeCacheRequest(request, [{ ids: pointers.map((p) => p.id) }]);
12
+ }
13
+ /** Creates a loader that gets an array of events from a list of relays */
14
+ 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) }])));
22
+ }
23
+ /** Creates a loader that gets an array of events from a single relay */
24
+ export function relayEventsLoader(request, relay) {
25
+ return relaysEventsLoader(request, [relay]);
26
+ }
27
+ /** Creates a loader that creates a new loader for each relay hint and uses them to load events */
28
+ export function relayHintsEventsLoader(request, upstream = relayEventsLoader) {
29
+ const loaders = new Map();
30
+ // Get or create a new loader for each relay
31
+ const getLoader = (relay) => {
32
+ let loader = loaders.get(relay);
33
+ if (!loader) {
34
+ loader = upstream(request, relay);
35
+ loaders.set(relay, loader);
36
+ }
37
+ return loader;
38
+ };
39
+ return (pointers) => {
40
+ // Group pointers by thier relay hints
41
+ const byRelay = groupByRelay(pointers);
42
+ // Request the pointers from each relay
43
+ return merge(...Array.from(byRelay).map(([relay, pointers]) => getLoader(relay)(pointers)));
44
+ };
45
+ }
46
+ /** Creates a loader that tries to load events from a list of loaders in order */
47
+ export function eventLoadingSequence(...loaders) {
48
+ return wrapGeneratorFunction(function* (pointers) {
49
+ const found = new Set();
50
+ let remaining = Array.from(pointers);
51
+ for (const loader of loaders) {
52
+ if (loader === undefined)
53
+ continue;
54
+ yield loader(remaining).pipe(
55
+ // If the loader throws an error, skip it
56
+ catchError(() => EMPTY),
57
+ // Record events that where found
58
+ tap((e) => found.add(e.id)));
59
+ // Remove poniters that where found
60
+ remaining = remaining.filter((p) => !found.has(p.id));
61
+ // If there are no remaining pointers, complete
62
+ if (remaining.length === 0)
63
+ return;
64
+ }
65
+ });
66
+ }
67
+ /** Create a pre-built address pointer loader that supports batching, caching, and lookup relays */
68
+ export function createEventLoader(pool, opts) {
69
+ const request = wrapUpstreamPool(pool);
70
+ const cacheRequest = opts?.cacheRequest ? wrapCacheRequest(opts.cacheRequest) : undefined;
71
+ return batchLoader(
72
+ // Create batching sequence
73
+ pipe(
74
+ // buffer requests by time or size
75
+ 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
+ // Create a loader for batching
81
+ eventLoadingSequence(
82
+ // Step 1. load from cache if available
83
+ cacheRequest ? cacheEventsLoader(cacheRequest) : undefined,
84
+ // Step 2. load from relay hints on pointers
85
+ opts?.followRelayHints !== false ? relayHintsEventsLoader(request) : undefined,
86
+ // Step 3. load from extra relays
87
+ opts?.extraRelays ? relaysEventsLoader(request, opts.extraRelays) : undefined),
88
+ // Filter resutls based on requests
89
+ (pointer, event) => event.id === pointer.id,
90
+ // Pass all events through the store if defined
91
+ opts?.eventStore && mapEventsToStore(opts.eventStore));
92
+ }
@@ -1,9 +1,9 @@
1
- export * from "./loader.js";
2
- export * from "./replaceable-loader.js";
3
- export * from "./single-event-loader.js";
4
- export * from "./user-sets-loader.js";
5
- export * from "./timeline-loader.js";
6
- export * from "./relay-timeline-loader.js";
7
- export * from "./cache-timeline-loader.js";
8
- export * from "./tag-value-loader.js";
1
+ export * from "./address-loader.js";
9
2
  export * from "./dns-identity-loader.js";
3
+ export * from "./event-loader.js";
4
+ export * from "./reactions-loader.js";
5
+ export * from "./social-graph.js";
6
+ export * from "./tag-value-loader.js";
7
+ export * from "./timeline-loader.js";
8
+ export * from "./user-lists-loader.js";
9
+ export * from "./zaps-loader.js";
@@ -1,9 +1,9 @@
1
- export * from "./loader.js";
2
- export * from "./replaceable-loader.js";
3
- export * from "./single-event-loader.js";
4
- export * from "./user-sets-loader.js";
5
- export * from "./timeline-loader.js";
6
- export * from "./relay-timeline-loader.js";
7
- export * from "./cache-timeline-loader.js";
8
- export * from "./tag-value-loader.js";
1
+ export * from "./address-loader.js";
9
2
  export * from "./dns-identity-loader.js";
3
+ export * from "./event-loader.js";
4
+ export * from "./reactions-loader.js";
5
+ export * from "./social-graph.js";
6
+ export * from "./tag-value-loader.js";
7
+ export * from "./timeline-loader.js";
8
+ export * from "./user-lists-loader.js";
9
+ export * from "./zaps-loader.js";
@@ -0,0 +1,12 @@
1
+ import { NostrEvent } from "nostr-tools";
2
+ import { Observable } from "rxjs";
3
+ import { UpstreamPool } from "../types.js";
4
+ import { TagValueLoaderOptions } from "./tag-value-loader.js";
5
+ /** A loader that takes an event and returns zaps */
6
+ export type ReactionsLoader = (event: NostrEvent, relays?: string[]) => Observable<NostrEvent>;
7
+ export type ReactionsLoaderOptions = Omit<TagValueLoaderOptions, "kinds"> & {
8
+ /** Whether to request reactions from the relays the event was seen on ( default true ) */
9
+ useSeenRelays?: boolean;
10
+ };
11
+ /** Creates a loader that loads reaction events for a given event */
12
+ export declare function createReactionsLoader(pool: UpstreamPool, opts?: ReactionsLoaderOptions): ReactionsLoader;
@@ -0,0 +1,18 @@
1
+ import { getReplaceableAddress, getSeenRelays, isReplaceable, mergeRelaySets } from "applesauce-core/helpers";
2
+ import { kinds } from "nostr-tools";
3
+ import { wrapUpstreamPool } from "../helpers/upstream.js";
4
+ import { createTagValueLoader } from "./tag-value-loader.js";
5
+ /** Creates a loader that loads reaction events for a given event */
6
+ export function createReactionsLoader(pool, opts) {
7
+ const request = wrapUpstreamPool(pool);
8
+ const eventLoader = createTagValueLoader(request, "e", { ...opts, kinds: [kinds.Reaction] });
9
+ const addressableLoader = createTagValueLoader(request, "a", { ...opts, kinds: [kinds.Reaction] });
10
+ // Return diffrent loaders depending on if the event is addressable
11
+ return (event, relays) => {
12
+ if (opts?.useSeenRelays ?? true)
13
+ relays = mergeRelaySets(relays, getSeenRelays(event));
14
+ return isReplaceable(event.kind)
15
+ ? addressableLoader({ value: getReplaceableAddress(event), relays })
16
+ : eventLoader({ value: event.id, relays });
17
+ };
18
+ }
@@ -0,0 +1,21 @@
1
+ import { IEventStore } from "applesauce-core";
2
+ import { NostrEvent } from "nostr-tools";
3
+ import { ProfilePointer } from "nostr-tools/nip19";
4
+ import { Observable } from "rxjs";
5
+ import { AddressPointerLoader } from "./address-loader.js";
6
+ /** A loader that loads the social graph of a user out to a set distance */
7
+ export type SocialGraphLoader = (user: ProfilePointer & {
8
+ distance: number;
9
+ }) => Observable<NostrEvent>;
10
+ export type SocialGraphLoaderOptions = Partial<{
11
+ /** An event store to send all the events to */
12
+ eventStore: IEventStore;
13
+ /** The number of parallel requests to make (default 300) */
14
+ parallel: number;
15
+ /** Extra relays to load from */
16
+ extraRelays?: string[] | Observable<string[]>;
17
+ /** Whether to follow relay hints in contact events */
18
+ hints?: boolean;
19
+ }>;
20
+ /** Create a social graph loader */
21
+ export declare function createSocialGraphLoader(addressLoader: AddressPointerLoader, opts?: SocialGraphLoaderOptions): SocialGraphLoader;
@@ -0,0 +1,50 @@
1
+ import { mapEventsToStore } from "applesauce-core";
2
+ import { getProfilePointersFromList, mergeRelaySets } from "applesauce-core/helpers";
3
+ import { kinds } from "nostr-tools";
4
+ import { firstValueFrom, identity, isObservable, lastValueFrom, toArray } from "rxjs";
5
+ import { wrapGeneratorFunction } from "../operators/generator.js";
6
+ /** Create a social graph loader */
7
+ export function createSocialGraphLoader(addressLoader, opts) {
8
+ return wrapGeneratorFunction(async function* (user) {
9
+ const seen = new Set();
10
+ const queue = [user];
11
+ // Maximum parallel requests (default to 300)
12
+ const maxParallel = opts?.parallel ?? 300;
13
+ // get the relays to load from
14
+ const relays = mergeRelaySets(user.relays, isObservable(opts?.extraRelays) ? await firstValueFrom(opts?.extraRelays) : opts?.extraRelays);
15
+ // Keep loading while the queue has items
16
+ while (queue.length > 0) {
17
+ // Process up to maxParallel items at once
18
+ const batch = queue.splice(0, maxParallel);
19
+ const promises = batch.map(async (pointer) => {
20
+ const address = {
21
+ kind: kinds.Contacts,
22
+ pubkey: pointer.pubkey,
23
+ relays: opts?.hints ? mergeRelaySets(pointer.relays, relays) : relays,
24
+ };
25
+ // load the contacts events
26
+ const events = await lastValueFrom(addressLoader(address).pipe(
27
+ // Pass all events to the store if set
28
+ opts?.eventStore ? mapEventsToStore(opts.eventStore) : identity,
29
+ // Conver to array
30
+ toArray()));
31
+ if (events.length === 0)
32
+ return;
33
+ const contacts = getProfilePointersFromList(events[events.length - 1]);
34
+ // if the distance is greater than 0, add the contacts to the queue
35
+ if (pointer.distance > 0) {
36
+ for (const contact of contacts) {
37
+ // Dont add any contacts that have already been seen
38
+ if (seen.has(contact.pubkey))
39
+ continue;
40
+ seen.add(contact.pubkey);
41
+ // Add to queue
42
+ queue.push({ ...contact, distance: pointer.distance - 1 });
43
+ }
44
+ }
45
+ });
46
+ // Wait for all parallel operations to complete
47
+ await Promise.all(promises);
48
+ }
49
+ });
50
+ }
@@ -1,7 +1,8 @@
1
- import { logger } from "applesauce-core";
1
+ import { IEventStore } from "applesauce-core";
2
2
  import { NostrEvent } from "nostr-tools";
3
- import { CacheRequest, Loader, NostrRequest } from "./loader.js";
4
- export type TabValuePointer = {
3
+ import { Observable } from "rxjs";
4
+ import { CacheRequest, NostrRequest, UpstreamPool } from "../types.js";
5
+ export type TagValuePointer = {
5
6
  /** The value of the tag to load */
6
7
  value: string;
7
8
  /** The relays to load from */
@@ -10,13 +11,10 @@ export type TabValuePointer = {
10
11
  force?: boolean;
11
12
  };
12
13
  export type TagValueLoaderOptions = {
13
- /** the name of this loader (for debugging) */
14
- name?: string;
15
- /**
16
- * Time interval to buffer requests in ms
17
- * @default 1000
18
- */
14
+ /** Time interval to buffer requests in ms ( default 1000 ) */
19
15
  bufferTime?: number;
16
+ /** Max buffer size ( default 200 ) */
17
+ bufferSize?: number;
20
18
  /** Restrict queries to specific kinds */
21
19
  kinds?: number[];
22
20
  /** Restrict queries to specific authors */
@@ -27,13 +25,13 @@ export type TagValueLoaderOptions = {
27
25
  cacheRequest?: CacheRequest;
28
26
  /** An array of relays to always fetch from */
29
27
  extraRelays?: string[];
28
+ /** An event store used to deduplicate events */
29
+ eventStore?: IEventStore;
30
30
  };
31
- export declare class TagValueLoader extends Loader<TabValuePointer, NostrEvent> {
32
- name: string;
33
- protected log: typeof logger;
34
- /** A method to load events from a local cache */
35
- cacheRequest?: CacheRequest;
36
- /** An array of relays to always fetch from */
37
- extraRelays?: string[];
38
- constructor(request: NostrRequest, tagName: string, opts?: TagValueLoaderOptions);
39
- }
31
+ export type TagValueLoader = (pointer: TagValuePointer) => Observable<NostrEvent>;
32
+ /** Creates a loader that gets tag values from the cache */
33
+ export declare function cacheTagValueLoader(request: CacheRequest, tagName: string, opts?: TagValueLoaderOptions): (pointers: TagValuePointer[]) => Observable<NostrEvent>;
34
+ /** Creates a loader that gets tag values from relays */
35
+ export declare function relaysTagValueLoader(request: NostrRequest, tagName: string, opts?: TagValueLoaderOptions): (pointers: TagValuePointer[]) => Observable<NostrEvent>;
36
+ /** Create a pre-built tag value loader that supports batching, caching, and relay hints */
37
+ export declare function createTagValueLoader(pool: UpstreamPool, tagName: string, opts?: TagValueLoaderOptions): TagValueLoader;
@@ -1,76 +1,77 @@
1
- import { logger } from "applesauce-core";
2
- import { markFromCache, mergeRelaySets } from "applesauce-core/helpers";
3
- import { bufferTime, filter, merge, mergeMap, tap } from "rxjs";
1
+ import { mapEventsToStore } from "applesauce-core";
2
+ import { mergeRelaySets } from "applesauce-core/helpers";
3
+ import { bufferTime, filter, merge, pipe } from "rxjs";
4
4
  import { unique } from "../helpers/array.js";
5
- import { completeOnEOSE } from "../operators/complete-on-eose.js";
5
+ import { makeCacheRequest, wrapCacheRequest } from "../helpers/cache.js";
6
+ import { batchLoader } from "../helpers/loaders.js";
6
7
  import { distinctRelaysBatch } from "../operators/distinct-relays.js";
7
- import { Loader } from "./loader.js";
8
- export class TagValueLoader extends Loader {
9
- name;
10
- log = logger.extend("TagValueLoader");
11
- /** A method to load events from a local cache */
12
- cacheRequest;
13
- /** An array of relays to always fetch from */
14
- extraRelays;
15
- constructor(request, tagName, opts) {
8
+ import { wrapUpstreamPool } from "../helpers/upstream.js";
9
+ /** Creates a loader that gets tag values from the cache */
10
+ export function cacheTagValueLoader(request, tagName, opts) {
11
+ return (pointers) => {
12
+ const baseFilter = {};
13
+ if (opts?.kinds)
14
+ baseFilter.kinds = opts.kinds;
15
+ if (opts?.authors)
16
+ baseFilter.authors = opts.authors;
17
+ if (opts?.since)
18
+ baseFilter.since = opts.since;
16
19
  const filterTag = `#${tagName}`;
17
- super((source) => source.pipe(
18
- // batch the pointers
19
- bufferTime(opts?.bufferTime ?? 1000),
20
- // filter out empty batches
21
- filter((pointers) => pointers.length > 0),
22
- // only request from each relay once
23
- distinctRelaysBatch((m) => m.value),
24
- // batch pointers into requests
25
- mergeMap((pointers) => {
26
- const baseFilter = {};
27
- if (opts?.kinds)
28
- baseFilter.kinds = opts.kinds;
29
- if (opts?.since)
30
- baseFilter.since = opts.since;
31
- if (opts?.authors)
32
- baseFilter.authors = opts.authors;
33
- // build request map for relays
34
- const requestMap = pointers.reduce((map, pointer) => {
35
- const relays = mergeRelaySets(pointer.relays, this.extraRelays);
36
- for (const relay of relays) {
37
- if (!map[relay]) {
38
- // create new filter for relay
39
- const filter = { ...baseFilter, [filterTag]: [pointer.value] };
40
- map[relay] = [filter];
41
- }
42
- else {
43
- // map for relay already exists, add the tag value
44
- const filter = map[relay][0];
45
- filter[filterTag].push(pointer.value);
46
- }
20
+ const filter = { ...baseFilter, [filterTag]: unique(pointers.map((p) => p.value)) };
21
+ return makeCacheRequest(request, [filter]);
22
+ };
23
+ }
24
+ /** Creates a loader that gets tag values from relays */
25
+ export function relaysTagValueLoader(request, tagName, opts) {
26
+ const filterTag = `#${tagName}`;
27
+ return (pointers) => {
28
+ const baseFilter = {};
29
+ if (opts?.kinds)
30
+ baseFilter.kinds = opts.kinds;
31
+ if (opts?.authors)
32
+ baseFilter.authors = opts.authors;
33
+ if (opts?.since)
34
+ baseFilter.since = opts.since;
35
+ // build request map for relays
36
+ const requestMap = pointers.reduce((map, pointer) => {
37
+ const relays = mergeRelaySets(pointer.relays, opts?.extraRelays);
38
+ for (const relay of relays) {
39
+ if (!map[relay]) {
40
+ // create new filter for relay
41
+ map[relay] = { ...baseFilter, [filterTag]: [pointer.value] };
42
+ }
43
+ else {
44
+ // map for relay already exists, add the tag value
45
+ map[relay][filterTag].push(pointer.value);
47
46
  }
48
- return map;
49
- }, {});
50
- let fromCache = 0;
51
- const cacheRequest = this?.cacheRequest?.([
52
- { ...baseFilter, [filterTag]: unique(pointers.map((p) => p.value)) },
53
- ]).pipe(
54
- // mark the event as from the cache
55
- tap({
56
- next: (event) => {
57
- markFromCache(event);
58
- fromCache++;
59
- },
60
- complete: () => {
61
- if (fromCache > 0)
62
- this.log(`Loaded ${fromCache} from cache`);
63
- },
64
- }));
65
- const requests = Object.entries(requestMap).map(([relay, filters]) => request([relay], filters).pipe(completeOnEOSE()));
66
- this.log(`Requesting ${pointers.length} tag values from ${requests.length} relays`);
67
- return cacheRequest ? merge(cacheRequest, ...requests) : merge(...requests);
68
- })));
69
- // Set options
70
- this.cacheRequest = opts?.cacheRequest;
71
- this.extraRelays = opts?.extraRelays;
72
- // create a unique logger for this instance
73
- this.name = opts?.name ?? "";
74
- this.log = this.log.extend(opts?.kinds ? `${this.name} ${filterTag} (${opts?.kinds?.join(",")})` : `${this.name} ${filterTag}`);
75
- }
47
+ }
48
+ return map;
49
+ }, {});
50
+ const requests = Object.entries(requestMap).map(([relay, filter]) => request([relay], [filter]));
51
+ return merge(...requests);
52
+ };
53
+ }
54
+ /** Create a pre-built tag value loader that supports batching, caching, and relay hints */
55
+ export function createTagValueLoader(pool, tagName, opts) {
56
+ const request = wrapUpstreamPool(pool);
57
+ const cacheRequest = opts?.cacheRequest ? wrapCacheRequest(opts.cacheRequest) : undefined;
58
+ return batchLoader(
59
+ // Create batching sequence
60
+ pipe(
61
+ // buffer requests by time or size
62
+ 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
+ // 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)),
73
+ // Filter results based on requests
74
+ (pointer, event) => event.tags.some((tag) => tag[0] === tagName && tag[1] === pointer.value),
75
+ // Pass all events through the store if defined
76
+ opts?.eventStore && mapEventsToStore(opts?.eventStore));
76
77
  }
@@ -1,22 +1,24 @@
1
- import { logger } from "applesauce-core";
1
+ import { IEventStore } from "applesauce-core";
2
2
  import { NostrEvent } from "nostr-tools";
3
- import { BehaviorSubject } from "rxjs";
4
- import { CacheTimelineLoader } from "./cache-timeline-loader.js";
5
- import { CacheRequest, Loader, NostrRequest, RelayFilterMap } from "./loader.js";
6
- import { RelayTimelineLoader, TimelessFilter } from "./relay-timeline-loader.js";
7
- export type TimelineLoaderOptions = {
8
- limit?: number;
9
- cacheRequest?: CacheRequest;
10
- };
11
- /** A multi-relay timeline loader that can be used to load a timeline from multiple relays */
12
- export declare class TimelineLoader extends Loader<number | undefined, NostrEvent> {
13
- id: string;
14
- loading$: BehaviorSubject<boolean>;
15
- get loading(): boolean;
16
- requests: RelayFilterMap<TimelessFilter>;
17
- protected log: typeof logger;
18
- protected cache?: CacheTimelineLoader;
19
- protected loaders: Map<string, RelayTimelineLoader>;
20
- constructor(request: NostrRequest, requests: RelayFilterMap<TimelessFilter>, opts?: TimelineLoaderOptions);
21
- static simpleFilterMap(relays: string[], filters: TimelessFilter[]): RelayFilterMap<TimelessFilter>;
22
- }
3
+ import { Observable } from "rxjs";
4
+ import { CacheRequest, FilterRequest, NostrRequest, TimelessFilter, UpstreamPool } from "../types.js";
5
+ /** A loader that optionally takes a timestamp to load till and returns a stream of events */
6
+ export type TimelineLoader = (since?: number) => Observable<NostrEvent>;
7
+ /** Common options for timeline loaders */
8
+ export type CommonTimelineLoaderOptions = Partial<{
9
+ limit: number;
10
+ }>;
11
+ /** A loader that loads blocks of events until none are returned or the since timestamp is reached */
12
+ export declare function filterBlockLoader(request: FilterRequest, filters: TimelessFilter[], opts?: CommonTimelineLoaderOptions): TimelineLoader;
13
+ /** Creates a loader that loads a timeline from a cache */
14
+ export declare function cacheTimelineLoader(request: CacheRequest, filters: TimelessFilter[], opts?: CommonTimelineLoaderOptions): TimelineLoader;
15
+ /** Creates a timeline loader that loads the same filters from multiple relays */
16
+ export declare function relaysTimelineLoader(request: NostrRequest, relays: string[], filters: TimelessFilter[], opts?: CommonTimelineLoaderOptions): TimelineLoader;
17
+ export type TimelineLoaderOptions = Partial<{
18
+ /** A method used to load the timeline from the cache */
19
+ cache: CacheRequest;
20
+ /** An event store to pass all the events to */
21
+ eventStore: IEventStore;
22
+ }> & CommonTimelineLoaderOptions;
23
+ /** A common timeline loader that takes an array of relays and a cache method */
24
+ export declare function createTimelineLoader(pool: UpstreamPool, relays: string[], filters: TimelessFilter[] | TimelessFilter, opts?: TimelineLoaderOptions): TimelineLoader;