applesauce-loaders 0.10.0 → 0.11.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 +28 -0
- package/dist/loaders/request-loader.js +42 -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 +26 -7
- 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 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { createFiltersFromAddressPointers } from "../address-pointer.js";
|
|
3
|
+
import { kinds } from "nostr-tools";
|
|
4
|
+
describe("address pointer helpers", () => {
|
|
5
|
+
describe("createFiltersFromAddressPointers", () => {
|
|
6
|
+
it("should separate replaceable and parameterized replaceable pointers", () => {
|
|
7
|
+
expect(createFiltersFromAddressPointers([
|
|
8
|
+
{ kind: kinds.BookmarkList, pubkey: "pubkey" },
|
|
9
|
+
{ kind: kinds.Metadata, pubkey: "pubkey" },
|
|
10
|
+
{ kind: kinds.Metadata, pubkey: "pubkey2" },
|
|
11
|
+
{ kind: kinds.Bookmarksets, identifier: "funny", pubkey: "pubkey" },
|
|
12
|
+
])).toEqual(expect.arrayContaining([
|
|
13
|
+
{ kinds: [kinds.Metadata], authors: ["pubkey", "pubkey2"] },
|
|
14
|
+
{ kinds: [kinds.BookmarkList], authors: ["pubkey"] },
|
|
15
|
+
{ "#d": ["funny"], authors: ["pubkey"], kinds: [kinds.Bookmarksets] },
|
|
16
|
+
]));
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import { AddressPointerWithoutD } from "applesauce-core/helpers";
|
|
2
2
|
import { Filter } from "nostr-tools";
|
|
3
|
+
import { AddressPointer } from "nostr-tools/nip19";
|
|
4
|
+
export type LoadableAddressPointer = {
|
|
5
|
+
kind: number;
|
|
6
|
+
pubkey: string;
|
|
7
|
+
/** Optional "d" tag for paramaritized replaceable */
|
|
8
|
+
identifier?: string;
|
|
9
|
+
/** Relays to load from */
|
|
10
|
+
relays?: string[];
|
|
11
|
+
/** Load this address pointer even if it has already been loaded */
|
|
12
|
+
force?: boolean;
|
|
13
|
+
};
|
|
3
14
|
/** Converts an array of address pointers to a filter */
|
|
4
|
-
export declare function createFilterFromAddressPointers(pointers: AddressPointerWithoutD[]): Filter;
|
|
15
|
+
export declare function createFilterFromAddressPointers(pointers: AddressPointerWithoutD[] | AddressPointer[]): Filter;
|
|
5
16
|
/** Takes a set of address pointers, groups them, then returns filters for the groups */
|
|
6
17
|
export declare function createFiltersFromAddressPointers(pointers: AddressPointerWithoutD[]): Filter[];
|
|
7
18
|
/** Checks if a relay will understand an address pointer */
|
|
@@ -11,6 +22,8 @@ export declare function groupAddressPointersByKind(pointers: AddressPointerWitho
|
|
|
11
22
|
/** Group an array of address pointers by pubkey */
|
|
12
23
|
export declare function groupAddressPointersByPubkey(pointers: AddressPointerWithoutD[]): Map<string, AddressPointerWithoutD[]>;
|
|
13
24
|
/** Groups address pointers by kind or pubkey depending on which is most optimal */
|
|
14
|
-
export declare function groupAddressPointersByPubkeyOrKind(pointers: AddressPointerWithoutD[]): Map<
|
|
25
|
+
export declare function groupAddressPointersByPubkeyOrKind(pointers: AddressPointerWithoutD[]): Map<string, AddressPointerWithoutD[]> | Map<number, AddressPointerWithoutD[]>;
|
|
15
26
|
export declare function getRelaysFromPointers(pointers: AddressPointerWithoutD[]): Set<string>;
|
|
16
27
|
export declare function getAddressPointerId<T extends AddressPointerWithoutD>(pointer: T): string;
|
|
28
|
+
/** deduplicates an array of address pointers and merges their relays array */
|
|
29
|
+
export declare function consolidateAddressPointers(pointers: LoadableAddressPointer[]): LoadableAddressPointer[];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getReplaceableUID } from "applesauce-core/helpers";
|
|
1
|
+
import { getReplaceableUID, mergeRelaySets } from "applesauce-core/helpers";
|
|
2
2
|
import { isParameterizedReplaceableKind, isReplaceableKind } from "nostr-tools/kinds";
|
|
3
3
|
import { unique } from "./array.js";
|
|
4
4
|
/** Converts an array of address pointers to a filter */
|
|
@@ -13,8 +13,19 @@ export function createFilterFromAddressPointers(pointers) {
|
|
|
13
13
|
}
|
|
14
14
|
/** Takes a set of address pointers, groups them, then returns filters for the groups */
|
|
15
15
|
export function createFiltersFromAddressPointers(pointers) {
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
// split the points in to two groups so they they don't mix in the filters
|
|
17
|
+
const parameterizedReplaceable = pointers.filter((p) => isParameterizedReplaceableKind(p.kind));
|
|
18
|
+
const replaceable = pointers.filter((p) => isReplaceableKind(p.kind));
|
|
19
|
+
const filters = [];
|
|
20
|
+
if (replaceable.length > 0) {
|
|
21
|
+
const groups = groupAddressPointersByPubkeyOrKind(replaceable);
|
|
22
|
+
filters.push(...Array.from(groups.values()).map(createFilterFromAddressPointers));
|
|
23
|
+
}
|
|
24
|
+
if (parameterizedReplaceable.length > 0) {
|
|
25
|
+
const groups = groupAddressPointersByPubkeyOrKind(parameterizedReplaceable);
|
|
26
|
+
filters.push(...Array.from(groups.values()).map(createFilterFromAddressPointers));
|
|
27
|
+
}
|
|
28
|
+
return filters;
|
|
18
29
|
}
|
|
19
30
|
/** Checks if a relay will understand an address pointer */
|
|
20
31
|
export function isLoadableAddressPointer(pointer) {
|
|
@@ -49,7 +60,7 @@ export function groupAddressPointersByPubkey(pointers) {
|
|
|
49
60
|
export function groupAddressPointersByPubkeyOrKind(pointers) {
|
|
50
61
|
const kinds = new Set(pointers.map((p) => p.kind));
|
|
51
62
|
const pubkeys = new Set(pointers.map((p) => p.pubkey));
|
|
52
|
-
return pubkeys.size
|
|
63
|
+
return pubkeys.size < kinds.size ? groupAddressPointersByPubkey(pointers) : groupAddressPointersByKind(pointers);
|
|
53
64
|
}
|
|
54
65
|
export function getRelaysFromPointers(pointers) {
|
|
55
66
|
const relays = new Set();
|
|
@@ -65,3 +76,36 @@ export function getRelaysFromPointers(pointers) {
|
|
|
65
76
|
export function getAddressPointerId(pointer) {
|
|
66
77
|
return getReplaceableUID(pointer.kind, pointer.pubkey, pointer.identifier);
|
|
67
78
|
}
|
|
79
|
+
/** deep clone a loadable pointer to ensure its safe to modify */
|
|
80
|
+
function cloneLoadablePointer(pointer) {
|
|
81
|
+
const clone = { ...pointer };
|
|
82
|
+
if (pointer.relays)
|
|
83
|
+
clone.relays = [...pointer.relays];
|
|
84
|
+
return clone;
|
|
85
|
+
}
|
|
86
|
+
/** deduplicates an array of address pointers and merges their relays array */
|
|
87
|
+
export function consolidateAddressPointers(pointers) {
|
|
88
|
+
const byId = new Map();
|
|
89
|
+
for (const pointer of pointers) {
|
|
90
|
+
const id = getAddressPointerId(pointer);
|
|
91
|
+
if (byId.has(id)) {
|
|
92
|
+
// duplicate, merge pointers
|
|
93
|
+
const current = byId.get(id);
|
|
94
|
+
// merge relays
|
|
95
|
+
if (pointer.relays) {
|
|
96
|
+
if (current.relays)
|
|
97
|
+
current.relays = mergeRelaySets(current.relays, pointer.relays);
|
|
98
|
+
else
|
|
99
|
+
current.relays = pointer.relays;
|
|
100
|
+
}
|
|
101
|
+
// merge force flag
|
|
102
|
+
if (pointer.force !== undefined) {
|
|
103
|
+
current.force = current.force || pointer.force;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else
|
|
107
|
+
byId.set(id, cloneLoadablePointer(pointer));
|
|
108
|
+
}
|
|
109
|
+
// return consolidated pointers
|
|
110
|
+
return Array.from(byId.values());
|
|
111
|
+
}
|
package/dist/helpers/array.d.ts
CHANGED
package/dist/helpers/array.js
CHANGED
|
@@ -2,20 +2,3 @@
|
|
|
2
2
|
export function unique(arr) {
|
|
3
3
|
return Array.from(new Set(arr));
|
|
4
4
|
}
|
|
5
|
-
/** split array into a set of arrays no larger than batchSize */
|
|
6
|
-
export function reduceToBatches(arr, batchSize) {
|
|
7
|
-
const batches = [];
|
|
8
|
-
for (const value of arr) {
|
|
9
|
-
if (batches.length === 0) {
|
|
10
|
-
batches.push([value]);
|
|
11
|
-
continue;
|
|
12
|
-
}
|
|
13
|
-
const current = batches[batches.length - 1];
|
|
14
|
-
if (current.length <= batchSize) {
|
|
15
|
-
current.push(value);
|
|
16
|
-
}
|
|
17
|
-
else
|
|
18
|
-
batches.push([value]);
|
|
19
|
-
}
|
|
20
|
-
return batches;
|
|
21
|
-
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export type DomainIdentityJson = {
|
|
2
|
+
names?: Record<string, string | undefined>;
|
|
3
|
+
relays?: Record<string, string[]>;
|
|
4
|
+
nip46?: Record<string, string[]>;
|
|
5
|
+
};
|
|
6
|
+
export declare enum IdentityStatus {
|
|
7
|
+
/** Got error when fetching identity document */
|
|
8
|
+
Error = "error",
|
|
9
|
+
/** Identity missing from document */
|
|
10
|
+
Missing = "missing",
|
|
11
|
+
/** Identity was found */
|
|
12
|
+
Found = "found"
|
|
13
|
+
}
|
|
14
|
+
export type BaseIdentity = {
|
|
15
|
+
name: string;
|
|
16
|
+
domain: string;
|
|
17
|
+
/** The unix timestamp of when the identity was checked */
|
|
18
|
+
checked: number;
|
|
19
|
+
};
|
|
20
|
+
export type ErrorIdentity = BaseIdentity & {
|
|
21
|
+
status: IdentityStatus.Error;
|
|
22
|
+
error: string;
|
|
23
|
+
};
|
|
24
|
+
export type MissingIdentity = BaseIdentity & {
|
|
25
|
+
status: IdentityStatus.Missing;
|
|
26
|
+
};
|
|
27
|
+
export type KnownIdentity = BaseIdentity & {
|
|
28
|
+
status: IdentityStatus.Found;
|
|
29
|
+
pubkey: string;
|
|
30
|
+
relays?: string[];
|
|
31
|
+
hasNip46?: boolean;
|
|
32
|
+
nip46Relays?: string[];
|
|
33
|
+
};
|
|
34
|
+
export type Identity = KnownIdentity | ErrorIdentity | MissingIdentity;
|
|
35
|
+
/** Gets an Identity from the .well-known/nostr.json document */
|
|
36
|
+
export declare function getIdentityFromJson(name: string, domain: string, json: DomainIdentityJson, checked?: number): MissingIdentity | KnownIdentity;
|
|
37
|
+
/** Returns all Identifies in a json document */
|
|
38
|
+
export declare function getIdentitiesFromJson(domain: string, json: DomainIdentityJson, checked?: number): Record<string, Identity>;
|
|
39
|
+
/** convert all keys in names, and relays to lower case */
|
|
40
|
+
export declare function normalizeIdentityJson(json: DomainIdentityJson): DomainIdentityJson;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { unixNow } from "applesauce-core/helpers";
|
|
2
|
+
export var IdentityStatus;
|
|
3
|
+
(function (IdentityStatus) {
|
|
4
|
+
/** Got error when fetching identity document */
|
|
5
|
+
IdentityStatus["Error"] = "error";
|
|
6
|
+
/** Identity missing from document */
|
|
7
|
+
IdentityStatus["Missing"] = "missing";
|
|
8
|
+
/** Identity was found */
|
|
9
|
+
IdentityStatus["Found"] = "found";
|
|
10
|
+
})(IdentityStatus || (IdentityStatus = {}));
|
|
11
|
+
/** Gets an Identity from the .well-known/nostr.json document */
|
|
12
|
+
export function getIdentityFromJson(name, domain, json, checked = unixNow()) {
|
|
13
|
+
const common = { name, domain, checked };
|
|
14
|
+
if (!json.names)
|
|
15
|
+
return { ...common, status: IdentityStatus.Missing };
|
|
16
|
+
const pubkey = json.names[name];
|
|
17
|
+
if (!pubkey)
|
|
18
|
+
return { ...common, status: IdentityStatus.Missing };
|
|
19
|
+
const relays = json.relays?.[pubkey];
|
|
20
|
+
const hasNip46 = !!json.nip46;
|
|
21
|
+
const nip46Relays = json.nip46?.[pubkey];
|
|
22
|
+
return { ...common, pubkey, relays, nip46Relays, hasNip46, status: IdentityStatus.Found };
|
|
23
|
+
}
|
|
24
|
+
/** Returns all Identifies in a json document */
|
|
25
|
+
export function getIdentitiesFromJson(domain, json, checked = unixNow()) {
|
|
26
|
+
if (!json.names)
|
|
27
|
+
return {};
|
|
28
|
+
return Object.keys(json.names).reduce((dir, name) => {
|
|
29
|
+
const address = `${name}@${domain}`;
|
|
30
|
+
const identity = getIdentityFromJson(name, domain, json, checked);
|
|
31
|
+
dir[address] = identity;
|
|
32
|
+
return dir;
|
|
33
|
+
}, {});
|
|
34
|
+
}
|
|
35
|
+
/** convert all keys in names, and relays to lower case */
|
|
36
|
+
export function normalizeIdentityJson(json) {
|
|
37
|
+
if (json.names) {
|
|
38
|
+
for (const [name, pubkey] of Object.entries(json.names)) {
|
|
39
|
+
delete json.names[name];
|
|
40
|
+
json.names[name.toLowerCase()] = pubkey;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (json.relays) {
|
|
44
|
+
for (const [name, pubkey] of Object.entries(json.relays)) {
|
|
45
|
+
delete json.relays[name];
|
|
46
|
+
json.relays[name.toLowerCase()] = pubkey;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return json;
|
|
50
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** deduplicates an array of event pointers and merges their relays array */
|
|
2
|
+
export function consolidateEventPointers(pointers) {
|
|
3
|
+
let ids = new Map();
|
|
4
|
+
for (let pointer of pointers) {
|
|
5
|
+
let existing = ids.get(pointer.id);
|
|
6
|
+
if (existing) {
|
|
7
|
+
// merge relays
|
|
8
|
+
if (pointer.relays) {
|
|
9
|
+
if (!existing.relays)
|
|
10
|
+
existing.relays = [...pointer.relays];
|
|
11
|
+
else
|
|
12
|
+
existing.relays = [...existing.relays, ...pointer.relays.filter((r) => !existing.relays.includes(r))];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
else
|
|
16
|
+
ids.set(pointer.id, pointer);
|
|
17
|
+
}
|
|
18
|
+
return Array.from(ids.values());
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./dns-identity.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./dns-identity.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function groupByRelay<T extends {
|
|
2
|
+
relays?: string[];
|
|
3
|
+
}>(pointers: T[], defaultKey?: string): Map<string, T[]>;
|
|
4
|
+
export interface MessageWithRelay {
|
|
5
|
+
relays?: string[];
|
|
6
|
+
/** Ignore timeout and force message through */
|
|
7
|
+
force?: boolean;
|
|
8
|
+
[key: string]: any;
|
|
9
|
+
}
|
|
10
|
+
/** Ensures that a message only is requested from each relay once in timeout */
|
|
11
|
+
export declare function removePreviouslyUsedRelays<T extends MessageWithRelay>(message: T, keyFn: (message: T) => string, cache: Map<string, number>, timeout?: number): T | null;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export function groupByRelay(pointers, defaultKey) {
|
|
2
|
+
let byRelay = new Map();
|
|
3
|
+
for (const pointer of pointers) {
|
|
4
|
+
let relays = pointer.relays?.length ? pointer.relays : defaultKey ? [defaultKey] : [];
|
|
5
|
+
for (const relay of relays) {
|
|
6
|
+
if (!byRelay.has(relay))
|
|
7
|
+
byRelay.set(relay, [pointer]);
|
|
8
|
+
else
|
|
9
|
+
byRelay.get(relay)?.push(pointer);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return byRelay;
|
|
13
|
+
}
|
|
14
|
+
/** Ensures that a message only is requested from each relay once in timeout */
|
|
15
|
+
export function removePreviouslyUsedRelays(message, keyFn, cache, timeout = 60_000) {
|
|
16
|
+
if (message.force)
|
|
17
|
+
return message;
|
|
18
|
+
let key = keyFn(message);
|
|
19
|
+
let now = Date.now();
|
|
20
|
+
if (message.relays) {
|
|
21
|
+
// requesting from specific relays
|
|
22
|
+
let relays = message.relays;
|
|
23
|
+
relays = relays.filter((relay) => {
|
|
24
|
+
let relayKey = key + " " + relay;
|
|
25
|
+
let last = cache.get(relayKey);
|
|
26
|
+
if (!last || now >= last + timeout) {
|
|
27
|
+
cache.set(relayKey, now);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
else
|
|
31
|
+
return false;
|
|
32
|
+
});
|
|
33
|
+
if (relays.length === 0)
|
|
34
|
+
return null;
|
|
35
|
+
return { ...message, relays };
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
// requesting from default relays
|
|
39
|
+
let last = cache.get(key);
|
|
40
|
+
if (!last || now >= last + timeout) {
|
|
41
|
+
cache.set(key, now);
|
|
42
|
+
return message;
|
|
43
|
+
}
|
|
44
|
+
else
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export *
|
|
1
|
+
export * from "./loaders/index.js";
|
|
2
2
|
export * as Operators from "./operators/index.js";
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export *
|
|
1
|
+
export * from "./loaders/index.js";
|
|
2
2
|
export * as Operators from "./operators/index.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { DnsIdentityLoader } from "../dns-identity-loader.js";
|
|
3
|
+
import { unixNow } from "applesauce-core/helpers";
|
|
4
|
+
import { IdentityStatus } from "../../helpers/dns-identity.js";
|
|
5
|
+
let loader;
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
loader = new DnsIdentityLoader();
|
|
8
|
+
});
|
|
9
|
+
describe("fetch", () => {
|
|
10
|
+
it("should not assign this in fetch method", async () => {
|
|
11
|
+
const loader = new DnsIdentityLoader();
|
|
12
|
+
let that = undefined;
|
|
13
|
+
function custom() {
|
|
14
|
+
//@ts-expect-error
|
|
15
|
+
that = this;
|
|
16
|
+
throw new Error("not implemented");
|
|
17
|
+
}
|
|
18
|
+
// @ts-expect-error
|
|
19
|
+
loader.fetch = custom;
|
|
20
|
+
await loader.fetchIdentity("_", "hzrd149.com");
|
|
21
|
+
expect(that).not.toBe(loader);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
describe("requestIdentity", () => {
|
|
25
|
+
it("should load from cache first", async () => {
|
|
26
|
+
const cache = {
|
|
27
|
+
save: vi.fn().mockResolvedValue(undefined),
|
|
28
|
+
load: vi.fn().mockResolvedValue({
|
|
29
|
+
name: "_",
|
|
30
|
+
domain: "hzrd149.com",
|
|
31
|
+
pubkey: "pubkey",
|
|
32
|
+
checked: unixNow(),
|
|
33
|
+
status: IdentityStatus.Found,
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
loader.cache = cache;
|
|
37
|
+
loader.fetch = vi.fn().mockRejectedValue(new Error("error"));
|
|
38
|
+
await loader.requestIdentity("_", "hzrd149.com");
|
|
39
|
+
expect(cache.load).toHaveBeenCalledWith("_@hzrd149.com");
|
|
40
|
+
expect(loader.fetch).not.toHaveBeenCalled();
|
|
41
|
+
});
|
|
42
|
+
it("should fetch if cache is too old", async () => {
|
|
43
|
+
const cache = {
|
|
44
|
+
save: vi.fn().mockResolvedValue(undefined),
|
|
45
|
+
load: vi.fn().mockResolvedValue({
|
|
46
|
+
name: "_",
|
|
47
|
+
domain: "hzrd149.com",
|
|
48
|
+
pubkey: "pubkey",
|
|
49
|
+
checked: unixNow() - 60 * 60 * 24 * 7 * 2,
|
|
50
|
+
status: IdentityStatus.Found,
|
|
51
|
+
}),
|
|
52
|
+
};
|
|
53
|
+
loader.cache = cache;
|
|
54
|
+
loader.fetch = vi.fn().mockRejectedValue(new Error("error"));
|
|
55
|
+
await loader.requestIdentity("_", "hzrd149.com");
|
|
56
|
+
expect(cache.load).toHaveBeenCalledWith("_@hzrd149.com");
|
|
57
|
+
expect(loader.fetch).toHaveBeenCalled();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { expect, it } from "vitest";
|
|
2
|
+
// import { createMockRelay, MockRelay } from "vitest-nostr";
|
|
3
|
+
// import { createRxNostr } from "rx-nostr";
|
|
4
|
+
// import { verifier } from "rx-nostr-crypto";
|
|
5
|
+
// import { RelayTimelineLoader } from "./relay-timeline-loader.js";
|
|
6
|
+
// let relay: MockRelay;
|
|
7
|
+
// beforeEach(async () => {
|
|
8
|
+
// relay = createMockRelay("ws://localhost:1234");
|
|
9
|
+
// });
|
|
10
|
+
it("should complete when 0 events are returned", async () => {
|
|
11
|
+
// const rxNostr = createRxNostr({ verifier });
|
|
12
|
+
// const loader = new RelayTimelineLoader(rxNostr, "ws://localhost:1234", [{ kinds: [1] }]);
|
|
13
|
+
// let received = 0;
|
|
14
|
+
// loader.subscribe(() => received++);
|
|
15
|
+
// // load first page
|
|
16
|
+
// loader.next(100);
|
|
17
|
+
// expect(loader.loading).toBe(true);
|
|
18
|
+
// await relay.connected;
|
|
19
|
+
// await expect(relay).toReceiveREQ();
|
|
20
|
+
// relay.emitEVENT(loader.id, faker.event({ kind: 1 }));
|
|
21
|
+
// relay.emitEOSE(loader.id);
|
|
22
|
+
// await new Promise((res) => setTimeout(res, 0));
|
|
23
|
+
// expect(received).toBe(1);
|
|
24
|
+
// expect(loader.loading).toBe(false);
|
|
25
|
+
expect(true).toBeTruthy();
|
|
26
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Subject } from "rxjs";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { TimeoutError } from "applesauce-core/observable";
|
|
4
|
+
import { EventStore, QueryStore } from "applesauce-core";
|
|
5
|
+
import { RequestLoader } from "../request-loader.js";
|
|
6
|
+
let eventStore;
|
|
7
|
+
let queryStore;
|
|
8
|
+
let loader;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
eventStore = new EventStore();
|
|
11
|
+
queryStore = new QueryStore(eventStore);
|
|
12
|
+
loader = new RequestLoader(queryStore);
|
|
13
|
+
// @ts-expect-error
|
|
14
|
+
loader.replaceableLoader = new Subject();
|
|
15
|
+
vi.spyOn(loader.replaceableLoader, "next").mockImplementation(() => { });
|
|
16
|
+
});
|
|
17
|
+
describe("profile", () => {
|
|
18
|
+
it("should return a promise that resolves", async () => {
|
|
19
|
+
const p = loader.profile({ pubkey: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d" });
|
|
20
|
+
expect(loader.replaceableLoader.next).toHaveBeenCalledWith(expect.objectContaining({ pubkey: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d" }));
|
|
21
|
+
eventStore.add({
|
|
22
|
+
content: '{"name":"fiatjaf","about":"~","picture":"https://fiatjaf.com/static/favicon.jpg","nip05":"_@fiatjaf.com","lud16":"fiatjaf@zbd.gg","website":"https://nostr.technology"}',
|
|
23
|
+
created_at: 1738588530,
|
|
24
|
+
id: "c43be8b4634298e97dde3020a5e6aeec37d7f5a4b0259705f496e81a550c8f8b",
|
|
25
|
+
kind: 0,
|
|
26
|
+
pubkey: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
|
|
27
|
+
sig: "202a1bf6a58943d660c1891662dbdda142aa8e5bca9d4a3cb03cde816ad3bdda6f4ec3b880671506c2820285b32218a0afdec2d172de9694d83972190ab4f9da",
|
|
28
|
+
tags: [],
|
|
29
|
+
});
|
|
30
|
+
expect(await p).toEqual(expect.objectContaining({ name: "fiatjaf" }));
|
|
31
|
+
});
|
|
32
|
+
it("should reject with TimeoutError after 10 seconds", async () => {
|
|
33
|
+
// reduce timeout for tests
|
|
34
|
+
loader.requestTimeout = 10;
|
|
35
|
+
await expect(loader.profile({ pubkey: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d" })).rejects.toThrow(TimeoutError);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { EventPacket } from "rx-nostr";
|
|
2
|
+
import { BehaviorSubject } from "rxjs";
|
|
3
|
+
import { logger } from "applesauce-core";
|
|
4
|
+
import { CacheRequest, Loader } from "./loader.js";
|
|
5
|
+
import { TimelessFilter } from "./relay-timeline-loader.js";
|
|
6
|
+
export type CacheTimelineLoaderOptions = {
|
|
7
|
+
/** default number of events to request in each batch */
|
|
8
|
+
limit?: number;
|
|
9
|
+
};
|
|
10
|
+
/** A loader that can be used to load a timeline in chunks */
|
|
11
|
+
export declare class CacheTimelineLoader extends Loader<number | void, EventPacket> {
|
|
12
|
+
filters: TimelessFilter[];
|
|
13
|
+
id: string;
|
|
14
|
+
loading$: BehaviorSubject<boolean>;
|
|
15
|
+
get loading(): boolean;
|
|
16
|
+
/** current "until" timestamp */
|
|
17
|
+
cursor: number;
|
|
18
|
+
/** set to true when 0 events are returned from last batch */
|
|
19
|
+
eose: boolean;
|
|
20
|
+
protected log: typeof logger;
|
|
21
|
+
constructor(cacheRequest: CacheRequest, filters: TimelessFilter[], opts?: CacheTimelineLoaderOptions);
|
|
22
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { BehaviorSubject, filter, map, mergeMap, tap } from "rxjs";
|
|
2
|
+
import { markFromCache, unixNow } from "applesauce-core/helpers";
|
|
3
|
+
import { logger } from "applesauce-core";
|
|
4
|
+
import { nanoid } from "nanoid";
|
|
5
|
+
import { Loader } from "./loader.js";
|
|
6
|
+
/** A loader that can be used to load a timeline in chunks */
|
|
7
|
+
export class CacheTimelineLoader extends Loader {
|
|
8
|
+
filters;
|
|
9
|
+
id = nanoid(8);
|
|
10
|
+
loading$ = new BehaviorSubject(false);
|
|
11
|
+
get loading() {
|
|
12
|
+
return this.loading$.value;
|
|
13
|
+
}
|
|
14
|
+
/** current "until" timestamp */
|
|
15
|
+
cursor = Infinity;
|
|
16
|
+
/** set to true when 0 events are returned from last batch */
|
|
17
|
+
eose = false;
|
|
18
|
+
log = logger.extend("CacheTimelineLoader");
|
|
19
|
+
constructor(cacheRequest, filters, opts) {
|
|
20
|
+
super((source) => source.pipe(filter(() => !this.loading && !this.eose), map((limit) => {
|
|
21
|
+
// build next batch filters
|
|
22
|
+
return filters.map((filter) => ({
|
|
23
|
+
limit: limit || opts?.limit,
|
|
24
|
+
...filter,
|
|
25
|
+
// limit curser to now
|
|
26
|
+
until: Math.min(unixNow(), this.cursor),
|
|
27
|
+
}));
|
|
28
|
+
}),
|
|
29
|
+
// ignore empty filters
|
|
30
|
+
filter((filters) => filters.length > 0), mergeMap((filters) => {
|
|
31
|
+
// make batch request
|
|
32
|
+
let count = 0;
|
|
33
|
+
this.loading$.next(true);
|
|
34
|
+
this.log(`Next batch starting at ${filters[0].until} limit ${filters[0].limit}`);
|
|
35
|
+
return cacheRequest(filters).pipe(tap({
|
|
36
|
+
next: (event) => {
|
|
37
|
+
// mark event from cache
|
|
38
|
+
markFromCache(event);
|
|
39
|
+
// update cursor when event is received
|
|
40
|
+
this.cursor = Math.min(event.created_at - 1, this.cursor);
|
|
41
|
+
count++;
|
|
42
|
+
},
|
|
43
|
+
complete: () => {
|
|
44
|
+
// set loading to false when batch completes
|
|
45
|
+
this.loading$.next(false);
|
|
46
|
+
// set eose if no events where returned
|
|
47
|
+
if (count === 0) {
|
|
48
|
+
this.eose = true;
|
|
49
|
+
this.log(`Got ${count} event, Complete`);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
this.log(`Finished batch, got ${count} events`);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
}), map((event) => ({ event, from: "", subId: "cache-timeline-loader", type: "EVENT" })));
|
|
56
|
+
})));
|
|
57
|
+
this.filters = filters;
|
|
58
|
+
// create a unique logger for this instance
|
|
59
|
+
this.log = this.log.extend("cache");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Identity } from "../helpers/dns-identity.js";
|
|
2
|
+
export type AsyncIdentityCache = {
|
|
3
|
+
/** Saves a batch of identities */
|
|
4
|
+
save: (identities: Record<string, Identity>) => Promise<void> | void;
|
|
5
|
+
/** Loads a single identity */
|
|
6
|
+
load: (address: string) => Promise<Identity | undefined> | Identity | undefined;
|
|
7
|
+
};
|
|
8
|
+
export declare class DnsIdentityLoader {
|
|
9
|
+
cache?: AsyncIdentityCache | undefined;
|
|
10
|
+
identities: Map<string, Identity>;
|
|
11
|
+
/** The fetch implementation this class should use */
|
|
12
|
+
fetch: (url: string, init: RequestInit) => Promise<Response>;
|
|
13
|
+
/** How long an identity should be kept until its considered expired (in seconds) defaults to 1 week */
|
|
14
|
+
expiration: number;
|
|
15
|
+
constructor(cache?: AsyncIdentityCache | undefined);
|
|
16
|
+
/** Makes an http request to fetch an identity */
|
|
17
|
+
fetchIdentity(name: string, domain: string): Promise<Identity>;
|
|
18
|
+
/** Loads an identity from the cache or fetches it */
|
|
19
|
+
loadIdentity(name: string, domain: string): Promise<Identity>;
|
|
20
|
+
private requesting;
|
|
21
|
+
/** Requests an identity to be loaded */
|
|
22
|
+
requestIdentity(name: string, domain: string): Promise<Identity>;
|
|
23
|
+
/** Checks if an identity is loaded */
|
|
24
|
+
getIdentity(name: string, domain: string): Identity | undefined;
|
|
25
|
+
}
|