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.
- package/README.md +204 -57
- package/dist/helpers/address-pointer.d.ts +2 -2
- package/dist/helpers/address-pointer.js +11 -13
- package/dist/helpers/cache.d.ts +9 -0
- package/dist/helpers/cache.js +22 -0
- package/dist/helpers/event-pointer.js +10 -10
- package/dist/helpers/index.d.ts +5 -0
- package/dist/helpers/index.js +5 -0
- package/dist/helpers/loaders.d.ts +3 -0
- package/dist/helpers/loaders.js +27 -0
- package/dist/helpers/pointer.d.ts +2 -1
- package/dist/helpers/pointer.js +4 -2
- package/dist/helpers/upstream.d.ts +7 -0
- package/dist/helpers/upstream.js +13 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/loaders/address-loader.d.ts +37 -0
- package/dist/loaders/address-loader.js +94 -0
- package/dist/loaders/event-loader.d.ts +33 -0
- package/dist/loaders/event-loader.js +92 -0
- package/dist/loaders/index.d.ts +8 -9
- package/dist/loaders/index.js +8 -9
- package/dist/loaders/reactions-loader.d.ts +12 -0
- package/dist/loaders/reactions-loader.js +18 -0
- package/dist/loaders/social-graph.d.ts +21 -0
- package/dist/loaders/social-graph.js +50 -0
- package/dist/loaders/tag-value-loader.d.ts +19 -15
- package/dist/loaders/tag-value-loader.js +73 -71
- package/dist/loaders/timeline-loader.d.ts +24 -22
- package/dist/loaders/timeline-loader.js +49 -55
- package/dist/loaders/user-lists-loader.d.ts +26 -0
- package/dist/loaders/user-lists-loader.js +33 -0
- package/dist/loaders/zaps-loader.d.ts +12 -0
- package/dist/loaders/zaps-loader.js +18 -0
- package/dist/operators/complete-on-eose.d.ts +6 -0
- package/dist/operators/complete-on-eose.js +8 -0
- package/dist/operators/generator.d.ts +13 -0
- package/dist/operators/generator.js +72 -0
- package/dist/operators/index.d.ts +1 -1
- package/dist/operators/index.js +1 -1
- package/dist/types.d.ts +14 -0
- package/package.json +8 -8
- package/dist/helpers/__tests__/address-pointer.test.js +0 -19
- package/dist/helpers/rx-nostr.d.ts +0 -2
- package/dist/helpers/rx-nostr.js +0 -5
- package/dist/loaders/__tests__/dns-identity-loader.test.d.ts +0 -1
- package/dist/loaders/__tests__/dns-identity-loader.test.js +0 -59
- package/dist/loaders/__tests__/relay-timeline-loader.test.d.ts +0 -1
- package/dist/loaders/__tests__/relay-timeline-loader.test.js +0 -26
- package/dist/loaders/__tests__/request-loader.test.d.ts +0 -1
- package/dist/loaders/__tests__/request-loader.test.js +0 -37
- package/dist/loaders/cache-timeline-loader.d.ts +0 -22
- package/dist/loaders/cache-timeline-loader.js +0 -61
- package/dist/loaders/loader.d.ts +0 -20
- package/dist/loaders/loader.js +0 -22
- package/dist/loaders/relay-timeline-loader.d.ts +0 -24
- package/dist/loaders/relay-timeline-loader.js +0 -70
- package/dist/loaders/replaceable-loader.d.ts +0 -23
- package/dist/loaders/replaceable-loader.js +0 -106
- package/dist/loaders/request-loader.d.ts +0 -30
- package/dist/loaders/request-loader.js +0 -57
- package/dist/loaders/single-event-loader.d.ts +0 -26
- package/dist/loaders/single-event-loader.js +0 -76
- package/dist/loaders/user-sets-loader.d.ts +0 -31
- package/dist/loaders/user-sets-loader.js +0 -66
- package/dist/operators/__tests__/distinct-relays.test.d.ts +0 -1
- package/dist/operators/__tests__/distinct-relays.test.js +0 -75
- package/dist/operators/__tests__/generator-sequence.test.d.ts +0 -1
- package/dist/operators/__tests__/generator-sequence.test.js +0 -38
- package/dist/operators/generator-sequence.d.ts +0 -3
- package/dist/operators/generator-sequence.js +0 -53
- /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
|
+
}
|
package/dist/loaders/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/loaders/index.js
CHANGED
|
@@ -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 {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
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
|
-
/**
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
/** A
|
|
12
|
-
export declare
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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;
|