applesauce-loaders 0.10.0 → 0.12.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/dist/helpers/__tests__/address-pointer.test.d.ts +1 -0
- package/dist/helpers/__tests__/address-pointer.test.js +19 -0
- package/dist/helpers/address-pointer.d.ts +15 -2
- package/dist/helpers/address-pointer.js +48 -4
- package/dist/helpers/array.d.ts +0 -2
- package/dist/helpers/array.js +0 -17
- package/dist/helpers/dns-identity.d.ts +40 -0
- package/dist/helpers/dns-identity.js +50 -0
- package/dist/helpers/event-pointer.d.ts +5 -0
- package/dist/helpers/event-pointer.js +19 -0
- package/dist/helpers/index.d.ts +1 -0
- package/dist/helpers/index.js +1 -0
- package/dist/helpers/pointer.d.ts +11 -0
- package/dist/helpers/pointer.js +47 -0
- package/dist/helpers/rx-nostr.d.ts +2 -0
- package/dist/helpers/rx-nostr.js +5 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/loaders/__tests__/dns-identity-loader.test.d.ts +1 -0
- package/dist/loaders/__tests__/dns-identity-loader.test.js +59 -0
- package/dist/loaders/__tests__/relay-timeline-loader.test.d.ts +1 -0
- package/dist/loaders/__tests__/relay-timeline-loader.test.js +26 -0
- package/dist/loaders/__tests__/request-loader.test.d.ts +1 -0
- package/dist/loaders/__tests__/request-loader.test.js +37 -0
- package/dist/loaders/cache-timeline-loader.d.ts +22 -0
- package/dist/loaders/cache-timeline-loader.js +61 -0
- package/dist/loaders/dns-identity-loader.d.ts +25 -0
- package/dist/loaders/dns-identity-loader.js +66 -0
- package/dist/loaders/index.d.ts +8 -1
- package/dist/loaders/index.js +8 -1
- package/dist/loaders/loader.d.ts +14 -8
- package/dist/loaders/loader.js +7 -2
- package/dist/loaders/relay-timeline-loader.d.ts +24 -0
- package/dist/loaders/relay-timeline-loader.js +70 -0
- package/dist/loaders/replaceable-loader.d.ts +7 -15
- package/dist/loaders/replaceable-loader.js +49 -106
- package/dist/loaders/request-loader.d.ts +30 -0
- package/dist/loaders/request-loader.js +57 -0
- package/dist/loaders/single-event-loader.d.ts +26 -0
- package/dist/loaders/single-event-loader.js +76 -0
- package/dist/loaders/tag-value-loader.d.ts +33 -0
- package/dist/loaders/tag-value-loader.js +75 -0
- package/dist/loaders/timeline-loader.d.ts +22 -0
- package/dist/loaders/timeline-loader.js +56 -0
- package/dist/loaders/user-sets-loader.d.ts +31 -0
- package/dist/loaders/user-sets-loader.js +66 -0
- package/dist/operators/__tests__/distinct-relays.test.d.ts +1 -0
- package/dist/operators/__tests__/distinct-relays.test.js +75 -0
- package/dist/operators/__tests__/generator-sequence.test.d.ts +1 -0
- package/dist/operators/__tests__/generator-sequence.test.js +38 -0
- package/dist/operators/distinct-relays.d.ts +4 -0
- package/dist/operators/distinct-relays.js +14 -0
- package/dist/operators/distinct-timeout.d.ts +3 -0
- package/dist/operators/distinct-timeout.js +15 -0
- package/dist/operators/generator-sequence.d.ts +1 -1
- package/dist/operators/generator-sequence.js +42 -34
- package/dist/operators/index.d.ts +2 -3
- package/dist/operators/index.js +2 -3
- package/package.json +28 -8
- package/dist/loaders/single-relay-replaceable-loader.d.ts +0 -14
- package/dist/loaders/single-relay-replaceable-loader.js +0 -51
- package/dist/operators/address-pointers-request.d.ts +0 -5
- package/dist/operators/address-pointers-request.js +0 -25
- package/dist/operators/max-filters.d.ts +0 -4
- package/dist/operators/max-filters.js +0 -8
- package/dist/operators/relay-request.d.ts +0 -4
- package/dist/operators/relay-request.js +0 -9
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { unixNow } from "applesauce-core/helpers";
|
|
2
|
+
import { getIdentitiesFromJson, IdentityStatus, normalizeIdentityJson, } from "../helpers/dns-identity.js";
|
|
3
|
+
export class DnsIdentityLoader {
|
|
4
|
+
cache;
|
|
5
|
+
identities = new Map();
|
|
6
|
+
/** The fetch implementation this class should use */
|
|
7
|
+
fetch = fetch;
|
|
8
|
+
/** How long an identity should be kept until its considered expired (in seconds) defaults to 1 week */
|
|
9
|
+
expiration = 60 * 60 * 24 * 7;
|
|
10
|
+
constructor(cache) {
|
|
11
|
+
this.cache = cache;
|
|
12
|
+
}
|
|
13
|
+
/** Makes an http request to fetch an identity */
|
|
14
|
+
async fetchIdentity(name, domain) {
|
|
15
|
+
const { fetch } = this;
|
|
16
|
+
const checked = unixNow();
|
|
17
|
+
try {
|
|
18
|
+
const res = await fetch(`https://${domain}/.well-known/nostr.json?name=${name}`, { redirect: "manual" });
|
|
19
|
+
if (res.status !== 200)
|
|
20
|
+
throw Error("Wrong response code");
|
|
21
|
+
const json = await res.json().then(normalizeIdentityJson);
|
|
22
|
+
const identities = getIdentitiesFromJson(domain, json, checked);
|
|
23
|
+
// save all identities to cache
|
|
24
|
+
if (this.cache && Object.values(identities).length > 0)
|
|
25
|
+
this.cache.save(identities);
|
|
26
|
+
for (const [address, identity] of Object.entries(identities))
|
|
27
|
+
this.identities.set(address, identity);
|
|
28
|
+
return identities[name + "@" + domain] || { name, domain, checked, status: IdentityStatus.Missing };
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error instanceof Error)
|
|
32
|
+
return { name, domain, checked, status: IdentityStatus.Error, error: error.message };
|
|
33
|
+
else
|
|
34
|
+
return { name, domain, checked, status: IdentityStatus.Error, error: "Unknown" };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Loads an identity from the cache or fetches it */
|
|
38
|
+
async loadIdentity(name, domain) {
|
|
39
|
+
let identity;
|
|
40
|
+
if (this.cache)
|
|
41
|
+
identity = await this.cache.load(name + "@" + domain);
|
|
42
|
+
// fetch the identity if its not in cache, or if its expired
|
|
43
|
+
if (!identity || unixNow() - identity.checked > this.expiration)
|
|
44
|
+
return await this.fetchIdentity(name, domain);
|
|
45
|
+
else
|
|
46
|
+
return identity;
|
|
47
|
+
}
|
|
48
|
+
requesting = new Map();
|
|
49
|
+
/** Requests an identity to be loaded */
|
|
50
|
+
requestIdentity(name, domain) {
|
|
51
|
+
const key = name + "@" + domain;
|
|
52
|
+
let existing = this.identities.get(key);
|
|
53
|
+
if (existing)
|
|
54
|
+
return Promise.resolve(existing);
|
|
55
|
+
let ongoing = this.requesting.get(key);
|
|
56
|
+
if (!ongoing) {
|
|
57
|
+
ongoing = this.loadIdentity(name, domain);
|
|
58
|
+
this.requesting.set(key, ongoing);
|
|
59
|
+
}
|
|
60
|
+
return ongoing;
|
|
61
|
+
}
|
|
62
|
+
/** Checks if an identity is loaded */
|
|
63
|
+
getIdentity(name, domain) {
|
|
64
|
+
return this.identities.get(name + "@" + domain);
|
|
65
|
+
}
|
|
66
|
+
}
|
package/dist/loaders/index.d.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
export * from "./loader.js";
|
|
2
2
|
export * from "./replaceable-loader.js";
|
|
3
|
-
export * from "./single-
|
|
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";
|
|
10
|
+
export * from "./dns-identity-loader.js";
|
package/dist/loaders/index.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
export * from "./loader.js";
|
|
2
2
|
export * from "./replaceable-loader.js";
|
|
3
|
-
export * from "./single-
|
|
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";
|
|
10
|
+
export * from "./dns-identity-loader.js";
|
package/dist/loaders/loader.d.ts
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Filter, NostrEvent } from "nostr-tools";
|
|
2
|
+
import { InteropObservable, Observable, OperatorFunction, Subject, Subscribable } from "rxjs";
|
|
3
|
+
export type RelayFilterMap<T = Filter> = {
|
|
4
|
+
[relay: string]: T[];
|
|
5
|
+
};
|
|
6
|
+
export type CacheRequest = (filters: Filter[]) => Observable<NostrEvent>;
|
|
2
7
|
export interface ILoader<T, R> extends Subscribable<R> {
|
|
3
8
|
next: (value: T) => void;
|
|
4
9
|
pipe: Observable<R>["pipe"];
|
|
5
10
|
}
|
|
6
11
|
/** Base loader class */
|
|
7
|
-
export declare class Loader<
|
|
8
|
-
protected subject: Subject<
|
|
9
|
-
|
|
10
|
-
pipe: Observable<
|
|
11
|
-
subscribe: Observable<
|
|
12
|
-
constructor(transform: OperatorFunction<
|
|
13
|
-
next(value:
|
|
12
|
+
export declare class Loader<Input, Output> implements ILoader<Input, Output>, InteropObservable<Output> {
|
|
13
|
+
protected subject: Subject<Input>;
|
|
14
|
+
observable: Observable<Output>;
|
|
15
|
+
pipe: Observable<Output>["pipe"];
|
|
16
|
+
subscribe: Observable<Output>["subscribe"];
|
|
17
|
+
constructor(transform: OperatorFunction<Input, Output>);
|
|
18
|
+
next(value: Input): void;
|
|
19
|
+
[Symbol.observable](): Observable<Output>;
|
|
14
20
|
}
|
package/dist/loaders/loader.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Subject } from "rxjs";
|
|
1
|
+
import { share, Subject } from "rxjs";
|
|
2
2
|
/** Base loader class */
|
|
3
3
|
export class Loader {
|
|
4
4
|
subject = new Subject();
|
|
@@ -6,7 +6,9 @@ export class Loader {
|
|
|
6
6
|
pipe;
|
|
7
7
|
subscribe;
|
|
8
8
|
constructor(transform) {
|
|
9
|
-
this.observable = this.subject.pipe(transform
|
|
9
|
+
this.observable = this.subject.pipe(transform,
|
|
10
|
+
// only create a single instance of the transformer
|
|
11
|
+
share());
|
|
10
12
|
// copy pipe function
|
|
11
13
|
this.pipe = this.observable.pipe.bind(this.observable);
|
|
12
14
|
this.subscribe = this.observable.subscribe.bind(this.observable);
|
|
@@ -14,4 +16,7 @@ export class Loader {
|
|
|
14
16
|
next(value) {
|
|
15
17
|
this.subject.next(value);
|
|
16
18
|
}
|
|
19
|
+
[Symbol.observable]() {
|
|
20
|
+
return this.observable;
|
|
21
|
+
}
|
|
17
22
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { EventPacket, RxNostr } from "rx-nostr";
|
|
2
|
+
import { BehaviorSubject } from "rxjs";
|
|
3
|
+
import { logger } from "applesauce-core";
|
|
4
|
+
import { Filter } from "nostr-tools";
|
|
5
|
+
import { Loader } from "./loader.js";
|
|
6
|
+
export type TimelessFilter = Omit<Filter, "since" | "until">;
|
|
7
|
+
export type RelayTimelineLoaderOptions = {
|
|
8
|
+
/** default number of events to request in each batch */
|
|
9
|
+
limit?: number;
|
|
10
|
+
};
|
|
11
|
+
/** A loader that can be used to load a timeline in chunks */
|
|
12
|
+
export declare class RelayTimelineLoader extends Loader<number | void, EventPacket> {
|
|
13
|
+
relay: string;
|
|
14
|
+
filters: TimelessFilter[];
|
|
15
|
+
id: string;
|
|
16
|
+
loading$: BehaviorSubject<boolean>;
|
|
17
|
+
get loading(): boolean;
|
|
18
|
+
/** current "until" timestamp */
|
|
19
|
+
cursor: number;
|
|
20
|
+
/** if the timeline is complete */
|
|
21
|
+
complete: boolean;
|
|
22
|
+
protected log: typeof logger;
|
|
23
|
+
constructor(rxNostr: RxNostr, relay: string, filters: TimelessFilter[], opts?: RelayTimelineLoaderOptions);
|
|
24
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { createRxOneshotReq } from "rx-nostr";
|
|
2
|
+
import { BehaviorSubject, filter, map, Observable } from "rxjs";
|
|
3
|
+
import { logger } from "applesauce-core";
|
|
4
|
+
import { nanoid } from "nanoid";
|
|
5
|
+
import { unixNow } from "applesauce-core/helpers";
|
|
6
|
+
import { Loader } from "./loader.js";
|
|
7
|
+
/** A loader that can be used to load a timeline in chunks */
|
|
8
|
+
export class RelayTimelineLoader extends Loader {
|
|
9
|
+
relay;
|
|
10
|
+
filters;
|
|
11
|
+
id = nanoid(8);
|
|
12
|
+
loading$ = new BehaviorSubject(false);
|
|
13
|
+
get loading() {
|
|
14
|
+
return this.loading$.value;
|
|
15
|
+
}
|
|
16
|
+
/** current "until" timestamp */
|
|
17
|
+
cursor = Infinity;
|
|
18
|
+
/** if the timeline is complete */
|
|
19
|
+
complete = false;
|
|
20
|
+
log = logger.extend("RelayTimelineLoader");
|
|
21
|
+
constructor(rxNostr, relay, filters, opts) {
|
|
22
|
+
super((source) => new Observable((observer) => {
|
|
23
|
+
return source
|
|
24
|
+
.pipe(filter(() => !this.loading && !this.complete), map((limit) => {
|
|
25
|
+
// build next batch filters
|
|
26
|
+
return filters.map((filter) => ({
|
|
27
|
+
limit: limit || opts?.limit,
|
|
28
|
+
...filter,
|
|
29
|
+
// limit curser to now
|
|
30
|
+
until: Math.min(unixNow(), this.cursor),
|
|
31
|
+
}));
|
|
32
|
+
}),
|
|
33
|
+
// ignore empty filters
|
|
34
|
+
filter((filters) => filters.length > 0))
|
|
35
|
+
.subscribe((filters) => {
|
|
36
|
+
// make batch request
|
|
37
|
+
let count = 0;
|
|
38
|
+
const req = createRxOneshotReq({ filters, rxReqId: this.id });
|
|
39
|
+
this.loading$.next(true);
|
|
40
|
+
this.log(`Next batch starting at ${filters[0].until} limit ${filters[0].limit}`);
|
|
41
|
+
rxNostr.use(req, { on: { relays: [relay] } }).subscribe({
|
|
42
|
+
next: (packet) => {
|
|
43
|
+
// update cursor when event is received
|
|
44
|
+
this.cursor = Math.min(packet.event.created_at - 1, this.cursor);
|
|
45
|
+
count++;
|
|
46
|
+
// forward packet
|
|
47
|
+
observer.next(packet);
|
|
48
|
+
},
|
|
49
|
+
error: (err) => observer.error(err),
|
|
50
|
+
complete: () => {
|
|
51
|
+
// set loading to false when batch completes
|
|
52
|
+
this.loading$.next(false);
|
|
53
|
+
// set complete the observable if 0 events where returned
|
|
54
|
+
if (count === 0) {
|
|
55
|
+
observer.complete();
|
|
56
|
+
this.log(`Got ${count} event, Complete`);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
this.log(`Finished batch, got ${count} events`);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}));
|
|
65
|
+
this.relay = relay;
|
|
66
|
+
this.filters = filters;
|
|
67
|
+
// create a unique logger for this instance
|
|
68
|
+
this.log = this.log.extend(relay);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -1,31 +1,23 @@
|
|
|
1
|
-
import { Observable } from "rxjs";
|
|
2
1
|
import { EventPacket, RxNostr } from "rx-nostr";
|
|
3
|
-
import { Filter, NostrEvent } from "nostr-tools";
|
|
4
2
|
import { logger } from "applesauce-core";
|
|
5
|
-
import { Loader } from "./loader.js";
|
|
6
|
-
|
|
7
|
-
kind: number;
|
|
8
|
-
pubkey: string;
|
|
9
|
-
/** Optional "d" tag for paramaritized replaceable */
|
|
10
|
-
identifier?: string;
|
|
11
|
-
/** Relays to load from */
|
|
12
|
-
relays?: string[];
|
|
13
|
-
/** Load this address pointer even if it has already been loaded */
|
|
14
|
-
force?: boolean;
|
|
15
|
-
};
|
|
16
|
-
export type CacheRequest = (filters: Filter[]) => Observable<NostrEvent>;
|
|
3
|
+
import { CacheRequest, Loader } from "./loader.js";
|
|
4
|
+
import { LoadableAddressPointer } from "../helpers/address-pointer.js";
|
|
17
5
|
export type ReplaceableLoaderOptions = {
|
|
18
6
|
/**
|
|
19
7
|
* Time interval to buffer requests in ms
|
|
20
8
|
* @default 1000
|
|
21
9
|
*/
|
|
22
10
|
bufferTime?: number;
|
|
23
|
-
/**
|
|
11
|
+
/** A method used to load events from a local cache */
|
|
24
12
|
cacheRequest?: CacheRequest;
|
|
25
13
|
/** Fallback lookup relays to check when event cant be found */
|
|
26
14
|
lookupRelays?: string[];
|
|
27
15
|
};
|
|
28
16
|
export declare class ReplaceableLoader extends Loader<LoadableAddressPointer, EventPacket> {
|
|
29
17
|
log: typeof logger;
|
|
18
|
+
/** A method used to load events from a local cache */
|
|
19
|
+
cacheRequest?: CacheRequest;
|
|
20
|
+
/** Fallback lookup relays to check when event cant be found */
|
|
21
|
+
lookupRelays?: string[];
|
|
30
22
|
constructor(rxNostr: RxNostr, opts?: ReplaceableLoaderOptions);
|
|
31
23
|
}
|
|
@@ -1,23 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { tap, filter, bufferTime, map } from "rxjs";
|
|
2
|
+
import { createRxOneshotReq } from "rx-nostr";
|
|
3
|
+
import { getEventUID, markFromCache, mergeRelaySets } from "applesauce-core/helpers";
|
|
3
4
|
import { logger } from "applesauce-core";
|
|
4
5
|
import { nanoid } from "nanoid";
|
|
5
6
|
import { Loader } from "./loader.js";
|
|
6
7
|
import { generatorSequence } from "../operators/generator-sequence.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
/** deep clone a loadable pointer to ensure its safe to modify */
|
|
11
|
-
function cloneLoadablePointer(pointer) {
|
|
12
|
-
const clone = { ...pointer };
|
|
13
|
-
if (pointer.relays)
|
|
14
|
-
clone.relays = [...pointer.relays];
|
|
15
|
-
return clone;
|
|
16
|
-
}
|
|
8
|
+
import { consolidateAddressPointers, createFiltersFromAddressPointers, getAddressPointerId, getRelaysFromPointers, isLoadableAddressPointer, } from "../helpers/address-pointer.js";
|
|
9
|
+
import { getDefaultReadRelays } from "../helpers/rx-nostr.js";
|
|
10
|
+
import { distinctRelaysBatch } from "../operators/distinct-relays.js";
|
|
17
11
|
/** A generator that tries to load the address pointers from the cache first, then tries the relays */
|
|
18
12
|
function* cacheFirstSequence(rxNostr, pointers, log, opts) {
|
|
19
|
-
const id = nanoid(
|
|
20
|
-
log = log.extend(id);
|
|
13
|
+
const id = nanoid(4);
|
|
21
14
|
let remaining = Array.from(pointers);
|
|
22
15
|
const pointerRelays = Array.from(getRelaysFromPointers(pointers));
|
|
23
16
|
// handle previous step results and decide if to exit
|
|
@@ -26,40 +19,40 @@ function* cacheFirstSequence(rxNostr, pointers, log, opts) {
|
|
|
26
19
|
const coordinates = new Set(results.map((p) => getEventUID(p.event)));
|
|
27
20
|
// if there where results, filter out any pointers that where found
|
|
28
21
|
remaining = remaining.filter((pointer) => {
|
|
29
|
-
const found = coordinates.has(
|
|
22
|
+
const found = coordinates.has(getAddressPointerId(pointer));
|
|
30
23
|
if (found && pointer.force !== true)
|
|
31
24
|
return false;
|
|
32
25
|
else
|
|
33
26
|
return true;
|
|
34
27
|
});
|
|
35
|
-
|
|
28
|
+
// If there are none left, complete
|
|
29
|
+
if (remaining.length === 0) {
|
|
30
|
+
log(`[${id}] Complete`);
|
|
36
31
|
return true;
|
|
32
|
+
}
|
|
37
33
|
}
|
|
38
34
|
return false;
|
|
39
35
|
};
|
|
40
36
|
// first attempt, load from cache relays
|
|
41
37
|
if (opts?.cacheRequest) {
|
|
42
|
-
log(`Checking cache`, remaining);
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
map(createFiltersFromAddressPointers),
|
|
46
|
-
// make requests
|
|
47
|
-
mergeMap((filters) => opts.cacheRequest(filters)),
|
|
38
|
+
log(`[${id}] Checking cache`, remaining);
|
|
39
|
+
const filters = createFiltersFromAddressPointers(remaining);
|
|
40
|
+
const results = yield opts.cacheRequest(filters).pipe(
|
|
48
41
|
// mark the event as from the cache
|
|
49
42
|
tap((event) => markFromCache(event)),
|
|
50
43
|
// convert to event packets
|
|
51
|
-
map((e) => ({ event: e, from: "
|
|
44
|
+
map((e) => ({ event: e, from: "", subId: "replaceable-loader", type: "EVENT" })));
|
|
52
45
|
if (handleResults(results))
|
|
53
46
|
return;
|
|
54
47
|
}
|
|
55
48
|
// load from pointer relays and default relays
|
|
56
|
-
const defaultRelays =
|
|
57
|
-
|
|
58
|
-
.map(([relay]) => relay);
|
|
59
|
-
const remoteRelays = [...pointerRelays, ...defaultRelays];
|
|
49
|
+
const defaultRelays = getDefaultReadRelays(rxNostr);
|
|
50
|
+
const remoteRelays = mergeRelaySets(pointerRelays, defaultRelays);
|
|
60
51
|
if (remoteRelays.length > 0) {
|
|
61
|
-
log(`Requesting`, remoteRelays, remaining);
|
|
62
|
-
const
|
|
52
|
+
log(`[${id}] Requesting`, remoteRelays, remaining);
|
|
53
|
+
const filters = createFiltersFromAddressPointers(remaining);
|
|
54
|
+
const req = createRxOneshotReq({ filters, rxReqId: id });
|
|
55
|
+
const results = yield rxNostr.use(req, { on: { relays: remoteRelays } });
|
|
63
56
|
if (handleResults(results))
|
|
64
57
|
return;
|
|
65
58
|
}
|
|
@@ -68,96 +61,46 @@ function* cacheFirstSequence(rxNostr, pointers, log, opts) {
|
|
|
68
61
|
// make sure we aren't asking a relay twice
|
|
69
62
|
const relays = opts.lookupRelays.filter((r) => !pointerRelays.includes(r));
|
|
70
63
|
if (relays.length > 0) {
|
|
71
|
-
log(`Request from lookup`, relays, remaining);
|
|
72
|
-
const
|
|
64
|
+
log(`[${id}] Request from lookup`, relays, remaining);
|
|
65
|
+
const filters = createFiltersFromAddressPointers(remaining);
|
|
66
|
+
const req = createRxOneshotReq({ filters, rxReqId: id });
|
|
67
|
+
const results = yield rxNostr.use(req, { on: { relays } });
|
|
73
68
|
if (handleResults(results))
|
|
74
69
|
return;
|
|
75
70
|
}
|
|
76
71
|
}
|
|
77
72
|
}
|
|
78
|
-
/** Batches address pointers and consolidates them */
|
|
79
|
-
function multiRelayBatcher(buffer) {
|
|
80
|
-
const requestedFrom = new Map();
|
|
81
|
-
const requestedDefault = new Set();
|
|
82
|
-
return (source) => source.pipe(
|
|
83
|
-
// buffer on time
|
|
84
|
-
bufferTime(buffer),
|
|
85
|
-
// ignore empty buffers
|
|
86
|
-
filter((buffer) => buffer.length > 0),
|
|
87
|
-
// consolidate buffered pointers
|
|
88
|
-
map((pointers) => {
|
|
89
|
-
const byId = new Map();
|
|
90
|
-
for (const pointer of pointers) {
|
|
91
|
-
const id = getAddressPointerId(pointer);
|
|
92
|
-
if (byId.has(id)) {
|
|
93
|
-
// duplicate, merge pointers
|
|
94
|
-
const current = byId.get(id);
|
|
95
|
-
// merge relays
|
|
96
|
-
if (pointer.relays) {
|
|
97
|
-
if (current.relays)
|
|
98
|
-
current.relays = unique([...current.relays, ...pointer.relays]);
|
|
99
|
-
else
|
|
100
|
-
current.relays = pointer.relays;
|
|
101
|
-
}
|
|
102
|
-
// merge force flag
|
|
103
|
-
if (pointer.force !== undefined) {
|
|
104
|
-
current.force = current.force || pointer.force;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
else
|
|
108
|
-
byId.set(id, cloneLoadablePointer(pointer));
|
|
109
|
-
}
|
|
110
|
-
// return consolidated pointers
|
|
111
|
-
return Array.from(byId.values());
|
|
112
|
-
}),
|
|
113
|
-
// ignore empty buffer
|
|
114
|
-
filter((buffer) => buffer.length > 0),
|
|
115
|
-
// ensure pointers are only requested from each relay once
|
|
116
|
-
map((pointers) => {
|
|
117
|
-
return pointers.filter((pointer) => {
|
|
118
|
-
const id = getAddressPointerId(pointer);
|
|
119
|
-
// skip if forced
|
|
120
|
-
if (pointer.force)
|
|
121
|
-
return true;
|
|
122
|
-
// skip if already requested from default relays
|
|
123
|
-
if (!pointer.relays) {
|
|
124
|
-
if (requestedDefault.has(id))
|
|
125
|
-
return false;
|
|
126
|
-
else {
|
|
127
|
-
requestedDefault.add(id);
|
|
128
|
-
return true;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
let set = requestedFrom.get(id);
|
|
132
|
-
if (!set) {
|
|
133
|
-
set = new Set();
|
|
134
|
-
requestedFrom.set(id, set);
|
|
135
|
-
}
|
|
136
|
-
// remove any relays that have already been used
|
|
137
|
-
pointer.relays = pointer.relays.filter((relay) => !set.has(relay));
|
|
138
|
-
// remember used relays
|
|
139
|
-
for (const relay of pointer.relays)
|
|
140
|
-
set.add(relay);
|
|
141
|
-
// if there are no new relays, ignore pointer
|
|
142
|
-
return pointer.relays.length > 0;
|
|
143
|
-
});
|
|
144
|
-
}),
|
|
145
|
-
// ignore empty buffer
|
|
146
|
-
filter((buffer) => buffer.length > 0));
|
|
147
|
-
}
|
|
148
73
|
export class ReplaceableLoader extends Loader {
|
|
149
74
|
log = logger.extend("ReplaceableLoader");
|
|
75
|
+
/** A method used to load events from a local cache */
|
|
76
|
+
cacheRequest;
|
|
77
|
+
/** Fallback lookup relays to check when event cant be found */
|
|
78
|
+
lookupRelays;
|
|
150
79
|
constructor(rxNostr, opts) {
|
|
151
80
|
super((source) => {
|
|
152
81
|
return source.pipe(
|
|
153
82
|
// filter out invalid pointers
|
|
154
83
|
filter(isLoadableAddressPointer),
|
|
155
|
-
//
|
|
156
|
-
|
|
84
|
+
// buffer on time
|
|
85
|
+
bufferTime(opts?.bufferTime ?? 1000),
|
|
86
|
+
// ignore empty buffers
|
|
87
|
+
filter((buffer) => buffer.length > 0),
|
|
88
|
+
// only fetch from each relay once
|
|
89
|
+
distinctRelaysBatch(getAddressPointerId),
|
|
90
|
+
// consolidate buffered pointers
|
|
91
|
+
map(consolidateAddressPointers),
|
|
92
|
+
// ignore empty buffer
|
|
93
|
+
filter((buffer) => buffer.length > 0),
|
|
157
94
|
// check cache, relays, lookup relays in that order
|
|
158
|
-
generatorSequence((pointers) => cacheFirstSequence(rxNostr, pointers, this.log,
|
|
159
|
-
|
|
160
|
-
|
|
95
|
+
generatorSequence((pointers) => cacheFirstSequence(rxNostr, pointers, this.log, {
|
|
96
|
+
cacheRequest: this.cacheRequest,
|
|
97
|
+
lookupRelays: this.lookupRelays,
|
|
98
|
+
}),
|
|
99
|
+
// there will always be more events, never complete
|
|
100
|
+
false));
|
|
161
101
|
});
|
|
102
|
+
// set options
|
|
103
|
+
this.cacheRequest = opts?.cacheRequest;
|
|
104
|
+
this.lookupRelays = opts?.lookupRelays;
|
|
162
105
|
}
|
|
163
106
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { IEventStore, QueryStore } from "applesauce-core";
|
|
2
|
+
import { ProfilePointer } from "nostr-tools/nip19";
|
|
3
|
+
import { Observable } from "rxjs";
|
|
4
|
+
import { ReplaceableLoader } from "./replaceable-loader.js";
|
|
5
|
+
import { LoadableAddressPointer } from "../helpers/address-pointer.js";
|
|
6
|
+
/** A special Promised based loader built on the {@link QueryStore} */
|
|
7
|
+
export declare class RequestLoader {
|
|
8
|
+
store: QueryStore;
|
|
9
|
+
requestTimeout: number;
|
|
10
|
+
replaceableLoader?: ReplaceableLoader;
|
|
11
|
+
constructor(store: QueryStore);
|
|
12
|
+
protected runWithTimeout<T extends unknown, Args extends Array<any>>(queryConstructor: (...args: Args) => {
|
|
13
|
+
key: string;
|
|
14
|
+
run: (events: IEventStore, store: QueryStore) => Observable<T>;
|
|
15
|
+
}, ...args: Args): Promise<NonNullable<T>>;
|
|
16
|
+
protected checkReplaceable(): ReplaceableLoader;
|
|
17
|
+
/** Requests a single replaceable event */
|
|
18
|
+
replaceable(pointer: LoadableAddressPointer, force?: boolean): Promise<import("nostr-tools").Event>;
|
|
19
|
+
/** Loads a pubkeys profile */
|
|
20
|
+
profile(pointer: ProfilePointer, force?: boolean): Promise<import("applesauce-core/helpers").ProfileContent>;
|
|
21
|
+
/** Loads a pubkeys profile */
|
|
22
|
+
mailboxes(pointer: ProfilePointer, force?: boolean): Promise<{
|
|
23
|
+
inboxes: string[];
|
|
24
|
+
outboxes: string[];
|
|
25
|
+
}>;
|
|
26
|
+
/** Loads a pubkeys profile */
|
|
27
|
+
contacts(pointer: ProfilePointer, force?: boolean): Promise<ProfilePointer[]>;
|
|
28
|
+
/** Loads a pubkeys blossom servers */
|
|
29
|
+
blossomServers(pointer: ProfilePointer, force?: boolean): Promise<URL[]>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { kinds } from "nostr-tools";
|
|
2
|
+
import { MailboxesQuery, ProfileQuery, ReplaceableQuery, UserBlossomServersQuery, UserContactsQuery, } from "applesauce-core/queries";
|
|
3
|
+
import { getObservableValue, simpleTimeout } from "applesauce-core/observable";
|
|
4
|
+
import { filter } from "rxjs";
|
|
5
|
+
import { BLOSSOM_SERVER_LIST_KIND } from "applesauce-core/helpers";
|
|
6
|
+
/** A special Promised based loader built on the {@link QueryStore} */
|
|
7
|
+
export class RequestLoader {
|
|
8
|
+
store;
|
|
9
|
+
requestTimeout = 10_000;
|
|
10
|
+
replaceableLoader;
|
|
11
|
+
constructor(store) {
|
|
12
|
+
this.store = store;
|
|
13
|
+
}
|
|
14
|
+
// hacky method to run queries with timeouts
|
|
15
|
+
async runWithTimeout(queryConstructor, ...args) {
|
|
16
|
+
return getObservableValue(this.store.createQuery(queryConstructor, ...args).pipe(
|
|
17
|
+
// ignore undefined and null values
|
|
18
|
+
filter((v) => v !== undefined && v !== null),
|
|
19
|
+
// timeout with an error is not values
|
|
20
|
+
simpleTimeout(this.requestTimeout)));
|
|
21
|
+
}
|
|
22
|
+
checkReplaceable() {
|
|
23
|
+
if (!this.replaceableLoader)
|
|
24
|
+
throw new Error("Missing ReplaceableLoader");
|
|
25
|
+
return this.replaceableLoader;
|
|
26
|
+
}
|
|
27
|
+
/** Requests a single replaceable event */
|
|
28
|
+
replaceable(pointer, force) {
|
|
29
|
+
this.checkReplaceable().next({ ...pointer, force });
|
|
30
|
+
return this.runWithTimeout(ReplaceableQuery, pointer.kind, pointer.pubkey, pointer.identifier);
|
|
31
|
+
}
|
|
32
|
+
/** Loads a pubkeys profile */
|
|
33
|
+
profile(pointer, force) {
|
|
34
|
+
this.checkReplaceable().next({ kind: kinds.Metadata, pubkey: pointer.pubkey, relays: pointer.relays, force });
|
|
35
|
+
return this.runWithTimeout(ProfileQuery, pointer.pubkey);
|
|
36
|
+
}
|
|
37
|
+
/** Loads a pubkeys profile */
|
|
38
|
+
mailboxes(pointer, force) {
|
|
39
|
+
this.checkReplaceable().next({ kind: kinds.RelayList, pubkey: pointer.pubkey, relays: pointer.relays, force });
|
|
40
|
+
return this.runWithTimeout(MailboxesQuery, pointer.pubkey);
|
|
41
|
+
}
|
|
42
|
+
/** Loads a pubkeys profile */
|
|
43
|
+
contacts(pointer, force) {
|
|
44
|
+
this.checkReplaceable().next({ kind: kinds.Contacts, pubkey: pointer.pubkey, relays: pointer.relays, force });
|
|
45
|
+
return this.runWithTimeout(UserContactsQuery, pointer.pubkey);
|
|
46
|
+
}
|
|
47
|
+
/** Loads a pubkeys blossom servers */
|
|
48
|
+
blossomServers(pointer, force) {
|
|
49
|
+
this.checkReplaceable().next({
|
|
50
|
+
kind: BLOSSOM_SERVER_LIST_KIND,
|
|
51
|
+
pubkey: pointer.pubkey,
|
|
52
|
+
relays: pointer.relays,
|
|
53
|
+
force,
|
|
54
|
+
});
|
|
55
|
+
return this.runWithTimeout(UserBlossomServersQuery, pointer.pubkey);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { EventPacket, RxNostr } from "rx-nostr";
|
|
2
|
+
import { logger } from "applesauce-core";
|
|
3
|
+
import { CacheRequest, Loader } from "./loader.js";
|
|
4
|
+
export type LoadableEventPointer = {
|
|
5
|
+
id: string;
|
|
6
|
+
/** Relays to load from */
|
|
7
|
+
relays?: string[];
|
|
8
|
+
};
|
|
9
|
+
export type SingleEventLoaderOptions = {
|
|
10
|
+
/**
|
|
11
|
+
* Time interval to buffer requests in ms
|
|
12
|
+
* @default 1000
|
|
13
|
+
*/
|
|
14
|
+
bufferTime?: number;
|
|
15
|
+
/** A method used to load events from a local cache */
|
|
16
|
+
cacheRequest?: CacheRequest;
|
|
17
|
+
/**
|
|
18
|
+
* How long the loader should wait before it allows an event pointer to be refreshed from a relay
|
|
19
|
+
* @default 60000
|
|
20
|
+
*/
|
|
21
|
+
refreshTimeout?: number;
|
|
22
|
+
};
|
|
23
|
+
export declare class SingleEventLoader extends Loader<LoadableEventPointer, EventPacket> {
|
|
24
|
+
log: typeof logger;
|
|
25
|
+
constructor(rxNostr: RxNostr, opts?: SingleEventLoaderOptions);
|
|
26
|
+
}
|