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.
Files changed (67) hide show
  1. package/dist/helpers/__tests__/address-pointer.test.d.ts +1 -0
  2. package/dist/helpers/__tests__/address-pointer.test.js +19 -0
  3. package/dist/helpers/address-pointer.d.ts +15 -2
  4. package/dist/helpers/address-pointer.js +48 -4
  5. package/dist/helpers/array.d.ts +0 -2
  6. package/dist/helpers/array.js +0 -17
  7. package/dist/helpers/dns-identity.d.ts +40 -0
  8. package/dist/helpers/dns-identity.js +50 -0
  9. package/dist/helpers/event-pointer.d.ts +5 -0
  10. package/dist/helpers/event-pointer.js +19 -0
  11. package/dist/helpers/index.d.ts +1 -0
  12. package/dist/helpers/index.js +1 -0
  13. package/dist/helpers/pointer.d.ts +11 -0
  14. package/dist/helpers/pointer.js +47 -0
  15. package/dist/helpers/rx-nostr.d.ts +2 -0
  16. package/dist/helpers/rx-nostr.js +5 -0
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.js +1 -1
  19. package/dist/loaders/__tests__/dns-identity-loader.test.d.ts +1 -0
  20. package/dist/loaders/__tests__/dns-identity-loader.test.js +59 -0
  21. package/dist/loaders/__tests__/relay-timeline-loader.test.d.ts +1 -0
  22. package/dist/loaders/__tests__/relay-timeline-loader.test.js +26 -0
  23. package/dist/loaders/__tests__/request-loader.test.d.ts +1 -0
  24. package/dist/loaders/__tests__/request-loader.test.js +37 -0
  25. package/dist/loaders/cache-timeline-loader.d.ts +22 -0
  26. package/dist/loaders/cache-timeline-loader.js +61 -0
  27. package/dist/loaders/dns-identity-loader.d.ts +25 -0
  28. package/dist/loaders/dns-identity-loader.js +66 -0
  29. package/dist/loaders/index.d.ts +8 -1
  30. package/dist/loaders/index.js +8 -1
  31. package/dist/loaders/loader.d.ts +14 -8
  32. package/dist/loaders/loader.js +7 -2
  33. package/dist/loaders/relay-timeline-loader.d.ts +24 -0
  34. package/dist/loaders/relay-timeline-loader.js +70 -0
  35. package/dist/loaders/replaceable-loader.d.ts +7 -15
  36. package/dist/loaders/replaceable-loader.js +49 -106
  37. package/dist/loaders/request-loader.d.ts +28 -0
  38. package/dist/loaders/request-loader.js +42 -0
  39. package/dist/loaders/single-event-loader.d.ts +26 -0
  40. package/dist/loaders/single-event-loader.js +76 -0
  41. package/dist/loaders/tag-value-loader.d.ts +33 -0
  42. package/dist/loaders/tag-value-loader.js +75 -0
  43. package/dist/loaders/timeline-loader.d.ts +22 -0
  44. package/dist/loaders/timeline-loader.js +56 -0
  45. package/dist/loaders/user-sets-loader.d.ts +31 -0
  46. package/dist/loaders/user-sets-loader.js +66 -0
  47. package/dist/operators/__tests__/distinct-relays.test.d.ts +1 -0
  48. package/dist/operators/__tests__/distinct-relays.test.js +75 -0
  49. package/dist/operators/__tests__/generator-sequence.test.d.ts +1 -0
  50. package/dist/operators/__tests__/generator-sequence.test.js +38 -0
  51. package/dist/operators/distinct-relays.d.ts +4 -0
  52. package/dist/operators/distinct-relays.js +14 -0
  53. package/dist/operators/distinct-timeout.d.ts +3 -0
  54. package/dist/operators/distinct-timeout.js +15 -0
  55. package/dist/operators/generator-sequence.d.ts +1 -1
  56. package/dist/operators/generator-sequence.js +42 -34
  57. package/dist/operators/index.d.ts +2 -3
  58. package/dist/operators/index.js +2 -3
  59. package/package.json +26 -7
  60. package/dist/loaders/single-relay-replaceable-loader.d.ts +0 -14
  61. package/dist/loaders/single-relay-replaceable-loader.js +0 -51
  62. package/dist/operators/address-pointers-request.d.ts +0 -5
  63. package/dist/operators/address-pointers-request.js +0 -25
  64. package/dist/operators/max-filters.d.ts +0 -4
  65. package/dist/operators/max-filters.js +0 -8
  66. package/dist/operators/relay-request.d.ts +0 -4
  67. 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<number, AddressPointerWithoutD[]> | Map<string, AddressPointerWithoutD[]>;
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
- const groups = groupAddressPointersByPubkeyOrKind(pointers);
17
- return Array.from(groups.values()).map((pointers) => createFilterFromAddressPointers(pointers));
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 > kinds.size ? groupAddressPointersByKind(pointers) : groupAddressPointersByPubkey(pointers);
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
+ }
@@ -1,4 +1,2 @@
1
1
  /** Does not preserve order */
2
2
  export declare function unique<T>(arr: T[]): T[];
3
- /** split array into a set of arrays no larger than batchSize */
4
- export declare function reduceToBatches<T>(arr: T[], batchSize: number): T[][];
@@ -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,5 @@
1
+ /** deduplicates an array of event pointers and merges their relays array */
2
+ export declare function consolidateEventPointers<T extends {
3
+ id: string;
4
+ relays?: string[];
5
+ }>(pointers: T[]): T[];
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ import { RxNostr } from "rx-nostr";
2
+ export declare function getDefaultReadRelays(rxNostr: RxNostr): string[];
@@ -0,0 +1,5 @@
1
+ export function getDefaultReadRelays(rxNostr) {
2
+ return Object.entries(rxNostr.getDefaultRelays())
3
+ .filter(([_, config]) => config.read)
4
+ .map(([relay]) => relay);
5
+ }
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export * as Loaders from "./loaders/index.js";
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 * as Loaders from "./loaders/index.js";
1
+ export * from "./loaders/index.js";
2
2
  export * as Operators from "./operators/index.js";
@@ -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,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
+ }