applesauce-loaders 0.12.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,24 +1,32 @@
1
1
  # applesauce-loaders
2
2
 
3
- A collection of observable based loaders built on top of [rx-nostr](https://penpenpng.github.io/rx-nostr/)
3
+ A collection of loader classes to make loading common events from multiple relays easier.
4
4
 
5
5
  ## Replaceable event loader
6
6
 
7
7
  The `ReplaceableLoader` class can be used to load profiles (kind 0), contact lists (kind 3), and any other replaceable (1xxxx) or parameterized replaceable event (3xxxx)
8
8
 
9
9
  ```ts
10
+ import { Observable } from "rxjs";
10
11
  import { EventStore } from "applesauce-core";
11
12
  import { ReplaceableLoader } from "applesauce-loaders/loaders";
12
- import { createRxNostr, nip07Signer } from "rx-nostr";
13
- import { verifier } from "rx-nostr-crypto";
14
13
 
15
14
  export const eventStore = new EventStore();
16
15
 
17
- export const rxNostr = createRxNostr({
18
- verifier,
19
- signer: nip07Signer(),
20
- connectionStrategy: "lazy-keep",
21
- });
16
+ // Create a method to let the loaders use nostr-tools relay pool
17
+ function nostrRequest(relays: string[], filters: Filter[]) {
18
+ return new Observable((observer) => {
19
+ const sub = pool.subscribe(filters, {
20
+ onevent: (event) => observer.next(event),
21
+ oneose: () => {
22
+ sub.close();
23
+ observer.complete();
24
+ },
25
+ });
26
+
27
+ return () => sub.close();
28
+ });
29
+ }
22
30
 
23
31
  // create method to load events from the cache relay
24
32
  function cacheRequest(filters: Filter[]) {
@@ -1,5 +1,5 @@
1
1
  import { getReplaceableUID, mergeRelaySets } from "applesauce-core/helpers";
2
- import { isParameterizedReplaceableKind, isReplaceableKind } from "nostr-tools/kinds";
2
+ import { isAddressableKind, isReplaceableKind } from "nostr-tools/kinds";
3
3
  import { unique } from "./array.js";
4
4
  /** Converts an array of address pointers to a filter */
5
5
  export function createFilterFromAddressPointers(pointers) {
@@ -14,7 +14,7 @@ export function createFilterFromAddressPointers(pointers) {
14
14
  /** Takes a set of address pointers, groups them, then returns filters for the groups */
15
15
  export function createFiltersFromAddressPointers(pointers) {
16
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));
17
+ const parameterizedReplaceable = pointers.filter((p) => isAddressableKind(p.kind));
18
18
  const replaceable = pointers.filter((p) => isReplaceableKind(p.kind));
19
19
  const filters = [];
20
20
  if (replaceable.length > 0) {
@@ -29,7 +29,7 @@ export function createFiltersFromAddressPointers(pointers) {
29
29
  }
30
30
  /** Checks if a relay will understand an address pointer */
31
31
  export function isLoadableAddressPointer(pointer) {
32
- if (isParameterizedReplaceableKind(pointer.kind))
32
+ if (isAddressableKind(pointer.kind))
33
33
  return !!pointer.identifier;
34
34
  else
35
35
  return isReplaceableKind(pointer.kind);
@@ -1,6 +1,6 @@
1
1
  export declare function groupByRelay<T extends {
2
2
  relays?: string[];
3
- }>(pointers: T[], defaultKey?: string): Map<string, T[]>;
3
+ }>(pointers: T[], extraRelays?: string[]): Map<string, T[]>;
4
4
  export interface MessageWithRelay {
5
5
  relays?: string[];
6
6
  /** Ignore timeout and force message through */
@@ -1,7 +1,8 @@
1
- export function groupByRelay(pointers, defaultKey) {
1
+ import { mergeRelaySets } from "applesauce-core/helpers";
2
+ export function groupByRelay(pointers, extraRelays) {
2
3
  let byRelay = new Map();
3
4
  for (const pointer of pointers) {
4
- let relays = pointer.relays?.length ? pointer.relays : defaultKey ? [defaultKey] : [];
5
+ let relays = mergeRelaySets(pointer.relays, extraRelays);
5
6
  for (const relay of relays) {
6
7
  if (!byRelay.has(relay))
7
8
  byRelay.set(relay, [pointer]);
@@ -1,6 +1,6 @@
1
- import { EventPacket } from "rx-nostr";
2
- import { BehaviorSubject } from "rxjs";
3
1
  import { logger } from "applesauce-core";
2
+ import { NostrEvent } from "nostr-tools";
3
+ import { BehaviorSubject } from "rxjs";
4
4
  import { CacheRequest, Loader } from "./loader.js";
5
5
  import { TimelessFilter } from "./relay-timeline-loader.js";
6
6
  export type CacheTimelineLoaderOptions = {
@@ -8,7 +8,7 @@ export type CacheTimelineLoaderOptions = {
8
8
  limit?: number;
9
9
  };
10
10
  /** A loader that can be used to load a timeline in chunks */
11
- export declare class CacheTimelineLoader extends Loader<number | void, EventPacket> {
11
+ export declare class CacheTimelineLoader extends Loader<number | void, NostrEvent> {
12
12
  filters: TimelessFilter[];
13
13
  id: string;
14
14
  loading$: BehaviorSubject<boolean>;
@@ -1,7 +1,7 @@
1
- import { BehaviorSubject, filter, map, mergeMap, tap } from "rxjs";
2
- import { markFromCache, unixNow } from "applesauce-core/helpers";
3
1
  import { logger } from "applesauce-core";
2
+ import { markFromCache, unixNow } from "applesauce-core/helpers";
4
3
  import { nanoid } from "nanoid";
4
+ import { BehaviorSubject, filter, map, mergeMap, tap } from "rxjs";
5
5
  import { Loader } from "./loader.js";
6
6
  /** A loader that can be used to load a timeline in chunks */
7
7
  export class CacheTimelineLoader extends Loader {
@@ -52,7 +52,7 @@ export class CacheTimelineLoader extends Loader {
52
52
  this.log(`Finished batch, got ${count} events`);
53
53
  }
54
54
  },
55
- }), map((event) => ({ event, from: "", subId: "cache-timeline-loader", type: "EVENT" })));
55
+ }));
56
56
  })));
57
57
  this.filters = filters;
58
58
  // create a unique logger for this instance
@@ -6,5 +6,4 @@ export * from "./timeline-loader.js";
6
6
  export * from "./relay-timeline-loader.js";
7
7
  export * from "./cache-timeline-loader.js";
8
8
  export * from "./tag-value-loader.js";
9
- export * from "./request-loader.js";
10
9
  export * from "./dns-identity-loader.js";
@@ -6,5 +6,4 @@ export * from "./timeline-loader.js";
6
6
  export * from "./relay-timeline-loader.js";
7
7
  export * from "./cache-timeline-loader.js";
8
8
  export * from "./tag-value-loader.js";
9
- export * from "./request-loader.js";
10
9
  export * from "./dns-identity-loader.js";
@@ -4,6 +4,8 @@ export type RelayFilterMap<T = Filter> = {
4
4
  [relay: string]: T[];
5
5
  };
6
6
  export type CacheRequest = (filters: Filter[]) => Observable<NostrEvent>;
7
+ export type NostrResponse = NostrEvent | "EOSE";
8
+ export type NostrRequest = (relays: string[], filters: Filter[], id?: string) => Observable<NostrResponse>;
7
9
  export interface ILoader<T, R> extends Subscribable<R> {
8
10
  next: (value: T) => void;
9
11
  pipe: Observable<R>["pipe"];
@@ -1,15 +1,14 @@
1
- import { EventPacket, RxNostr } from "rx-nostr";
2
- import { BehaviorSubject } from "rxjs";
3
1
  import { logger } from "applesauce-core";
4
- import { Filter } from "nostr-tools";
5
- import { Loader } from "./loader.js";
2
+ import { Filter, NostrEvent } from "nostr-tools";
3
+ import { BehaviorSubject } from "rxjs";
4
+ import { Loader, NostrRequest } from "./loader.js";
6
5
  export type TimelessFilter = Omit<Filter, "since" | "until">;
7
6
  export type RelayTimelineLoaderOptions = {
8
7
  /** default number of events to request in each batch */
9
8
  limit?: number;
10
9
  };
11
10
  /** A loader that can be used to load a timeline in chunks */
12
- export declare class RelayTimelineLoader extends Loader<number | void, EventPacket> {
11
+ export declare class RelayTimelineLoader extends Loader<number | void, NostrEvent> {
13
12
  relay: string;
14
13
  filters: TimelessFilter[];
15
14
  id: string;
@@ -20,5 +19,5 @@ export declare class RelayTimelineLoader extends Loader<number | void, EventPack
20
19
  /** if the timeline is complete */
21
20
  complete: boolean;
22
21
  protected log: typeof logger;
23
- constructor(rxNostr: RxNostr, relay: string, filters: TimelessFilter[], opts?: RelayTimelineLoaderOptions);
22
+ constructor(request: NostrRequest, relay: string, filters: TimelessFilter[], opts?: RelayTimelineLoaderOptions);
24
23
  }
@@ -1,8 +1,8 @@
1
- import { createRxOneshotReq } from "rx-nostr";
2
- import { BehaviorSubject, filter, map, Observable } from "rxjs";
3
1
  import { logger } from "applesauce-core";
4
- import { nanoid } from "nanoid";
5
2
  import { unixNow } from "applesauce-core/helpers";
3
+ import { nanoid } from "nanoid";
4
+ import { BehaviorSubject, filter, map, Observable } from "rxjs";
5
+ import { completeOnEOSE } from "../operators/complete-on-eose.js";
6
6
  import { Loader } from "./loader.js";
7
7
  /** A loader that can be used to load a timeline in chunks */
8
8
  export class RelayTimelineLoader extends Loader {
@@ -18,7 +18,7 @@ export class RelayTimelineLoader extends Loader {
18
18
  /** if the timeline is complete */
19
19
  complete = false;
20
20
  log = logger.extend("RelayTimelineLoader");
21
- constructor(rxNostr, relay, filters, opts) {
21
+ constructor(request, relay, filters, opts) {
22
22
  super((source) => new Observable((observer) => {
23
23
  return source
24
24
  .pipe(filter(() => !this.loading && !this.complete), map((limit) => {
@@ -35,16 +35,17 @@ export class RelayTimelineLoader extends Loader {
35
35
  .subscribe((filters) => {
36
36
  // make batch request
37
37
  let count = 0;
38
- const req = createRxOneshotReq({ filters, rxReqId: this.id });
39
38
  this.loading$.next(true);
40
39
  this.log(`Next batch starting at ${filters[0].until} limit ${filters[0].limit}`);
41
- rxNostr.use(req, { on: { relays: [relay] } }).subscribe({
42
- next: (packet) => {
40
+ request([relay], filters)
41
+ .pipe(completeOnEOSE())
42
+ .subscribe({
43
+ next: (event) => {
43
44
  // update cursor when event is received
44
- this.cursor = Math.min(packet.event.created_at - 1, this.cursor);
45
+ this.cursor = Math.min(event.created_at - 1, this.cursor);
45
46
  count++;
46
47
  // forward packet
47
- observer.next(packet);
48
+ observer.next(event);
48
49
  },
49
50
  error: (err) => observer.error(err),
50
51
  complete: () => {
@@ -1,7 +1,7 @@
1
- import { EventPacket, RxNostr } from "rx-nostr";
2
1
  import { logger } from "applesauce-core";
3
- import { CacheRequest, Loader } from "./loader.js";
2
+ import { NostrEvent } from "nostr-tools";
4
3
  import { LoadableAddressPointer } from "../helpers/address-pointer.js";
4
+ import { CacheRequest, Loader, NostrRequest } from "./loader.js";
5
5
  export type ReplaceableLoaderOptions = {
6
6
  /**
7
7
  * Time interval to buffer requests in ms
@@ -12,12 +12,16 @@ export type ReplaceableLoaderOptions = {
12
12
  cacheRequest?: CacheRequest;
13
13
  /** Fallback lookup relays to check when event cant be found */
14
14
  lookupRelays?: string[];
15
+ /** An array of relays to always fetch from */
16
+ extraRelays?: string[];
15
17
  };
16
- export declare class ReplaceableLoader extends Loader<LoadableAddressPointer, EventPacket> {
18
+ export declare class ReplaceableLoader extends Loader<LoadableAddressPointer, NostrEvent> {
17
19
  log: typeof logger;
18
20
  /** A method used to load events from a local cache */
19
21
  cacheRequest?: CacheRequest;
20
22
  /** Fallback lookup relays to check when event cant be found */
21
23
  lookupRelays?: string[];
22
- constructor(rxNostr: RxNostr, opts?: ReplaceableLoaderOptions);
24
+ /** An array of relays to always fetch from */
25
+ extraRelays?: string[];
26
+ constructor(request: NostrRequest, opts?: ReplaceableLoaderOptions);
23
27
  }
@@ -1,22 +1,21 @@
1
- import { tap, filter, bufferTime, map } from "rxjs";
2
- import { createRxOneshotReq } from "rx-nostr";
3
- import { getEventUID, markFromCache, mergeRelaySets } from "applesauce-core/helpers";
4
1
  import { logger } from "applesauce-core";
2
+ import { getEventUID, markFromCache, mergeRelaySets } from "applesauce-core/helpers";
5
3
  import { nanoid } from "nanoid";
6
- import { Loader } from "./loader.js";
7
- import { generatorSequence } from "../operators/generator-sequence.js";
4
+ import { bufferTime, filter, map, tap } from "rxjs";
8
5
  import { consolidateAddressPointers, createFiltersFromAddressPointers, getAddressPointerId, getRelaysFromPointers, isLoadableAddressPointer, } from "../helpers/address-pointer.js";
9
- import { getDefaultReadRelays } from "../helpers/rx-nostr.js";
6
+ import { completeOnEOSE } from "../operators/complete-on-eose.js";
10
7
  import { distinctRelaysBatch } from "../operators/distinct-relays.js";
8
+ import { generatorSequence } from "../operators/generator-sequence.js";
9
+ import { Loader } from "./loader.js";
11
10
  /** A generator that tries to load the address pointers from the cache first, then tries the relays */
12
- function* cacheFirstSequence(rxNostr, pointers, log, opts) {
11
+ function* cacheFirstSequence(request, pointers, log, opts) {
13
12
  const id = nanoid(4);
14
13
  let remaining = Array.from(pointers);
15
14
  const pointerRelays = Array.from(getRelaysFromPointers(pointers));
16
15
  // handle previous step results and decide if to exit
17
16
  const handleResults = (results) => {
18
17
  if (results.length) {
19
- const coordinates = new Set(results.map((p) => getEventUID(p.event)));
18
+ const coordinates = new Set(results.map((event) => getEventUID(event)));
20
19
  // if there where results, filter out any pointers that where found
21
20
  remaining = remaining.filter((pointer) => {
22
21
  const found = coordinates.has(getAddressPointerId(pointer));
@@ -39,20 +38,16 @@ function* cacheFirstSequence(rxNostr, pointers, log, opts) {
39
38
  const filters = createFiltersFromAddressPointers(remaining);
40
39
  const results = yield opts.cacheRequest(filters).pipe(
41
40
  // mark the event as from the cache
42
- tap((event) => markFromCache(event)),
43
- // convert to event packets
44
- map((e) => ({ event: e, from: "", subId: "replaceable-loader", type: "EVENT" })));
41
+ tap((event) => markFromCache(event)));
45
42
  if (handleResults(results))
46
43
  return;
47
44
  }
48
- // load from pointer relays and default relays
49
- const defaultRelays = getDefaultReadRelays(rxNostr);
50
- const remoteRelays = mergeRelaySets(pointerRelays, defaultRelays);
51
- if (remoteRelays.length > 0) {
52
- log(`[${id}] Requesting`, remoteRelays, remaining);
45
+ // load from pointer relays and extra relays
46
+ const mergedRelays = mergeRelaySets(pointerRelays, opts?.extraRelays);
47
+ if (mergedRelays.length > 0) {
48
+ log(`[${id}] Requesting`, mergedRelays, remaining);
53
49
  const filters = createFiltersFromAddressPointers(remaining);
54
- const req = createRxOneshotReq({ filters, rxReqId: id });
55
- const results = yield rxNostr.use(req, { on: { relays: remoteRelays } });
50
+ const results = yield request(mergedRelays, filters).pipe(completeOnEOSE());
56
51
  if (handleResults(results))
57
52
  return;
58
53
  }
@@ -63,8 +58,7 @@ function* cacheFirstSequence(rxNostr, pointers, log, opts) {
63
58
  if (relays.length > 0) {
64
59
  log(`[${id}] Request from lookup`, relays, remaining);
65
60
  const filters = createFiltersFromAddressPointers(remaining);
66
- const req = createRxOneshotReq({ filters, rxReqId: id });
67
- const results = yield rxNostr.use(req, { on: { relays } });
61
+ const results = yield request(relays, filters).pipe(completeOnEOSE());
68
62
  if (handleResults(results))
69
63
  return;
70
64
  }
@@ -76,7 +70,9 @@ export class ReplaceableLoader extends Loader {
76
70
  cacheRequest;
77
71
  /** Fallback lookup relays to check when event cant be found */
78
72
  lookupRelays;
79
- constructor(rxNostr, opts) {
73
+ /** An array of relays to always fetch from */
74
+ extraRelays;
75
+ constructor(request, opts) {
80
76
  super((source) => {
81
77
  return source.pipe(
82
78
  // filter out invalid pointers
@@ -92,9 +88,10 @@ export class ReplaceableLoader extends Loader {
92
88
  // ignore empty buffer
93
89
  filter((buffer) => buffer.length > 0),
94
90
  // check cache, relays, lookup relays in that order
95
- generatorSequence((pointers) => cacheFirstSequence(rxNostr, pointers, this.log, {
91
+ generatorSequence((pointers) => cacheFirstSequence(request, pointers, this.log, {
96
92
  cacheRequest: this.cacheRequest,
97
93
  lookupRelays: this.lookupRelays,
94
+ extraRelays: this.extraRelays,
98
95
  }),
99
96
  // there will always be more events, never complete
100
97
  false));
@@ -102,5 +99,6 @@ export class ReplaceableLoader extends Loader {
102
99
  // set options
103
100
  this.cacheRequest = opts?.cacheRequest;
104
101
  this.lookupRelays = opts?.lookupRelays;
102
+ this.extraRelays = opts?.extraRelays;
105
103
  }
106
104
  }
@@ -1,6 +1,6 @@
1
- import { EventPacket, RxNostr } from "rx-nostr";
2
1
  import { logger } from "applesauce-core";
3
- import { CacheRequest, Loader } from "./loader.js";
2
+ import { NostrEvent } from "nostr-tools";
3
+ import { CacheRequest, Loader, NostrRequest } from "./loader.js";
4
4
  export type LoadableEventPointer = {
5
5
  id: string;
6
6
  /** Relays to load from */
@@ -12,15 +12,21 @@ export type SingleEventLoaderOptions = {
12
12
  * @default 1000
13
13
  */
14
14
  bufferTime?: number;
15
- /** A method used to load events from a local cache */
16
- cacheRequest?: CacheRequest;
17
15
  /**
18
16
  * How long the loader should wait before it allows an event pointer to be refreshed from a relay
19
17
  * @default 60000
20
18
  */
21
19
  refreshTimeout?: number;
20
+ /** A method used to load events from a local cache */
21
+ cacheRequest?: CacheRequest;
22
+ /** An array of relays to always fetch from */
23
+ extraRelays?: string[];
22
24
  };
23
- export declare class SingleEventLoader extends Loader<LoadableEventPointer, EventPacket> {
25
+ export declare class SingleEventLoader extends Loader<LoadableEventPointer, NostrEvent> {
24
26
  log: typeof logger;
25
- constructor(rxNostr: RxNostr, opts?: SingleEventLoaderOptions);
27
+ /** A method used to load events from a local cache */
28
+ cacheRequest?: CacheRequest;
29
+ /** An array of relays to always fetch from */
30
+ extraRelays?: string[];
31
+ constructor(request: NostrRequest, opts?: SingleEventLoaderOptions);
26
32
  }
@@ -1,5 +1,4 @@
1
1
  import { bufferTime, filter, from, map, mergeAll, tap } from "rxjs";
2
- import { createRxOneshotReq } from "rx-nostr";
3
2
  import { markFromCache } from "applesauce-core/helpers";
4
3
  import { logger } from "applesauce-core";
5
4
  import { nanoid } from "nanoid";
@@ -8,21 +7,20 @@ import { generatorSequence } from "../operators/generator-sequence.js";
8
7
  import { distinctRelaysBatch } from "../operators/distinct-relays.js";
9
8
  import { groupByRelay } from "../helpers/pointer.js";
10
9
  import { consolidateEventPointers } from "../helpers/event-pointer.js";
11
- function* cacheFirstSequence(rxNostr, pointers, opts, log) {
10
+ import { completeOnEOSE } from "../operators/complete-on-eose.js";
11
+ function* cacheFirstSequence(request, pointers, opts, log) {
12
12
  let remaining = [...pointers];
13
13
  const id = nanoid(8);
14
14
  log = log.extend(id);
15
15
  const loaded = (packets) => {
16
- const ids = new Set(packets.map((p) => p.event.id));
16
+ const ids = new Set(packets.map((p) => p.id));
17
17
  remaining = remaining.filter((p) => !ids.has(p.id));
18
18
  };
19
19
  if (opts?.cacheRequest) {
20
20
  let filter = { ids: remaining.map((e) => e.id) };
21
21
  const results = yield opts.cacheRequest([filter]).pipe(
22
22
  // mark the event as from the cache
23
- tap((event) => markFromCache(event)),
24
- // convert to event packets
25
- map((e) => ({ event: e, from: "", subId: "single-event-loader", type: "EVENT" })));
23
+ tap((event) => markFromCache(event)));
26
24
  if (results.length > 0) {
27
25
  log(`Loaded ${results.length} events from cache`);
28
26
  loaded(results);
@@ -31,20 +29,15 @@ function* cacheFirstSequence(rxNostr, pointers, opts, log) {
31
29
  // exit early if all pointers are loaded
32
30
  if (remaining.length === 0)
33
31
  return;
34
- let byRelay = groupByRelay(remaining, "default");
32
+ let byRelay = groupByRelay(remaining, opts.extraRelays);
35
33
  // load remaining pointers from the relays
36
34
  let results = yield from(Array.from(byRelay.entries()).map(([relay, pointers]) => {
37
35
  let filter = { ids: pointers.map((e) => e.id) };
38
36
  let count = 0;
39
- const req = createRxOneshotReq({ filters: [filter], rxReqId: id });
40
37
  log(`Requesting from ${relay}`, filter.ids);
41
- let sub$;
42
- // don't specify relay if this is the "default" relay
43
- if (relay === "default")
44
- sub$ = rxNostr.use(req);
45
- else
46
- sub$ = rxNostr.use(req, { on: { relays: [relay] } });
47
- return sub$.pipe(tap({
38
+ return request([relay], [filter], id)
39
+ .pipe(completeOnEOSE())
40
+ .pipe(tap({
48
41
  next: () => count++,
49
42
  complete: () => log(`Completed ${relay}, loaded ${count} events`),
50
43
  }));
@@ -57,20 +50,25 @@ function* cacheFirstSequence(rxNostr, pointers, opts, log) {
57
50
  }
58
51
  export class SingleEventLoader extends Loader {
59
52
  log = logger.extend("SingleEventLoader");
60
- constructor(rxNostr, opts) {
61
- let options = opts || {};
53
+ /** A method used to load events from a local cache */
54
+ cacheRequest;
55
+ /** An array of relays to always fetch from */
56
+ extraRelays;
57
+ constructor(request, opts) {
62
58
  super((source) => source.pipe(
63
- // load first from cache
59
+ // batch every second
64
60
  bufferTime(opts?.bufferTime ?? 1000),
65
61
  // ignore empty buffers
66
62
  filter((buffer) => buffer.length > 0),
67
63
  // only request events from relays once
68
- distinctRelaysBatch((p) => p.id, options.refreshTimeout ?? 60_000),
64
+ distinctRelaysBatch((p) => p.id, opts?.refreshTimeout ?? 60_000),
69
65
  // ensure there is only one of each event pointer
70
66
  map(consolidateEventPointers),
71
67
  // run the loader sequence
72
- generatorSequence((pointers) => cacheFirstSequence(rxNostr, pointers, options, this.log),
68
+ generatorSequence((pointers) => cacheFirstSequence(request, pointers, { cacheRequest: this.cacheRequest, extraRelays: this.extraRelays }, this.log),
73
69
  // there will always be more events, never complete
74
70
  false)));
71
+ this.cacheRequest = opts?.cacheRequest;
72
+ this.extraRelays = opts?.extraRelays;
75
73
  }
76
74
  }
@@ -1,6 +1,6 @@
1
- import { EventPacket, RxNostr } from "rx-nostr";
2
1
  import { logger } from "applesauce-core";
3
- import { CacheRequest, Loader } from "./loader.js";
2
+ import { NostrEvent } from "nostr-tools";
3
+ import { CacheRequest, Loader, NostrRequest } from "./loader.js";
4
4
  export type TabValuePointer = {
5
5
  /** The value of the tag to load */
6
6
  value: string;
@@ -25,9 +25,15 @@ export type TagValueLoaderOptions = {
25
25
  since?: number;
26
26
  /** Method used to load from the cache */
27
27
  cacheRequest?: CacheRequest;
28
+ /** An array of relays to always fetch from */
29
+ extraRelays?: string[];
28
30
  };
29
- export declare class TagValueLoader extends Loader<TabValuePointer, EventPacket> {
31
+ export declare class TagValueLoader extends Loader<TabValuePointer, NostrEvent> {
30
32
  name: string;
31
33
  protected log: typeof logger;
32
- constructor(rxNostr: RxNostr, tagName: string, opts?: TagValueLoaderOptions);
34
+ /** A method to load events from a local cache */
35
+ cacheRequest?: CacheRequest;
36
+ /** An array of relays to always fetch from */
37
+ extraRelays?: string[];
38
+ constructor(request: NostrRequest, tagName: string, opts?: TagValueLoaderOptions);
33
39
  }
@@ -1,15 +1,18 @@
1
- import { createRxOneshotReq } from "rx-nostr";
2
- import { bufferTime, filter, map, merge, mergeMap, tap } from "rxjs";
3
- import { markFromCache } from "applesauce-core/helpers";
4
1
  import { logger } from "applesauce-core";
5
- import { Loader } from "./loader.js";
6
- import { distinctRelaysBatch } from "../operators/distinct-relays.js";
7
- import { getDefaultReadRelays } from "../helpers/rx-nostr.js";
2
+ import { markFromCache, mergeRelaySets } from "applesauce-core/helpers";
3
+ import { bufferTime, filter, merge, mergeMap, tap } from "rxjs";
8
4
  import { unique } from "../helpers/array.js";
5
+ import { completeOnEOSE } from "../operators/complete-on-eose.js";
6
+ import { distinctRelaysBatch } from "../operators/distinct-relays.js";
7
+ import { Loader } from "./loader.js";
9
8
  export class TagValueLoader extends Loader {
10
9
  name;
11
10
  log = logger.extend("TagValueLoader");
12
- constructor(rxNostr, tagName, opts) {
11
+ /** A method to load events from a local cache */
12
+ cacheRequest;
13
+ /** An array of relays to always fetch from */
14
+ extraRelays;
15
+ constructor(request, tagName, opts) {
13
16
  const filterTag = `#${tagName}`;
14
17
  super((source) => source.pipe(
15
18
  // batch the pointers
@@ -29,7 +32,7 @@ export class TagValueLoader extends Loader {
29
32
  baseFilter.authors = opts.authors;
30
33
  // build request map for relays
31
34
  const requestMap = pointers.reduce((map, pointer) => {
32
- const relays = pointer.relays ?? getDefaultReadRelays(rxNostr);
35
+ const relays = mergeRelaySets(pointer.relays, this.extraRelays);
33
36
  for (const relay of relays) {
34
37
  if (!map[relay]) {
35
38
  // create new filter for relay
@@ -45,9 +48,9 @@ export class TagValueLoader extends Loader {
45
48
  return map;
46
49
  }, {});
47
50
  let fromCache = 0;
48
- const cacheRequest = opts
49
- ?.cacheRequest?.([{ ...baseFilter, [filterTag]: unique(pointers.map((p) => p.value)) }])
50
- .pipe(
51
+ const cacheRequest = this?.cacheRequest?.([
52
+ { ...baseFilter, [filterTag]: unique(pointers.map((p) => p.value)) },
53
+ ]).pipe(
51
54
  // mark the event as from the cache
52
55
  tap({
53
56
  next: (event) => {
@@ -58,16 +61,14 @@ export class TagValueLoader extends Loader {
58
61
  if (fromCache > 0)
59
62
  this.log(`Loaded ${fromCache} from cache`);
60
63
  },
61
- }),
62
- // convert to event packets
63
- map((e) => ({ event: e, from: "", subId: "replaceable-loader", type: "EVENT" })));
64
- const requests = Object.entries(requestMap).map(([relay, filters]) => {
65
- const req = createRxOneshotReq({ filters });
66
- return rxNostr.use(req, { on: { relays: [relay] } });
67
- });
64
+ }));
65
+ const requests = Object.entries(requestMap).map(([relay, filters]) => request([relay], filters).pipe(completeOnEOSE()));
68
66
  this.log(`Requesting ${pointers.length} tag values from ${requests.length} relays`);
69
67
  return cacheRequest ? merge(cacheRequest, ...requests) : merge(...requests);
70
68
  })));
69
+ // Set options
70
+ this.cacheRequest = opts?.cacheRequest;
71
+ this.extraRelays = opts?.extraRelays;
71
72
  // create a unique logger for this instance
72
73
  this.name = opts?.name ?? "";
73
74
  this.log = this.log.extend(opts?.kinds ? `${this.name} ${filterTag} (${opts?.kinds?.join(",")})` : `${this.name} ${filterTag}`);
@@ -1,15 +1,15 @@
1
- import { EventPacket, RxNostr } from "rx-nostr";
2
- import { BehaviorSubject } from "rxjs";
3
1
  import { logger } from "applesauce-core";
4
- import { RelayTimelineLoader, TimelessFilter } from "./relay-timeline-loader.js";
5
- import { CacheRequest, Loader, RelayFilterMap } from "./loader.js";
2
+ import { NostrEvent } from "nostr-tools";
3
+ import { BehaviorSubject } from "rxjs";
6
4
  import { CacheTimelineLoader } from "./cache-timeline-loader.js";
5
+ import { CacheRequest, Loader, NostrRequest, RelayFilterMap } from "./loader.js";
6
+ import { RelayTimelineLoader, TimelessFilter } from "./relay-timeline-loader.js";
7
7
  export type TimelineLoaderOptions = {
8
8
  limit?: number;
9
9
  cacheRequest?: CacheRequest;
10
10
  };
11
11
  /** A multi-relay timeline loader that can be used to load a timeline from multiple relays */
12
- export declare class TimelineLoader extends Loader<number | undefined, EventPacket> {
12
+ export declare class TimelineLoader extends Loader<number | undefined, NostrEvent> {
13
13
  id: string;
14
14
  loading$: BehaviorSubject<boolean>;
15
15
  get loading(): boolean;
@@ -17,6 +17,6 @@ export declare class TimelineLoader extends Loader<number | undefined, EventPack
17
17
  protected log: typeof logger;
18
18
  protected cache?: CacheTimelineLoader;
19
19
  protected loaders: Map<string, RelayTimelineLoader>;
20
- constructor(rxNostr: RxNostr, requests: RelayFilterMap<TimelessFilter>, opts?: TimelineLoaderOptions);
20
+ constructor(request: NostrRequest, requests: RelayFilterMap<TimelessFilter>, opts?: TimelineLoaderOptions);
21
21
  static simpleFilterMap(relays: string[], filters: TimelessFilter[]): RelayFilterMap<TimelessFilter>;
22
22
  }
@@ -1,10 +1,10 @@
1
- import { BehaviorSubject, combineLatest, connect, merge, tap } from "rxjs";
2
1
  import { logger } from "applesauce-core";
3
2
  import { mergeFilters } from "applesauce-core/helpers";
4
3
  import { nanoid } from "nanoid";
5
- import { RelayTimelineLoader } from "./relay-timeline-loader.js";
6
- import { Loader } from "./loader.js";
4
+ import { BehaviorSubject, combineLatest, connect, merge, tap } from "rxjs";
7
5
  import { CacheTimelineLoader } from "./cache-timeline-loader.js";
6
+ import { Loader } from "./loader.js";
7
+ import { RelayTimelineLoader } from "./relay-timeline-loader.js";
8
8
  /** A multi-relay timeline loader that can be used to load a timeline from multiple relays */
9
9
  export class TimelineLoader extends Loader {
10
10
  id = nanoid(8);
@@ -16,7 +16,7 @@ export class TimelineLoader extends Loader {
16
16
  log = logger.extend("TimelineLoader");
17
17
  cache;
18
18
  loaders;
19
- constructor(rxNostr, requests, opts) {
19
+ constructor(request, requests, opts) {
20
20
  const loaders = new Map();
21
21
  // create cache loader
22
22
  const cache = opts?.cacheRequest
@@ -24,7 +24,7 @@ export class TimelineLoader extends Loader {
24
24
  : undefined;
25
25
  // create loaders
26
26
  for (const [relay, filters] of Object.entries(requests)) {
27
- loaders.set(relay, new RelayTimelineLoader(rxNostr, relay, filters, opts));
27
+ loaders.set(relay, new RelayTimelineLoader(request, relay, filters, opts));
28
28
  }
29
29
  const allLoaders = cache ? [cache, ...loaders.values()] : Array.from(loaders.values());
30
30
  super((source) => {
@@ -1,6 +1,6 @@
1
- import { EventPacket, RxNostr } from "rx-nostr";
2
1
  import { logger } from "applesauce-core";
3
- import { CacheRequest, Loader } from "./loader.js";
2
+ import { NostrEvent } from "nostr-tools";
3
+ import { CacheRequest, Loader, NostrRequest } from "./loader.js";
4
4
  export type LoadableSetPointer = {
5
5
  /** A replaceable kind >= 30000 & < 40000 */
6
6
  kind: number;
@@ -25,7 +25,9 @@ export type UserSetsLoaderOptions = {
25
25
  refreshTimeout?: number;
26
26
  };
27
27
  /** A loader that can be used to load users NIP-51 sets events ( kind >= 30000 < 40000) */
28
- export declare class UserSetsLoader extends Loader<LoadableSetPointer, EventPacket> {
28
+ export declare class UserSetsLoader extends Loader<LoadableSetPointer, NostrEvent> {
29
29
  log: typeof logger;
30
- constructor(rxNostr: RxNostr, opts?: UserSetsLoaderOptions);
30
+ /** An array of relays to always fetch from */
31
+ extraRelays?: string[];
32
+ constructor(request: NostrRequest, opts?: UserSetsLoaderOptions);
31
33
  }
@@ -1,15 +1,15 @@
1
- import { tap, from, filter, map, mergeAll, bufferTime } from "rxjs";
2
- import { createRxOneshotReq } from "rx-nostr";
3
- import { markFromCache } from "applesauce-core/helpers";
4
1
  import { logger } from "applesauce-core";
2
+ import { markFromCache } from "applesauce-core/helpers";
5
3
  import { nanoid } from "nanoid";
6
- import { Loader } from "./loader.js";
7
- import { generatorSequence } from "../operators/generator-sequence.js";
4
+ import { bufferTime, filter, from, map, mergeAll, tap } from "rxjs";
8
5
  import { consolidateAddressPointers, createFiltersFromAddressPointers } from "../helpers/address-pointer.js";
9
6
  import { groupByRelay } from "../helpers/pointer.js";
7
+ import { completeOnEOSE } from "../operators/complete-on-eose.js";
10
8
  import { distinctRelaysBatch } from "../operators/distinct-relays.js";
9
+ import { generatorSequence } from "../operators/generator-sequence.js";
10
+ import { Loader } from "./loader.js";
11
11
  /** A generator that tries to load the address pointers from the cache first, then tries the relays */
12
- function* cacheFirstSequence(rxNostr, pointers, log, opts) {
12
+ function* cacheFirstSequence(request, pointers, log, opts) {
13
13
  const id = nanoid(8);
14
14
  log = log.extend(id);
15
15
  // first attempt, load from cache relays
@@ -18,27 +18,18 @@ function* cacheFirstSequence(rxNostr, pointers, log, opts) {
18
18
  const filters = createFiltersFromAddressPointers(pointers);
19
19
  const results = yield opts.cacheRequest(filters).pipe(
20
20
  // mark the event as from the cache
21
- tap((event) => markFromCache(event)),
22
- // convert to event packets
23
- map((e) => ({ event: e, from: "", subId: "user-sets-loader", type: "EVENT" })));
21
+ tap((event) => markFromCache(event)));
24
22
  if (results.length > 0) {
25
23
  log(`Loaded ${results.length} events from cache`);
26
24
  }
27
25
  }
28
- let byRelay = groupByRelay(pointers, "default");
26
+ let byRelay = groupByRelay(pointers, opts?.extraRelays);
29
27
  // load sets from relays
30
28
  yield from(Array.from(byRelay.entries()).map(([relay, pointers]) => {
31
29
  let filters = createFiltersFromAddressPointers(pointers);
32
30
  let count = 0;
33
- const req = createRxOneshotReq({ filters, rxReqId: id });
34
31
  log(`Requesting from ${relay}`, pointers);
35
- let sub$;
36
- // don't specify relay if this is the "default" relay
37
- if (relay === "default")
38
- sub$ = rxNostr.use(req);
39
- else
40
- sub$ = rxNostr.use(req, { on: { relays: [relay] } });
41
- return sub$.pipe(tap({
32
+ return request([relay], filters, id).pipe(completeOnEOSE(), tap({
42
33
  next: () => count++,
43
34
  complete: () => log(`Completed ${relay}, loaded ${count} events`),
44
35
  }));
@@ -47,7 +38,9 @@ function* cacheFirstSequence(rxNostr, pointers, log, opts) {
47
38
  /** A loader that can be used to load users NIP-51 sets events ( kind >= 30000 < 40000) */
48
39
  export class UserSetsLoader extends Loader {
49
40
  log = logger.extend("UserSetsLoader");
50
- constructor(rxNostr, opts) {
41
+ /** An array of relays to always fetch from */
42
+ extraRelays;
43
+ constructor(request, opts) {
51
44
  let options = opts || {};
52
45
  super((source) => source.pipe(
53
46
  // load first from cache
@@ -59,7 +52,7 @@ export class UserSetsLoader extends Loader {
59
52
  // deduplicate address pointers
60
53
  map(consolidateAddressPointers),
61
54
  // check cache, relays, lookup relays in that order
62
- generatorSequence((pointers) => cacheFirstSequence(rxNostr, pointers, this.log, options),
55
+ generatorSequence((pointers) => cacheFirstSequence(request, pointers, this.log, options),
63
56
  // there will always be more events, never complete
64
57
  false)));
65
58
  }
@@ -0,0 +1,3 @@
1
+ import { OperatorFunction } from "rxjs";
2
+ /** Completes the observable when an EOSE message is received */
3
+ export declare function completeOnEOSE<T extends unknown>(): OperatorFunction<T | "EOSE", T>;
@@ -0,0 +1,5 @@
1
+ import { takeWhile } from "rxjs";
2
+ /** Completes the observable when an EOSE message is received */
3
+ export function completeOnEOSE() {
4
+ return (source) => source.pipe(takeWhile((m) => m !== "EOSE", false));
5
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-loaders",
3
- "version": "0.12.0",
3
+ "version": "1.0.0",
4
4
  "description": "A collection of observable based loaders built on rx-nostr",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -53,18 +53,16 @@
53
53
  }
54
54
  },
55
55
  "dependencies": {
56
- "applesauce-core": "^0.12.0",
56
+ "applesauce-core": "^1.0.0",
57
57
  "nanoid": "^5.0.9",
58
58
  "nostr-tools": "^2.10.4",
59
- "rx-nostr": "^3.5.0",
60
59
  "rxjs": "^7.8.1"
61
60
  },
62
61
  "devDependencies": {
63
- "rx-nostr-crypto": "^3.1.3",
64
- "typescript": "^5.7.3",
65
- "vitest": "^3.0.5",
62
+ "typescript": "^5.8.3",
63
+ "vitest": "^3.1.1",
66
64
  "vitest-nostr": "^0.4.1",
67
- "vitest-websocket-mock": "^0.4.0"
65
+ "vitest-websocket-mock": "^0.5.0"
68
66
  },
69
67
  "funding": {
70
68
  "type": "lightning",
@@ -1,2 +0,0 @@
1
- import { RxNostr } from "rx-nostr";
2
- export declare function getDefaultReadRelays(rxNostr: RxNostr): string[];
@@ -1,5 +0,0 @@
1
- export function getDefaultReadRelays(rxNostr) {
2
- return Object.entries(rxNostr.getDefaultRelays())
3
- .filter(([_, config]) => config.read)
4
- .map(([relay]) => relay);
5
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,37 +0,0 @@
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
- });
@@ -1,30 +0,0 @@
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
- }
@@ -1,57 +0,0 @@
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
- }