applesauce-loaders 0.12.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 (72) hide show
  1. package/README.md +204 -57
  2. package/dist/helpers/address-pointer.d.ts +2 -2
  3. package/dist/helpers/address-pointer.js +11 -13
  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 +2 -1
  12. package/dist/helpers/pointer.js +4 -2
  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 -9
  22. package/dist/loaders/index.js +8 -9
  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 +19 -15
  28. package/dist/loaders/tag-value-loader.js +73 -71
  29. package/dist/loaders/timeline-loader.d.ts +24 -22
  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 +6 -0
  36. package/dist/operators/complete-on-eose.js +8 -0
  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 +8 -8
  43. package/dist/helpers/__tests__/address-pointer.test.js +0 -19
  44. package/dist/helpers/rx-nostr.d.ts +0 -2
  45. package/dist/helpers/rx-nostr.js +0 -5
  46. package/dist/loaders/__tests__/dns-identity-loader.test.d.ts +0 -1
  47. package/dist/loaders/__tests__/dns-identity-loader.test.js +0 -59
  48. package/dist/loaders/__tests__/relay-timeline-loader.test.d.ts +0 -1
  49. package/dist/loaders/__tests__/relay-timeline-loader.test.js +0 -26
  50. package/dist/loaders/__tests__/request-loader.test.d.ts +0 -1
  51. package/dist/loaders/__tests__/request-loader.test.js +0 -37
  52. package/dist/loaders/cache-timeline-loader.d.ts +0 -22
  53. package/dist/loaders/cache-timeline-loader.js +0 -61
  54. package/dist/loaders/loader.d.ts +0 -20
  55. package/dist/loaders/loader.js +0 -22
  56. package/dist/loaders/relay-timeline-loader.d.ts +0 -24
  57. package/dist/loaders/relay-timeline-loader.js +0 -70
  58. package/dist/loaders/replaceable-loader.d.ts +0 -23
  59. package/dist/loaders/replaceable-loader.js +0 -106
  60. package/dist/loaders/request-loader.d.ts +0 -30
  61. package/dist/loaders/request-loader.js +0 -57
  62. package/dist/loaders/single-event-loader.d.ts +0 -26
  63. package/dist/loaders/single-event-loader.js +0 -76
  64. package/dist/loaders/user-sets-loader.d.ts +0 -31
  65. package/dist/loaders/user-sets-loader.js +0 -66
  66. package/dist/operators/__tests__/distinct-relays.test.d.ts +0 -1
  67. package/dist/operators/__tests__/distinct-relays.test.js +0 -75
  68. package/dist/operators/__tests__/generator-sequence.test.d.ts +0 -1
  69. package/dist/operators/__tests__/generator-sequence.test.js +0 -38
  70. package/dist/operators/generator-sequence.d.ts +0 -3
  71. package/dist/operators/generator-sequence.js +0 -53
  72. /package/dist/{helpers/__tests__/address-pointer.test.d.ts → types.js} +0 -0
@@ -0,0 +1,94 @@
1
+ import { mapEventsToStore } from "applesauce-core";
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";
8
+ import { wrapUpstreamPool } from "../helpers/upstream.js";
9
+ /**
10
+ * Loads address pointers from an async cache
11
+ * @note ignores pointers with force=true
12
+ */
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)));
17
+ }
18
+ /** Loads address pointers from the relay hints */
19
+ export function relayHintsAddressPointersLoader(request) {
20
+ return (pointers) => {
21
+ const relays = mergeRelaySets(...pointers.map((p) => p.relays));
22
+ if (relays.length === 0)
23
+ return EMPTY;
24
+ const filters = createFiltersFromAddressPointers(pointers);
25
+ return request(relays, filters);
26
+ };
27
+ }
28
+ /** Loads address pointers from an array of relays */
29
+ 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) => {
37
+ if (relays.length === 0)
38
+ return EMPTY;
39
+ const filters = createFiltersFromAddressPointers(pointers);
40
+ return request(relays, filters);
41
+ }));
42
+ }
43
+ /** Creates a loader that loads all event pointers based on their relays */
44
+ export function addressPointerLoadingSequence(...loaders) {
45
+ return wrapGeneratorFunction(function* (pointers) {
46
+ let remaining = Array.from(pointers);
47
+ for (const loader of loaders) {
48
+ if (loader === undefined)
49
+ continue;
50
+ const results = yield loader(remaining).pipe(
51
+ // If the loader throws an error, skip it
52
+ catchError(() => EMPTY));
53
+ // Get set of addresses loaded
54
+ const addresses = new Set(results.filter((e) => isReplaceable(e.kind)).map((event) => getReplaceableAddress(event)));
55
+ // Remove the pointers that were loaded
56
+ remaining = remaining.filter((p) => !addresses.has(createReplaceableAddress(p.kind, p.pubkey, p.identifier)) || p.force === true);
57
+ // If there are no remaining pointers, complete
58
+ if (remaining.length === 0)
59
+ return;
60
+ }
61
+ });
62
+ }
63
+ /** Create a pre-built address pointer loader that supports batching, caching, and lookup relays */
64
+ export function createAddressLoader(pool, opts) {
65
+ const request = wrapUpstreamPool(pool);
66
+ const cacheRequest = opts?.cacheRequest ? wrapCacheRequest(opts.cacheRequest) : undefined;
67
+ return batchLoader(
68
+ // Create batching sequence
69
+ pipe(
70
+ // filter out invalid pointers
71
+ filter(isLoadableAddressPointer),
72
+ // buffer requests by time or size
73
+ 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
+ // Create a loader for batching
79
+ addressPointerLoadingSequence(
80
+ // Step 1. load from cache if available
81
+ cacheRequest ? cacheAddressPointersLoader(cacheRequest) : undefined,
82
+ // Step 2. load from relay hints on pointers
83
+ opts?.followRelayHints !== false ? relayHintsAddressPointersLoader(request) : undefined,
84
+ // Step 3. load from extra relays
85
+ opts?.extraRelays ? relaysAddressPointersLoader(request, opts.extraRelays) : undefined,
86
+ // Step 4. load from lookup relays
87
+ opts?.lookupRelays ? relaysAddressPointersLoader(request, opts.lookupRelays) : undefined),
88
+ // Filter resutls based on requests
89
+ (pointer, event) => event.kind === pointer.kind &&
90
+ event.pubkey === pointer.pubkey &&
91
+ (pointer.identifier ? getReplaceableIdentifier(event) === pointer.identifier : true),
92
+ // Pass all events through the store if defined
93
+ opts?.eventStore && mapEventsToStore(opts.eventStore));
94
+ }
@@ -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,10 +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";
9
- export * from "./request-loader.js";
1
+ export * from "./address-loader.js";
10
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,10 +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";
9
- export * from "./request-loader.js";
1
+ export * from "./address-loader.js";
10
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 { EventPacket, RxNostr } from "rx-nostr";
2
- import { logger } from "applesauce-core";
3
- import { CacheRequest, Loader } from "./loader.js";
4
- export type TabValuePointer = {
1
+ import { IEventStore } from "applesauce-core";
2
+ import { NostrEvent } from "nostr-tools";
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 */
@@ -25,9 +23,15 @@ export type TagValueLoaderOptions = {
25
23
  since?: number;
26
24
  /** Method used to load from the cache */
27
25
  cacheRequest?: CacheRequest;
26
+ /** An array of relays to always fetch from */
27
+ extraRelays?: string[];
28
+ /** An event store used to deduplicate events */
29
+ eventStore?: IEventStore;
28
30
  };
29
- export declare class TagValueLoader extends Loader<TabValuePointer, EventPacket> {
30
- name: string;
31
- protected log: typeof logger;
32
- constructor(rxNostr: RxNostr, tagName: string, opts?: TagValueLoaderOptions);
33
- }
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,75 +1,77 @@
1
- import { createRxOneshotReq } from "rx-nostr";
2
- import { bufferTime, filter, map, merge, mergeMap, tap } from "rxjs";
3
- import { markFromCache } from "applesauce-core/helpers";
4
- import { logger } from "applesauce-core";
5
- import { Loader } from "./loader.js";
6
- import { distinctRelaysBatch } from "../operators/distinct-relays.js";
7
- import { getDefaultReadRelays } from "../helpers/rx-nostr.js";
1
+ import { mapEventsToStore } from "applesauce-core";
2
+ import { mergeRelaySets } from "applesauce-core/helpers";
3
+ import { bufferTime, filter, merge, pipe } from "rxjs";
8
4
  import { unique } from "../helpers/array.js";
9
- export class TagValueLoader extends Loader {
10
- name;
11
- log = logger.extend("TagValueLoader");
12
- constructor(rxNostr, tagName, opts) {
5
+ import { makeCacheRequest, wrapCacheRequest } from "../helpers/cache.js";
6
+ import { batchLoader } from "../helpers/loaders.js";
7
+ import { distinctRelaysBatch } from "../operators/distinct-relays.js";
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;
13
19
  const filterTag = `#${tagName}`;
14
- super((source) => source.pipe(
15
- // batch the pointers
16
- bufferTime(opts?.bufferTime ?? 1000),
17
- // filter out empty batches
18
- filter((pointers) => pointers.length > 0),
19
- // only request from each relay once
20
- distinctRelaysBatch((m) => m.value),
21
- // batch pointers into requests
22
- mergeMap((pointers) => {
23
- const baseFilter = {};
24
- if (opts?.kinds)
25
- baseFilter.kinds = opts.kinds;
26
- if (opts?.since)
27
- baseFilter.since = opts.since;
28
- if (opts?.authors)
29
- baseFilter.authors = opts.authors;
30
- // build request map for relays
31
- const requestMap = pointers.reduce((map, pointer) => {
32
- const relays = pointer.relays ?? getDefaultReadRelays(rxNostr);
33
- for (const relay of relays) {
34
- if (!map[relay]) {
35
- // create new filter for relay
36
- const filter = { ...baseFilter, [filterTag]: [pointer.value] };
37
- map[relay] = [filter];
38
- }
39
- else {
40
- // map for relay already exists, add the tag value
41
- const filter = map[relay][0];
42
- filter[filterTag].push(pointer.value);
43
- }
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] };
44
42
  }
45
- return map;
46
- }, {});
47
- let fromCache = 0;
48
- const cacheRequest = opts
49
- ?.cacheRequest?.([{ ...baseFilter, [filterTag]: unique(pointers.map((p) => p.value)) }])
50
- .pipe(
51
- // mark the event as from the cache
52
- tap({
53
- next: (event) => {
54
- markFromCache(event);
55
- fromCache++;
56
- },
57
- complete: () => {
58
- if (fromCache > 0)
59
- this.log(`Loaded ${fromCache} from cache`);
60
- },
61
- }),
62
- // convert to event packets
63
- map((e) => ({ event: e, from: "", subId: "replaceable-loader", type: "EVENT" })));
64
- const requests = Object.entries(requestMap).map(([relay, filters]) => {
65
- const req = createRxOneshotReq({ filters });
66
- return rxNostr.use(req, { on: { relays: [relay] } });
67
- });
68
- this.log(`Requesting ${pointers.length} tag values from ${requests.length} relays`);
69
- return cacheRequest ? merge(cacheRequest, ...requests) : merge(...requests);
70
- })));
71
- // create a unique logger for this instance
72
- this.name = opts?.name ?? "";
73
- this.log = this.log.extend(opts?.kinds ? `${this.name} ${filterTag} (${opts?.kinds?.join(",")})` : `${this.name} ${filterTag}`);
74
- }
43
+ else {
44
+ // map for relay already exists, add the tag value
45
+ map[relay][filterTag].push(pointer.value);
46
+ }
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));
75
77
  }
@@ -1,22 +1,24 @@
1
- import { EventPacket, RxNostr } from "rx-nostr";
2
- import { BehaviorSubject } from "rxjs";
3
- import { logger } from "applesauce-core";
4
- import { RelayTimelineLoader, TimelessFilter } from "./relay-timeline-loader.js";
5
- import { CacheRequest, Loader, RelayFilterMap } from "./loader.js";
6
- import { CacheTimelineLoader } from "./cache-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, EventPacket> {
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(rxNostr: RxNostr, requests: RelayFilterMap<TimelessFilter>, opts?: TimelineLoaderOptions);
21
- static simpleFilterMap(relays: string[], filters: TimelessFilter[]): RelayFilterMap<TimelessFilter>;
22
- }
1
+ import { IEventStore } from "applesauce-core";
2
+ import { NostrEvent } from "nostr-tools";
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;