applesauce-loaders 5.1.0 → 6.1.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.
|
@@ -7,8 +7,8 @@ export declare function createFiltersFromAddressPointers(pointers: AddressPointe
|
|
|
7
7
|
/** Checks if a relay will understand an address pointer */
|
|
8
8
|
export declare function isLoadableAddressPointer<T extends AddressPointerWithoutD>(pointer: T): boolean;
|
|
9
9
|
/** Group an array of address pointers by kind */
|
|
10
|
-
export declare function groupAddressPointersByKind(pointers:
|
|
10
|
+
export declare function groupAddressPointersByKind<T extends AddressPointerWithoutD>(pointers: T[]): Map<number, T[]>;
|
|
11
11
|
/** Group an array of address pointers by pubkey */
|
|
12
|
-
export declare function groupAddressPointersByPubkey(pointers:
|
|
12
|
+
export declare function groupAddressPointersByPubkey<T extends AddressPointerWithoutD>(pointers: T[]): Map<string, T[]>;
|
|
13
13
|
/** Groups address pointers by kind or pubkey depending on which is most optimal */
|
|
14
|
-
export declare function groupAddressPointersByPubkeyOrKind(pointers:
|
|
14
|
+
export declare function groupAddressPointersByPubkeyOrKind<T extends AddressPointerWithoutD>(pointers: T[]): Map<string, T[]> | Map<number, T[]>;
|
|
@@ -16,14 +16,14 @@ export function createFiltersFromAddressPointers(pointers) {
|
|
|
16
16
|
const replaceable = pointers.filter((p) => isReplaceableKind(p.kind));
|
|
17
17
|
const addressable = pointers.filter((p) => isAddressableKind(p.kind));
|
|
18
18
|
const filters = [];
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
const addGroupFilters = (group) => {
|
|
20
|
+
if (group.length === 0)
|
|
21
|
+
return;
|
|
22
|
+
const groups = groupAddressPointersByPubkeyOrKind(group);
|
|
21
23
|
filters.push(...Array.from(groups.values()).map(createFilterFromAddressPointers));
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
filters.push(...Array.from(groups.values()).map(createFilterFromAddressPointers));
|
|
26
|
-
}
|
|
24
|
+
};
|
|
25
|
+
addGroupFilters(replaceable);
|
|
26
|
+
addGroupFilters(addressable);
|
|
27
27
|
return filters;
|
|
28
28
|
}
|
|
29
29
|
/** Checks if a relay will understand an address pointer */
|
|
@@ -1,16 +1,28 @@
|
|
|
1
|
+
import { IAsyncEventStoreActions, IAsyncEventStoreRead, IEventStoreActions, IEventStoreRead } from "applesauce-core";
|
|
1
2
|
import { NostrEvent } from "applesauce-core/helpers/event";
|
|
2
3
|
import { ProfilePointer } from "applesauce-core/helpers/pointers";
|
|
3
|
-
import { mapEventsToStore } from "applesauce-core/observable";
|
|
4
4
|
import { Observable } from "rxjs";
|
|
5
|
-
import {
|
|
6
|
-
/**
|
|
5
|
+
import { CacheRequest, UpstreamPool } from "../types.js";
|
|
6
|
+
/**
|
|
7
|
+
* A loader that loads the social graph of a user out to a set distance.
|
|
8
|
+
*
|
|
9
|
+
* Pass `since` (unix seconds) to skip follow lists the relay already knows are older.
|
|
10
|
+
* When `since` is set and the relay returns nothing for a user, the loader falls back
|
|
11
|
+
* to `eventStore.getReplaceable(kinds.Contacts, pubkey)` so crawl expansion still
|
|
12
|
+
* happens from the cached copy.
|
|
13
|
+
*/
|
|
7
14
|
export type SocialGraphLoader = (user: ProfilePointer & {
|
|
8
15
|
distance: number;
|
|
16
|
+
since?: number;
|
|
9
17
|
}) => Observable<NostrEvent>;
|
|
18
|
+
/** An event store that the social graph loader can both write to and read from */
|
|
19
|
+
export type SocialGraphEventStore = (IEventStoreActions | IAsyncEventStoreActions) & (IEventStoreRead | IAsyncEventStoreRead);
|
|
10
20
|
export type SocialGraphLoaderOptions = Partial<{
|
|
11
|
-
/** An event store to send all the events to */
|
|
12
|
-
eventStore?:
|
|
13
|
-
/**
|
|
21
|
+
/** An event store to send all the events to and fall back to when the relay returns nothing */
|
|
22
|
+
eventStore?: SocialGraphEventStore;
|
|
23
|
+
/** A method used to load events from a local cache */
|
|
24
|
+
cacheRequest: CacheRequest;
|
|
25
|
+
/** The number of parallel contacts to load at once (default 300) */
|
|
14
26
|
parallel: number;
|
|
15
27
|
/** Extra relays to load from */
|
|
16
28
|
extraRelays?: string[] | Observable<string[]>;
|
|
@@ -18,4 +30,4 @@ export type SocialGraphLoaderOptions = Partial<{
|
|
|
18
30
|
hints?: boolean;
|
|
19
31
|
}>;
|
|
20
32
|
/** Create a social graph loader */
|
|
21
|
-
export declare function createSocialGraphLoader(
|
|
33
|
+
export declare function createSocialGraphLoader(pool: UpstreamPool, opts?: SocialGraphLoaderOptions): SocialGraphLoader;
|
|
@@ -2,50 +2,108 @@ import { getPublicContacts } from "applesauce-core/helpers";
|
|
|
2
2
|
import { kinds } from "applesauce-core/helpers/event";
|
|
3
3
|
import { mergeRelaySets } from "applesauce-core/helpers/relays";
|
|
4
4
|
import { mapEventsToStore } from "applesauce-core/observable";
|
|
5
|
-
import { firstValueFrom, identity, isObservable,
|
|
5
|
+
import { catchError, EMPTY, filter, firstValueFrom, identity, isObservable, tap } from "rxjs";
|
|
6
|
+
import { makeCacheRequest } from "../helpers/cache.js";
|
|
7
|
+
import { wrapUpstreamPool } from "../helpers/upstream.js";
|
|
6
8
|
import { wrapGeneratorFunction } from "../operators/generator.js";
|
|
9
|
+
/** Create filters for loading contact lists, keeping different since windows separate. */
|
|
10
|
+
function createContactsFilters(pointers) {
|
|
11
|
+
const bySince = new Map();
|
|
12
|
+
for (const pointer of pointers) {
|
|
13
|
+
const authors = bySince.get(pointer.since);
|
|
14
|
+
if (authors)
|
|
15
|
+
authors.push(pointer.pubkey);
|
|
16
|
+
else
|
|
17
|
+
bySince.set(pointer.since, [pointer.pubkey]);
|
|
18
|
+
}
|
|
19
|
+
return Array.from(bySince.entries()).map(([since, authors]) => {
|
|
20
|
+
const filter = { kinds: [kinds.Contacts], authors };
|
|
21
|
+
if (since !== undefined)
|
|
22
|
+
filter.since = since;
|
|
23
|
+
return filter;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function isRequestedContactsEvent(event, pointers) {
|
|
27
|
+
return event.kind === kinds.Contacts && pointers.some((pointer) => pointer.pubkey === event.pubkey);
|
|
28
|
+
}
|
|
29
|
+
function getBatchRelays(pointers, baseRelays, hints) {
|
|
30
|
+
if (hints)
|
|
31
|
+
return mergeRelaySets(baseRelays, ...pointers.map((pointer) => pointer.relays));
|
|
32
|
+
else
|
|
33
|
+
return baseRelays;
|
|
34
|
+
}
|
|
7
35
|
/** Create a social graph loader */
|
|
8
|
-
export function createSocialGraphLoader(
|
|
36
|
+
export function createSocialGraphLoader(pool, opts) {
|
|
37
|
+
const request = wrapUpstreamPool(pool);
|
|
9
38
|
return wrapGeneratorFunction(async function* (user) {
|
|
10
39
|
const seen = new Set();
|
|
40
|
+
// Carry `since` on every queue entry so descendants share the same window
|
|
11
41
|
const queue = [user];
|
|
12
42
|
// Maximum parallel requests (default to 300)
|
|
13
43
|
const maxParallel = opts?.parallel ?? 300;
|
|
14
44
|
// get the relays to load from
|
|
15
|
-
const
|
|
45
|
+
const baseRelays = mergeRelaySets(user.relays, isObservable(opts?.extraRelays) ? await firstValueFrom(opts?.extraRelays) : opts?.extraRelays);
|
|
16
46
|
// Keep loading while the queue has items
|
|
17
47
|
while (queue.length > 0) {
|
|
18
48
|
// Process up to maxParallel items at once
|
|
19
49
|
const batch = queue.splice(0, maxParallel);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
50
|
+
let remaining = batch;
|
|
51
|
+
// Track the latest contacts event per pubkey so we can expand the queue once
|
|
52
|
+
// the batch observable completes. Using a side-effect here lets us stream every
|
|
53
|
+
// event out to subscribers as it arrives rather than buffering to arrays.
|
|
54
|
+
const latestByPubkey = new Map();
|
|
55
|
+
const trackLatest = tap((event) => {
|
|
56
|
+
const current = latestByPubkey.get(event.pubkey);
|
|
57
|
+
if (!current || event.created_at > current.created_at)
|
|
58
|
+
latestByPubkey.set(event.pubkey, event);
|
|
59
|
+
});
|
|
60
|
+
if (opts?.cacheRequest) {
|
|
61
|
+
yield makeCacheRequest(opts.cacheRequest, createContactsFilters(batch)).pipe(filter((event) => isRequestedContactsEvent(event, batch)),
|
|
28
62
|
// Pass all events to the store if set
|
|
29
63
|
opts?.eventStore ? mapEventsToStore(opts.eventStore) : identity,
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
64
|
+
// Remember the newest contacts event per pubkey for queue expansion
|
|
65
|
+
trackLatest,
|
|
66
|
+
// If the cache throws an error, skip it
|
|
67
|
+
catchError(() => EMPTY));
|
|
68
|
+
remaining = batch.filter((pointer) => pointer.since !== undefined || !latestByPubkey.has(pointer.pubkey));
|
|
69
|
+
}
|
|
70
|
+
const relays = getBatchRelays(remaining, baseRelays, opts?.hints);
|
|
71
|
+
if (remaining.length > 0 && relays.length > 0) {
|
|
72
|
+
// Yield the relay observable so every event streams out to subscribers
|
|
73
|
+
// as it arrives from the relay.
|
|
74
|
+
yield request(relays, createContactsFilters(remaining)).pipe(filter((event) => isRequestedContactsEvent(event, remaining)),
|
|
75
|
+
// Pass all events to the store if set
|
|
76
|
+
opts?.eventStore ? mapEventsToStore(opts.eventStore) : identity,
|
|
77
|
+
// Remember the newest contacts event per pubkey for queue expansion
|
|
78
|
+
trackLatest,
|
|
79
|
+
// If the relay request throws an error, continue expanding from cache/store
|
|
80
|
+
catchError(() => EMPTY));
|
|
81
|
+
}
|
|
82
|
+
// Batch has completed — expand the queue using the latest contacts event
|
|
83
|
+
// for each pointer, falling back to the event store if the relay returned
|
|
84
|
+
// nothing (typically because `since` let it skip).
|
|
85
|
+
for (const pointer of batch) {
|
|
86
|
+
let latest = latestByPubkey.get(pointer.pubkey);
|
|
87
|
+
if (!latest && opts?.eventStore) {
|
|
88
|
+
const cached = await opts.eventStore.getReplaceable(kinds.Contacts, pointer.pubkey);
|
|
89
|
+
if (cached)
|
|
90
|
+
latest = cached;
|
|
91
|
+
}
|
|
92
|
+
if (!latest)
|
|
93
|
+
continue;
|
|
35
94
|
// if the distance is greater than 0, add the contacts to the queue
|
|
36
95
|
if (pointer.distance > 0) {
|
|
96
|
+
const contacts = getPublicContacts(latest);
|
|
37
97
|
for (const contact of contacts) {
|
|
38
98
|
// Dont add any contacts that have already been seen
|
|
39
99
|
if (seen.has(contact.pubkey))
|
|
40
100
|
continue;
|
|
41
101
|
seen.add(contact.pubkey);
|
|
42
|
-
//
|
|
43
|
-
queue.push({ ...contact, distance: pointer.distance - 1 });
|
|
102
|
+
// Forward `since` onto descendants so the whole crawl shares the window
|
|
103
|
+
queue.push({ ...contact, distance: pointer.distance - 1, since: pointer.since });
|
|
44
104
|
}
|
|
45
105
|
}
|
|
46
|
-
}
|
|
47
|
-
// Wait for all parallel operations to complete
|
|
48
|
-
await Promise.all(promises);
|
|
106
|
+
}
|
|
49
107
|
}
|
|
50
108
|
});
|
|
51
109
|
}
|
|
@@ -42,13 +42,18 @@ export function loadBackwardBlocks(request, opts) {
|
|
|
42
42
|
loading = true;
|
|
43
43
|
// Count returned events so complete set
|
|
44
44
|
let count = 0;
|
|
45
|
+
let minCreatedAt;
|
|
46
|
+
const until = cursor;
|
|
45
47
|
log?.(`Loading block since:${cursor}`);
|
|
46
48
|
// Request the next block of events
|
|
47
|
-
return request(
|
|
49
|
+
return request(until).pipe(tap((event) => {
|
|
48
50
|
count++;
|
|
49
|
-
|
|
50
|
-
cursor = Math.min(event.created_at, cursor ?? Infinity);
|
|
51
|
+
minCreatedAt = Math.min(event.created_at, minCreatedAt ?? Infinity);
|
|
51
52
|
}), finalize(() => {
|
|
53
|
+
// NIP-01 defines `until` as inclusive. Move past the oldest event in
|
|
54
|
+
// the block so the next request does not repeat the boundary second.
|
|
55
|
+
if (minCreatedAt !== undefined)
|
|
56
|
+
cursor = minCreatedAt - 1;
|
|
52
57
|
loading = false;
|
|
53
58
|
complete = count === 0;
|
|
54
59
|
log?.(`Found ${count} events`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "applesauce-loaders",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.1.0",
|
|
4
4
|
"description": "A collection of observable based loaders built on rx-nostr",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -52,13 +52,13 @@
|
|
|
52
52
|
}
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"applesauce-core": "^
|
|
55
|
+
"applesauce-core": "^6.1.0",
|
|
56
56
|
"nanoid": "^5.0.9",
|
|
57
57
|
"rxjs": "^7.8.1"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@hirez_io/observer-spy": "^2.2.0",
|
|
61
|
-
"applesauce-signers": "^
|
|
61
|
+
"applesauce-signers": "^6.0.0",
|
|
62
62
|
"rimraf": "^6.0.1",
|
|
63
63
|
"typescript": "^5.8.3",
|
|
64
64
|
"vitest": "^4.0.15",
|