applesauce-loaders 4.0.0 → 5.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
@@ -94,6 +94,51 @@ eventLoader({
94
94
  });
95
95
  ```
96
96
 
97
+ ## Unified Event Loader
98
+
99
+ The Unified Event Loader is a single loader that can handle both `EventPointer` and `AddressPointer` types. It automatically routes to the appropriate loader (`createEventLoader` for events by ID, `createAddressLoader` for addressable/replaceable events) based on the pointer type.
100
+
101
+ This is the recommended approach when setting up loaders for an EventStore, as it provides a single loader that works with the unified `eventLoader` property.
102
+
103
+ ```ts
104
+ import { createUnifiedEventLoader, createEventLoaderForStore } from "applesauce-loaders/loaders";
105
+ import { EventStore } from "applesauce-core";
106
+ import { RelayPool } from "applesauce-relay";
107
+
108
+ const eventStore = new EventStore();
109
+ const pool = new RelayPool();
110
+
111
+ // Option 1: Create and assign manually
112
+ const unifiedLoader = createUnifiedEventLoader(pool, {
113
+ eventStore,
114
+ bufferTime: 1000,
115
+ followRelayHints: true,
116
+ extraRelays: ["wss://relay.example.com"],
117
+ lookupRelays: ["wss://purplepag.es", "wss://index.hzrd149.com"],
118
+ });
119
+
120
+ eventStore.eventLoader = unifiedLoader;
121
+
122
+ // Option 2: Use the convenience function (recommended)
123
+ createEventLoaderForStore(eventStore, pool, {
124
+ bufferTime: 1000,
125
+ followRelayHints: true,
126
+ extraRelays: ["wss://relay.example.com"],
127
+ lookupRelays: ["wss://purplepag.es", "wss://index.hzrd149.com"],
128
+ });
129
+
130
+ // Now the event store can load both events by ID and addressable events
131
+ eventStore.event({ id: "event_id" }).subscribe((event) => {
132
+ console.log("Loaded event:", event);
133
+ });
134
+
135
+ eventStore.replaceable({ kind: 0, pubkey: "pubkey" }).subscribe((profile) => {
136
+ console.log("Loaded profile:", profile);
137
+ });
138
+ ```
139
+
140
+ The unified loader accepts all options from both `EventPointerLoaderOptions` and `AddressLoaderOptions`, making it easy to configure both types of loading in one place.
141
+
97
142
  ## Timeline Loader
98
143
 
99
144
  The Timeline Loader is designed for fetching paginated Nostr events in chronological order. It maintains state between calls, allowing you to efficiently load timeline events in blocks until you reach a specific timestamp or exhaust available events.
@@ -181,6 +226,12 @@ All loaders accept these common configuration options:
181
226
  - `followRelayHints`: Whether to follow relay hints (default true)
182
227
  - `extraRelays`: An array of relays to always fetch from
183
228
 
229
+ ### Unified Event Loader Options
230
+
231
+ The unified event loader accepts all options from both Event Loader and Address Loader options, plus:
232
+
233
+ - `lookupRelays`: Fallback lookup relays to check when event can't be found (from Address Loader)
234
+
184
235
  ### Timeline Loader Options
185
236
 
186
237
  - `limit`: Maximum number of events to request per filter
@@ -1,6 +1,5 @@
1
- import { AddressPointerWithoutD } from "applesauce-core/helpers";
2
- import { Filter } from "nostr-tools";
3
- import { AddressPointer } from "nostr-tools/nip19";
1
+ import { Filter } from "applesauce-core/helpers/filter";
2
+ import { AddressPointer, AddressPointerWithoutD } from "applesauce-core/helpers/pointers";
4
3
  /** Converts an array of address pointers to a filter */
5
4
  export declare function createFilterFromAddressPointers(pointers: AddressPointerWithoutD[] | AddressPointer[]): Filter;
6
5
  /** Takes a set of address pointers, groups them, then returns filters for the groups */
@@ -1,11 +1,11 @@
1
- import { isAddressableKind, isReplaceableKind } from "nostr-tools/kinds";
1
+ import { isAddressableKind, isReplaceableKind } from "applesauce-core/helpers/event";
2
2
  import { unique } from "./array.js";
3
3
  /** Converts an array of address pointers to a filter */
4
4
  export function createFilterFromAddressPointers(pointers) {
5
5
  const filter = {};
6
6
  filter.kinds = unique(pointers.map((p) => p.kind));
7
7
  filter.authors = unique(pointers.map((p) => p.pubkey));
8
- const identifiers = unique(pointers.map((p) => p.identifier).filter((d) => !!d));
8
+ const identifiers = unique(pointers.map((p) => p.identifier).filter((d) => d !== undefined));
9
9
  if (identifiers.length > 0)
10
10
  filter["#d"] = identifiers;
11
11
  return filter;
@@ -29,7 +29,7 @@ export function createFiltersFromAddressPointers(pointers) {
29
29
  /** Checks if a relay will understand an address pointer */
30
30
  export function isLoadableAddressPointer(pointer) {
31
31
  if (isAddressableKind(pointer.kind))
32
- return !!pointer.identifier;
32
+ return pointer.identifier !== undefined;
33
33
  else
34
34
  return isReplaceableKind(pointer.kind);
35
35
  }
@@ -1,5 +1,6 @@
1
+ import { NostrEvent } from "applesauce-core/helpers/event";
2
+ import { Filter } from "applesauce-core/helpers/filter";
1
3
  import { Observable } from "rxjs";
2
- import { Filter, NostrEvent } from "nostr-tools";
3
4
  import { CacheRequest } from "../types.js";
4
5
  /** Calls the cache request and converts the reponse into an observable */
5
6
  export declare function unwrapCacheRequest(request: CacheRequest, filters: Filter[]): Observable<NostrEvent>;
@@ -1,18 +1,24 @@
1
+ import { markFromCache } from "applesauce-core/helpers/event";
1
2
  import { from, isObservable, of, switchMap, tap } from "rxjs";
2
- import { markFromCache } from "applesauce-core/helpers";
3
3
  /** Calls the cache request and converts the reponse into an observable */
4
4
  export function unwrapCacheRequest(request, filters) {
5
5
  const result = request(filters);
6
6
  if (isObservable(result))
7
7
  return result;
8
- else if (result instanceof Promise)
9
- return from(result).pipe(switchMap((v) => (Array.isArray(v) ? from(v) : of(v))));
10
- else if (Array.isArray(result))
8
+ else if (result instanceof Promise) {
9
+ return from(result).pipe(switchMap((v) => (Array.isArray(v) ? from(v) : of(v))), tap((e) => markFromCache(e)));
10
+ }
11
+ else if (Array.isArray(result)) {
12
+ for (const event of result)
13
+ markFromCache(event);
11
14
  return from(result);
12
- else
15
+ }
16
+ else {
17
+ markFromCache(result);
13
18
  return of(result);
19
+ }
14
20
  }
15
21
  /** Calls a cache request method with filters and marks all returned events as being from the cache */
16
22
  export function makeCacheRequest(request, filters) {
17
- return unwrapCacheRequest(request, filters).pipe(tap((e) => markFromCache(e)));
23
+ return unwrapCacheRequest(request, filters);
18
24
  }
@@ -1,6 +1,7 @@
1
+ import { NostrEvent } from "applesauce-core/helpers/event";
2
+ import { Filter } from "applesauce-core/helpers/filter";
1
3
  import { Observable } from "rxjs";
2
4
  import { NostrRequest, UpstreamPool } from "../types.js";
3
- import { Filter, NostrEvent } from "nostr-tools";
4
5
  /** Makes a nostr request on the upstream pool */
5
6
  export declare function makeUpstreamRequest(pool: UpstreamPool, relays: string[], filters: Filter[]): Observable<NostrEvent>;
6
7
  /** Wraps an upstream pool and returns a NostrRequest */
@@ -1,5 +1,5 @@
1
- import { mapEventsToStore } from "applesauce-core";
2
- import { NostrEvent } from "nostr-tools";
1
+ import { filterDuplicateEvents } from "applesauce-core";
2
+ import { NostrEvent } from "applesauce-core/helpers/event";
3
3
  import { Observable } from "rxjs";
4
4
  import { CacheRequest, NostrRequest, UpstreamPool } from "../types.js";
5
5
  export type LoadableAddressPointer = {
@@ -33,8 +33,8 @@ export type AddressLoaderOptions = Partial<{
33
33
  bufferTime: number;
34
34
  /** Max buffer size ( default 200 ) */
35
35
  bufferSize: number;
36
- /** An event store used to deduplicate events */
37
- eventStore?: Parameters<typeof mapEventsToStore>[0];
36
+ /** An event store used to deduplicate events. Set to null to disable deduplication */
37
+ eventStore?: Parameters<typeof filterDuplicateEvents>[0] | null;
38
38
  /** A method used to load events from a local cache */
39
39
  cacheRequest: CacheRequest;
40
40
  /** Whether to follow relay hints ( default true ) */
@@ -1,6 +1,6 @@
1
- import { mapEventsToStore } from "applesauce-core";
1
+ import { EventMemory, filterDuplicateEvents } from "applesauce-core";
2
2
  import { createReplaceableAddress, getReplaceableAddress, getReplaceableIdentifier, isReplaceable, mergeRelaySets, } from "applesauce-core/helpers";
3
- import { bufferTime, catchError, EMPTY } from "rxjs";
3
+ import { bufferTime, catchError, EMPTY, identity } from "rxjs";
4
4
  import { createFiltersFromAddressPointers, isLoadableAddressPointer } from "../helpers/address-pointer.js";
5
5
  import { makeCacheRequest } from "../helpers/cache.js";
6
6
  import { batchLoader, unwrap } from "../helpers/loaders.js";
@@ -115,7 +115,7 @@ export function createAddressLoader(pool, opts) {
115
115
  // Filter resutls based on requests
116
116
  (pointer, event) => event.kind === pointer.kind &&
117
117
  event.pubkey === pointer.pubkey &&
118
- (pointer.identifier ? getReplaceableIdentifier(event) === pointer.identifier : true),
119
- // Pass all events through the store if defined
120
- opts?.eventStore && mapEventsToStore(opts.eventStore));
118
+ (pointer.identifier !== undefined ? getReplaceableIdentifier(event) === pointer.identifier : true),
119
+ // Pass all events through the store if provided, or use EventMemory for deduplication by default
120
+ opts?.eventStore === null ? identity : filterDuplicateEvents(opts?.eventStore || new EventMemory()));
121
121
  }
@@ -1,6 +1,6 @@
1
- import { mapEventsToStore } from "applesauce-core";
2
- import { NostrEvent } from "nostr-tools";
3
- import { EventPointer } from "nostr-tools/nip19";
1
+ import { NostrEvent } from "applesauce-core/helpers/event";
2
+ import { EventPointer } from "applesauce-core/helpers/pointers";
3
+ import { filterDuplicateEvents } from "applesauce-core/observable";
4
4
  import { Observable } from "rxjs";
5
5
  import { CacheRequest, NostrRequest, UpstreamPool } from "../types.js";
6
6
  export type LoadableEventPointer = EventPointer & {
@@ -23,8 +23,8 @@ export type EventPointerLoaderOptions = Partial<{
23
23
  bufferTime: number;
24
24
  /** Max buffer size ( default 200 ) */
25
25
  bufferSize: number;
26
- /** An event store used to deduplicate events */
27
- eventStore?: Parameters<typeof mapEventsToStore>[0];
26
+ /** An event store used to deduplicate events. Set to null to disable deduplication */
27
+ eventStore?: Parameters<typeof filterDuplicateEvents>[0] | null;
28
28
  /** A method used to load events from a local cache */
29
29
  cacheRequest: CacheRequest;
30
30
  /** Whether to follow relay hints ( default true ) */
@@ -1,5 +1,6 @@
1
- import { mapEventsToStore } from "applesauce-core";
2
- import { bufferTime, catchError, EMPTY, merge, tap } from "rxjs";
1
+ import { EventMemory } from "applesauce-core/event-store";
2
+ import { filterDuplicateEvents } from "applesauce-core/observable";
3
+ import { bufferTime, catchError, EMPTY, identity, merge, tap } from "rxjs";
3
4
  import { makeCacheRequest } from "../helpers/cache.js";
4
5
  import { consolidateEventPointers } from "../helpers/event-pointer.js";
5
6
  import { batchLoader, unwrap } from "../helpers/loaders.js";
@@ -85,6 +86,6 @@ export function createEventLoader(pool, opts) {
85
86
  opts?.extraRelays ? relaysEventsLoader(request, opts.extraRelays) : undefined),
86
87
  // Filter resutls based on requests
87
88
  (pointer, event) => event.id === pointer.id,
88
- // Pass all events through the store if defined
89
- opts?.eventStore && mapEventsToStore(opts.eventStore));
89
+ // Pass all events through the store if provided, or use EventMemory for deduplication by default
90
+ opts?.eventStore === null ? identity : filterDuplicateEvents(opts?.eventStore || new EventMemory()));
90
91
  }
@@ -5,5 +5,6 @@ export * from "./reactions-loader.js";
5
5
  export * from "./social-graph.js";
6
6
  export * from "./tag-value-loader.js";
7
7
  export * from "./timeline-loader.js";
8
+ export * from "./unified-event-loader.js";
8
9
  export * from "./user-lists-loader.js";
9
10
  export * from "./zaps-loader.js";
@@ -5,5 +5,6 @@ export * from "./reactions-loader.js";
5
5
  export * from "./social-graph.js";
6
6
  export * from "./tag-value-loader.js";
7
7
  export * from "./timeline-loader.js";
8
+ export * from "./unified-event-loader.js";
8
9
  export * from "./user-lists-loader.js";
9
10
  export * from "./zaps-loader.js";
@@ -1,4 +1,4 @@
1
- import { NostrEvent } from "nostr-tools";
1
+ import { NostrEvent } from "applesauce-core/helpers/event";
2
2
  import { Observable } from "rxjs";
3
3
  import { UpstreamPool } from "../types.js";
4
4
  import { TagValueLoaderOptions } from "./tag-value-loader.js";
@@ -1,5 +1,6 @@
1
- import { getReplaceableAddress, getSeenRelays, isReplaceable, mergeRelaySets } from "applesauce-core/helpers";
2
- import { kinds } from "nostr-tools";
1
+ import { getReplaceableAddress, isReplaceable, kinds } from "applesauce-core/helpers/event";
2
+ import { getSeenRelays, mergeRelaySets } from "applesauce-core/helpers/relays";
3
+ import { EMPTY } from "rxjs";
3
4
  import { wrapUpstreamPool } from "../helpers/upstream.js";
4
5
  import { createTagValueLoader } from "./tag-value-loader.js";
5
6
  /** Creates a loader that loads reaction events for a given event */
@@ -11,8 +12,14 @@ export function createReactionsLoader(pool, opts) {
11
12
  return (event, relays) => {
12
13
  if (opts?.useSeenRelays ?? true)
13
14
  relays = mergeRelaySets(relays, getSeenRelays(event));
14
- return isReplaceable(event.kind)
15
- ? addressableLoader({ value: getReplaceableAddress(event), relays })
16
- : eventLoader({ value: event.id, relays });
15
+ if (isReplaceable(event.kind)) {
16
+ const address = getReplaceableAddress(event);
17
+ if (!address)
18
+ return EMPTY;
19
+ return addressableLoader({ value: address, relays });
20
+ }
21
+ else {
22
+ return eventLoader({ value: event.id, relays });
23
+ }
17
24
  };
18
25
  }
@@ -1,6 +1,6 @@
1
- import { mapEventsToStore } from "applesauce-core";
2
- import { NostrEvent } from "nostr-tools";
3
- import { ProfilePointer } from "nostr-tools/nip19";
1
+ import { NostrEvent } from "applesauce-core/helpers/event";
2
+ import { ProfilePointer } from "applesauce-core/helpers/pointers";
3
+ import { mapEventsToStore } from "applesauce-core/observable";
4
4
  import { Observable } from "rxjs";
5
5
  import { AddressPointerLoader } from "./address-loader.js";
6
6
  /** A loader that loads the social graph of a user out to a set distance */
@@ -1,6 +1,7 @@
1
- import { mapEventsToStore } from "applesauce-core";
2
- import { getProfilePointersFromList, mergeRelaySets } from "applesauce-core/helpers";
3
- import { kinds } from "nostr-tools";
1
+ import { getPublicContacts } from "applesauce-core/helpers";
2
+ import { kinds } from "applesauce-core/helpers/event";
3
+ import { mergeRelaySets } from "applesauce-core/helpers/relays";
4
+ import { mapEventsToStore } from "applesauce-core/observable";
4
5
  import { firstValueFrom, identity, isObservable, lastValueFrom, toArray } from "rxjs";
5
6
  import { wrapGeneratorFunction } from "../operators/generator.js";
6
7
  /** Create a social graph loader */
@@ -30,7 +31,7 @@ export function createSocialGraphLoader(addressLoader, opts) {
30
31
  toArray()));
31
32
  if (events.length === 0)
32
33
  return;
33
- const contacts = getProfilePointersFromList(events[events.length - 1]);
34
+ const contacts = getPublicContacts(events[events.length - 1]);
34
35
  // if the distance is greater than 0, add the contacts to the queue
35
36
  if (pointer.distance > 0) {
36
37
  for (const contact of contacts) {
@@ -1,5 +1,5 @@
1
- import { mapEventsToStore } from "applesauce-core";
2
- import { NostrEvent } from "nostr-tools";
1
+ import { NostrEvent } from "applesauce-core/helpers/event";
2
+ import { filterDuplicateEvents } from "applesauce-core/observable";
3
3
  import { Observable } from "rxjs";
4
4
  import { CacheRequest, NostrRequest, UpstreamPool } from "../types.js";
5
5
  export type TagValuePointer = {
@@ -25,8 +25,8 @@ export type TagValueLoaderOptions = {
25
25
  cacheRequest?: CacheRequest;
26
26
  /** An array of relays to always fetch from */
27
27
  extraRelays?: string[] | Observable<string[]>;
28
- /** An event store used to deduplicate events */
29
- eventStore?: Parameters<typeof mapEventsToStore>[0];
28
+ /** An event store used to deduplicate events. Set to null to disable deduplication */
29
+ eventStore?: Parameters<typeof filterDuplicateEvents>[0] | null;
30
30
  };
31
31
  export type TagValueLoader = (pointer: TagValuePointer) => Observable<NostrEvent>;
32
32
  /** Creates a loader that gets tag values from the cache */
@@ -1,6 +1,7 @@
1
- import { mapEventsToStore } from "applesauce-core";
2
- import { mergeRelaySets } from "applesauce-core/helpers";
3
- import { bufferTime, EMPTY, merge } from "rxjs";
1
+ import { EventMemory } from "applesauce-core/event-store";
2
+ import { mergeRelaySets } from "applesauce-core/helpers/relays";
3
+ import { filterDuplicateEvents } from "applesauce-core/observable";
4
+ import { bufferTime, EMPTY, identity, merge } from "rxjs";
4
5
  import { unique } from "../helpers/array.js";
5
6
  import { makeCacheRequest } from "../helpers/cache.js";
6
7
  import { batchLoader, unwrap } from "../helpers/loaders.js";
@@ -70,6 +71,6 @@ export function createTagValueLoader(pool, tagName, opts) {
70
71
  },
71
72
  // Filter results based on requests
72
73
  (pointer, event) => event.tags.some((tag) => tag[0] === tagName && tag[1] === pointer.value),
73
- // Pass all events through the store if defined
74
- opts?.eventStore && mapEventsToStore(opts?.eventStore));
74
+ // Pass all events through the store if provided, or use EventMemory for deduplication by default
75
+ opts?.eventStore === null ? identity : filterDuplicateEvents(opts?.eventStore || new EventMemory()));
75
76
  }
@@ -1,24 +1,75 @@
1
- import { mapEventsToStore } from "applesauce-core";
2
- import { NostrEvent } from "nostr-tools";
3
- import { Observable } from "rxjs";
4
- import { CacheRequest, FilterRequest, NostrRequest, TimelessFilter, UpstreamPool } from "../types.js";
1
+ import { NostrEvent } from "applesauce-core/helpers/event";
2
+ import { Filter } from "applesauce-core/helpers/filter";
3
+ import { ProfilePointer } from "applesauce-core/helpers/pointers";
4
+ import { FilterMap, OutboxMap } from "applesauce-core/helpers/relay-selection";
5
+ import { mapEventsToStore } from "applesauce-core/observable";
6
+ import { Observable, OperatorFunction } from "rxjs";
7
+ import { CacheRequest, TimelessFilter, UpstreamPool } from "../types.js";
5
8
  /** A loader that optionally takes a timestamp to load till and returns a stream of events */
6
- export type TimelineLoader = (since?: number) => Observable<NostrEvent>;
9
+ export type TimelineLoader = (since?: number | TimelineWindow) => Observable<NostrEvent>;
10
+ /**
11
+ * The current `since` value of the timeline loader
12
+ * `undefined` is used to initialize the loader to "now"
13
+ * -Infinity (for since) and Infinity (for until) should be used to force loading the next block of events
14
+ */
15
+ export type TimelineWindow = {
16
+ since?: number;
17
+ until?: number;
18
+ };
7
19
  /** Common options for timeline loaders */
8
20
  export type CommonTimelineLoaderOptions = Partial<{
9
21
  limit: number;
22
+ /** Logger to extend */
23
+ logger?: debug.Debugger;
10
24
  }>;
25
+ /**
26
+ * Watches for changes in the window and loads blocks of events going backward
27
+ * NOTE: this operator is stateful
28
+ */
29
+ export declare function loadBackwardBlocks(request: (until?: number) => Observable<NostrEvent>, opts?: CommonTimelineLoaderOptions): OperatorFunction<TimelineWindow, NostrEvent>;
30
+ /**
31
+ * Watches for changes in the window and loads blocks of events going forward
32
+ * NOTE: this operator is stateful
33
+ */
34
+ export declare function loadForwardBlocks(request: (since?: number) => Observable<NostrEvent>, opts?: CommonTimelineLoaderOptions): OperatorFunction<TimelineWindow, NostrEvent>;
11
35
  /** A loader that loads blocks of events until none are returned or the since timestamp is reached */
12
- export declare function filterBlockLoader(request: FilterRequest, filters: TimelessFilter[], opts?: CommonTimelineLoaderOptions): TimelineLoader;
13
- /** Creates a loader that loads a timeline from a cache */
14
- export declare function cacheTimelineLoader(request: CacheRequest, filters: TimelessFilter[], opts?: CommonTimelineLoaderOptions): TimelineLoader;
15
- /** Creates a timeline loader that loads the same filters from multiple relays */
16
- export declare function relaysTimelineLoader(request: NostrRequest, relays: string[], filters: TimelessFilter[], opts?: CommonTimelineLoaderOptions): TimelineLoader;
36
+ export declare function loadBlocksForTimelineWindow(request: (base: Filter) => Observable<NostrEvent>, opts?: CommonTimelineLoaderOptions): OperatorFunction<TimelineWindow, NostrEvent>;
37
+ /** Loads timeline blocs from cache using a cache request */
38
+ export declare function loadBlocksFromCache(request: CacheRequest, filters: TimelessFilter[] | TimelessFilter, opts?: CommonTimelineLoaderOptions): OperatorFunction<TimelineWindow, NostrEvent>;
39
+ /** Loads timeline blocs from relays using a pool or request method */
40
+ export declare function loadBlocksFromRelays(pool: UpstreamPool, relays: string[], filters: TimelessFilter[] | TimelessFilter, opts?: CommonTimelineLoaderOptions): OperatorFunction<TimelineWindow, NostrEvent>;
41
+ /** Loads timeline blocs from relay set using a pool or request method */
42
+ export declare function loadBlocksFromRelay(pool: UpstreamPool, relay: string, filters: TimelessFilter[] | TimelessFilter, opts?: CommonTimelineLoaderOptions): OperatorFunction<TimelineWindow, NostrEvent>;
43
+ /** Loads timeline blocks from a map of relays and filters */
44
+ export declare function loadBlocksFromFilterMap(pool: UpstreamPool, relayMap: FilterMap | Observable<FilterMap>, opts?: CommonTimelineLoaderOptions): OperatorFunction<TimelineWindow, NostrEvent>;
45
+ /** Loads timeline blocks from an {@link OutboxMap} and {@link Filter} or a function that projects users to a filter */
46
+ export declare function loadBlocksFromOutboxMap(pool: UpstreamPool, outboxes: OutboxMap | Observable<OutboxMap>, filter: TimelessFilter | ((users: ProfilePointer[]) => TimelessFilter | TimelessFilter[]), opts?: CommonTimelineLoaderOptions): OperatorFunction<TimelineWindow, NostrEvent>;
47
+ /** Loads timeline blocks from a {@link CacheRequest} using a {@link OutboxMap} and filters */
48
+ export declare function loadBlocksFromOutboxMapCache(cache: CacheRequest, outboxes: OutboxMap | Observable<OutboxMap>, filter: TimelessFilter | ((users: ProfilePointer[]) => TimelessFilter | TimelessFilter[]), opts?: CommonTimelineLoaderOptions): OperatorFunction<TimelineWindow, NostrEvent>;
49
+ /** Options for the create timeline loader methods */
17
50
  export type TimelineLoaderOptions = Partial<{
18
51
  /** A method used to load the timeline from the cache */
19
52
  cache: CacheRequest;
20
53
  /** An event store to pass all the events to */
21
54
  eventStore?: Parameters<typeof mapEventsToStore>[0];
22
55
  }> & CommonTimelineLoaderOptions;
23
- /** A common timeline loader that takes an array of relays and a cache method */
56
+ /** @deprecated Use the {@link loadBlocksFromCache} operator instead */
57
+ export declare function cacheTimelineLoader(request: CacheRequest, filters: TimelessFilter[] | TimelessFilter, opts?: CommonTimelineLoaderOptions): TimelineLoader;
58
+ /** @deprecated Use the {@link createTimelineLoader} operator instead */
59
+ export declare function relaysTimelineLoader(pool: UpstreamPool, relays: string[], filters: TimelessFilter[] | TimelessFilter, opts?: CommonTimelineLoaderOptions): TimelineLoader;
60
+ /**
61
+ * Creates a {@link TimelineLoader} that loads events from multiple relays and a cache
62
+ * @param pool - The upstream pool to use
63
+ * @param relays - The relays to load from
64
+ * @param filters - The filters to use
65
+ * @param opts - The options for the timeline loader
66
+ */
24
67
  export declare function createTimelineLoader(pool: UpstreamPool, relays: string[], filters: TimelessFilter[] | TimelessFilter, opts?: TimelineLoaderOptions): TimelineLoader;
68
+ /**
69
+ * Creates a {@link TimelineLoader} that loads events for a {@link OutboxMap} or an observable of {@link OutboxMap}
70
+ * @param pool - The upstream pool to use
71
+ * @param outboxMap - An {@link OutboxMap} or an observable of {@link OutboxMap}
72
+ * @param filter - A function to create filters for a set of users
73
+ * @param opts - The options for the timeline loader
74
+ */
75
+ export declare function createOutboxTimelineLoader(pool: UpstreamPool, outboxes: OutboxMap | Observable<OutboxMap>, filter: TimelessFilter | ((users: ProfilePointer[]) => TimelessFilter | TimelessFilter[]), opts?: TimelineLoaderOptions): TimelineLoader;
@@ -1,50 +1,282 @@
1
- import { mapEventsToStore } from "applesauce-core";
2
- import { EMPTY, finalize, identity, merge, tap } from "rxjs";
1
+ import { logger as baseLogger } from "applesauce-core";
2
+ import { EventMemory } from "applesauce-core/event-store";
3
+ import { isFilterEqual, mergeFilters } from "applesauce-core/helpers/filter";
4
+ import { createFilterMap } from "applesauce-core/helpers/relay-selection";
5
+ import { filterDuplicateEvents } from "applesauce-core/observable";
6
+ import { nanoid } from "nanoid";
7
+ import { BehaviorSubject, distinctUntilChanged, EMPTY, filter, finalize, identity, isObservable, map, merge, mergeMap, Observable, of, share, switchMap, tap, } from "rxjs";
3
8
  import { makeCacheRequest } from "../helpers/cache.js";
4
9
  import { wrapUpstreamPool } from "../helpers/upstream.js";
5
- /** A loader that loads blocks of events until none are returned or the since timestamp is reached */
6
- export function filterBlockLoader(request, filters, opts) {
7
- let cursor = Infinity;
8
- let complete = false;
9
- return (since) => {
10
- if (complete)
11
- return EMPTY;
12
- if (since !== undefined && cursor <= since)
13
- return EMPTY;
14
- // Keep loading blocks until none are returned or an event is found that is ealier then the new cursor
15
- const withTime = filters.map((filter) => ({
16
- ...filter,
17
- limit: filter.limit || opts?.limit,
18
- until: cursor !== Infinity ? cursor : undefined,
10
+ /**
11
+ * Watches for changes in the window and loads blocks of events going backward
12
+ * NOTE: this operator is stateful
13
+ */
14
+ export function loadBackwardBlocks(request, opts) {
15
+ return (source) => {
16
+ const log = opts?.logger?.extend("backward").extend(nanoid(8));
17
+ let cursor = undefined;
18
+ let loading = false;
19
+ let complete = false;
20
+ return source.pipe(filter(({ since, until }) => {
21
+ // Once complete, prevent further requests
22
+ if (complete)
23
+ return false;
24
+ // If max is unset, initialize to until
25
+ if (cursor === undefined && until !== undefined && Number.isFinite(until))
26
+ cursor = until;
27
+ // Skip loads if since is still in range
28
+ if (
29
+ // Ignore undefined since values
30
+ since === undefined ||
31
+ // If number is finite and in the loaded range, skip
32
+ (Number.isFinite(since) && cursor !== undefined && since <= cursor))
33
+ return false;
34
+ // Don't load blocks in parallel
35
+ if (loading)
36
+ return false;
37
+ return true;
38
+ }),
39
+ // NOTE: use mergeMap here to ensure old requests continue to load
40
+ mergeMap(() => {
41
+ // Set loading lock
42
+ loading = true;
43
+ // Count returned events so complete set
44
+ let count = 0;
45
+ log?.(`Loading block since:${cursor}`);
46
+ // Request the next block of events
47
+ return request(cursor).pipe(tap((event) => {
48
+ count++;
49
+ // Track the minimum created_at seen from the events
50
+ cursor = Math.min(event.created_at, cursor ?? Infinity);
51
+ }), finalize(() => {
52
+ loading = false;
53
+ complete = count === 0;
54
+ log?.(`Found ${count} events`);
55
+ if (complete)
56
+ log?.("Complete");
57
+ }));
19
58
  }));
20
- let count = 0;
21
- // Load the next block of events
22
- return request(withTime).pipe(tap((event) => {
23
- // Update the cursor to the oldest event
24
- cursor = Math.min(event.created_at - 1, cursor);
25
- count++;
26
- }), finalize(() => {
27
- // Set the loader to complete if no events are returned
28
- if (count === 0)
29
- complete = true;
59
+ };
60
+ }
61
+ /**
62
+ * Watches for changes in the window and loads blocks of events going forward
63
+ * NOTE: this operator is stateful
64
+ */
65
+ export function loadForwardBlocks(request, opts) {
66
+ return (source) => {
67
+ const log = opts?.logger?.extend("forward").extend(nanoid(8));
68
+ let cursor = undefined;
69
+ let loading = false;
70
+ let complete = false;
71
+ return source.pipe(filter(({ since, until }) => {
72
+ // Once complete, prevent further requests
73
+ if (complete)
74
+ return false;
75
+ // If min is unset, initialize to since
76
+ if (cursor === undefined && since !== undefined && Number.isFinite(since))
77
+ cursor = since;
78
+ // Skip loads if until is still in range
79
+ if (
80
+ // Ignore undefined until values
81
+ until === undefined ||
82
+ // If number is finite and in the loaded range, skip
83
+ (Number.isFinite(until) && cursor !== undefined && until <= cursor))
84
+ return false;
85
+ // Don't load blocks in parallel
86
+ if (loading)
87
+ return false;
88
+ return true;
89
+ }),
90
+ // NOTE: use mergeMap here to ensure old requests continue to load
91
+ mergeMap(() => {
92
+ // Set loading lock
93
+ loading = true;
94
+ // Count returned events so complete set
95
+ let count = 0;
96
+ log?.(`Loading block until:${cursor}`);
97
+ // Request the next block of events
98
+ return request(cursor).pipe(tap((event) => {
99
+ count++;
100
+ // Track the maximum created_at seen from the events
101
+ cursor = Math.max(event.created_at, cursor ?? -Infinity);
102
+ }), finalize(() => {
103
+ loading = false;
104
+ complete = count === 0;
105
+ log?.(`Found ${count} events`);
106
+ if (complete)
107
+ log?.("Complete");
108
+ }));
30
109
  }));
31
110
  };
32
111
  }
33
- /** Creates a loader that loads a timeline from a cache */
34
- export function cacheTimelineLoader(request, filters, opts) {
35
- return filterBlockLoader((filters) => makeCacheRequest(request, filters), filters, opts);
112
+ /** A loader that loads blocks of events until none are returned or the since timestamp is reached */
113
+ export function loadBlocksForTimelineWindow(request, opts) {
114
+ return (source) => merge(source.pipe(loadBackwardBlocks((until) => request({ until }), opts)), source.pipe(loadForwardBlocks((since) => request({ since }), opts)));
36
115
  }
37
- /** Creates a timeline loader that loads the same filters from multiple relays */
38
- export function relaysTimelineLoader(request, relays, filters, opts) {
39
- const loaders = relays.map((relay) => filterBlockLoader((f) => request([relay], f), filters, opts));
40
- return (since) => merge(...loaders.map((l) => l(since)));
116
+ /** Loads timeline blocs from cache using a cache request */
117
+ export function loadBlocksFromCache(request, filters, opts) {
118
+ if (!Array.isArray(filters))
119
+ filters = [filters];
120
+ const logger = opts?.logger?.extend("cache");
121
+ const loader = (base) => makeCacheRequest(request,
122
+ // Create filters from filters, base, and optional limit
123
+ filters.map((f) => (opts?.limit ? mergeFilters(f, base, { limit: opts.limit }) : mergeFilters(f, base))));
124
+ return (source) => source.pipe(loadBlocksForTimelineWindow(loader, { ...opts, logger }));
41
125
  }
42
- /** A common timeline loader that takes an array of relays and a cache method */
43
- export function createTimelineLoader(pool, relays, filters, opts) {
126
+ /** Loads timeline blocs from relays using a pool or request method */
127
+ export function loadBlocksFromRelays(pool, relays, filters, opts) {
128
+ if (!Array.isArray(filters))
129
+ filters = [filters];
130
+ const logger = opts?.logger?.extend("relays");
131
+ const request = wrapUpstreamPool(pool);
132
+ const loader = (base) => request(relays,
133
+ // Create filters from filters, base, and optional limit
134
+ filters.map((f) => (opts?.limit ? mergeFilters(f, base, { limit: opts.limit }) : mergeFilters(f, base))));
135
+ return (source) => source.pipe(loadBlocksForTimelineWindow(loader, { ...opts, logger }));
136
+ }
137
+ /** Loads timeline blocs from relay set using a pool or request method */
138
+ export function loadBlocksFromRelay(pool, relay, filters, opts) {
44
139
  if (!Array.isArray(filters))
45
140
  filters = [filters];
141
+ const logger = opts?.logger?.extend(relay);
46
142
  const request = wrapUpstreamPool(pool);
47
- const cacheLoader = opts?.cache && cacheTimelineLoader(opts.cache, filters, opts);
48
- const relayLoader = relaysTimelineLoader(request, relays, filters, opts);
49
- return (since) => merge(cacheLoader?.(since) ?? EMPTY, relayLoader?.(since)).pipe(opts?.eventStore ? mapEventsToStore(opts.eventStore) : identity);
143
+ const loader = (base) => request([relay],
144
+ // Create filters from filters, base, and optional limit
145
+ filters.map((f) => (opts?.limit ? mergeFilters(f, base, { limit: opts.limit }) : mergeFilters(f, base))));
146
+ return (source) => source.pipe(loadBlocksForTimelineWindow(loader, { ...opts, logger }));
147
+ }
148
+ /** Loads timeline blocks from a map of relays and filters */
149
+ export function loadBlocksFromFilterMap(pool, relayMap, opts) {
150
+ return (source) => {
151
+ const map$ = isObservable(relayMap) ? relayMap : of(relayMap);
152
+ const cache = new Map();
153
+ const loaders$ = map$.pipe(map((relayMap) => Object.entries(relayMap).map(([relay, filters]) => {
154
+ const existing = cache.get(relay);
155
+ if (existing && isFilterEqual(existing.filters, filters))
156
+ return existing.loader;
157
+ // Create a new loader for this relay + filter set
158
+ const loader = source.pipe(loadBlocksFromRelay(pool, relay, filters, opts));
159
+ cache.set(relay, { filters, loader });
160
+ return loader;
161
+ })));
162
+ return loaders$.pipe(switchMap((loaders) => merge(...loaders)));
163
+ };
164
+ }
165
+ /** Loads timeline blocks from an {@link OutboxMap} and {@link Filter} or a function that projects users to a filter */
166
+ export function loadBlocksFromOutboxMap(pool, outboxes, filter, opts) {
167
+ const outboxes$ = isObservable(outboxes) ? outboxes : of(outboxes);
168
+ // Project outbox map to filter map
169
+ const filterMap$ = outboxes$.pipe(map((outboxes) => {
170
+ // Filter is a dynamic method that returns a filter
171
+ if (typeof filter === "function")
172
+ return Object.fromEntries(Object.entries(outboxes).map(([relay, users]) => [relay, filter(users)]));
173
+ // Filter is just a filter
174
+ return createFilterMap(outboxes, filter);
175
+ }));
176
+ // Load blocks from the filter map
177
+ return loadBlocksFromFilterMap(pool, filterMap$, opts);
178
+ }
179
+ /** Loads timeline blocks from a {@link CacheRequest} using a {@link OutboxMap} and filters */
180
+ export function loadBlocksFromOutboxMapCache(cache, outboxes, filter, opts) {
181
+ const outboxes$ = isObservable(outboxes) ? outboxes : of(outboxes);
182
+ // Project outboxes to filters
183
+ const filters$ = outboxes$.pipe(map((outboxes) => {
184
+ // Get all pubkeys from all relays
185
+ const pubkeys = new Set();
186
+ for (const users of Object.values(outboxes)) {
187
+ for (const user of users) {
188
+ pubkeys.add(user.pubkey);
189
+ }
190
+ }
191
+ // Create filters for all the pubkeys
192
+ if (typeof filter === "function") {
193
+ const users = Array.from(pubkeys).map((pubkey) => ({ pubkey }));
194
+ return filter(users);
195
+ }
196
+ else {
197
+ return mergeFilters(filter, { authors: Array.from(pubkeys) });
198
+ }
199
+ }),
200
+ // Only create a new loader if the filters change
201
+ distinctUntilChanged((a, b) => isFilterEqual(a, b)));
202
+ // Load blocks from the filter map
203
+ return (source) => filters$.pipe(
204
+ // Every time the filters change, create a new cache loader instance
205
+ switchMap((filters) => source.pipe(loadBlocksFromCache(cache, filters, opts))));
206
+ }
207
+ /** Internal logic for function based timeline loaders */
208
+ function wrapTimelineLoader(window$, loader$, eventStore) {
209
+ const singleton$ = loader$.pipe(
210
+ // Pass all events through the store if provided
211
+ eventStore === null ? identity : filterDuplicateEvents(eventStore || new EventMemory()),
212
+ // Ensure a single subscription to the requests
213
+ share());
214
+ return (since) => {
215
+ // Return the loader so it can be subscribed to
216
+ return new Observable((observer) => {
217
+ const sub = singleton$.subscribe(observer);
218
+ // Once subscribed, update the window
219
+ if (typeof since === "object" && since !== undefined) {
220
+ // Pass new window to the loader
221
+ window$.next(since);
222
+ }
223
+ else {
224
+ // Only update window when this request is subscribed to (prevents window getting lost before subscription)
225
+ window$.next({
226
+ // Default to -Infinity to force loading the next block of events (legacy behavior)
227
+ since: since ?? -Infinity,
228
+ });
229
+ }
230
+ return () => sub.unsubscribe();
231
+ });
232
+ };
233
+ }
234
+ /** @deprecated Use the {@link loadBlocksFromCache} operator instead */
235
+ export function cacheTimelineLoader(request, filters, opts) {
236
+ const window$ = new BehaviorSubject({});
237
+ return wrapTimelineLoader(window$, window$.pipe(loadBlocksFromCache(request, filters, opts)));
238
+ }
239
+ /** @deprecated Use the {@link createTimelineLoader} operator instead */
240
+ export function relaysTimelineLoader(pool, relays, filters, opts) {
241
+ const window$ = new BehaviorSubject({});
242
+ return wrapTimelineLoader(window$, window$.pipe(loadBlocksFromRelays(pool, relays, filters, opts)));
243
+ }
244
+ /**
245
+ * Creates a {@link TimelineLoader} that loads events from multiple relays and a cache
246
+ * @param pool - The upstream pool to use
247
+ * @param relays - The relays to load from
248
+ * @param filters - The filters to use
249
+ * @param opts - The options for the timeline loader
250
+ */
251
+ export function createTimelineLoader(pool, relays, filters, opts) {
252
+ const logger = (opts?.logger ?? baseLogger).extend("timeline").extend(nanoid(4));
253
+ const window$ = new BehaviorSubject({});
254
+ // Create cache and relays loaders
255
+ const cache$ = opts?.cache ? window$.pipe(loadBlocksFromCache(opts.cache, filters, { ...opts, logger })) : EMPTY;
256
+ // Load blocks from relays
257
+ const relays$ = window$.pipe(loadBlocksFromRelays(pool, relays, filters, { ...opts, logger }));
258
+ // Merge the cache and relays loaders
259
+ const loader$ = merge(cache$, relays$);
260
+ return wrapTimelineLoader(window$, loader$, opts?.eventStore);
261
+ }
262
+ /**
263
+ * Creates a {@link TimelineLoader} that loads events for a {@link OutboxMap} or an observable of {@link OutboxMap}
264
+ * @param pool - The upstream pool to use
265
+ * @param outboxMap - An {@link OutboxMap} or an observable of {@link OutboxMap}
266
+ * @param filter - A function to create filters for a set of users
267
+ * @param opts - The options for the timeline loader
268
+ */
269
+ export function createOutboxTimelineLoader(pool, outboxes, filter, opts) {
270
+ const logger = (opts?.logger ?? baseLogger).extend("outbox-timeline").extend(nanoid(4));
271
+ const window$ = new BehaviorSubject({});
272
+ // An observable of a cache loader instance for all users
273
+ const cache$ = opts?.cache
274
+ ? window$.pipe(loadBlocksFromOutboxMapCache(opts?.cache, outboxes, filter, { ...opts, logger }))
275
+ : EMPTY;
276
+ // Load blocks from relays using outboxes
277
+ const relays$ = window$.pipe(loadBlocksFromOutboxMap(pool, outboxes, filter, { ...opts, logger }));
278
+ // Merge the cache and relays loaders
279
+ const loader$ = merge(cache$, relays$);
280
+ // Wrap the loader in a timeline loader function
281
+ return wrapTimelineLoader(window$, loader$, opts?.eventStore);
50
282
  }
@@ -0,0 +1,19 @@
1
+ import { filterDuplicateEvents, IMissingEventLoader } from "applesauce-core";
2
+ import { NostrEvent } from "applesauce-core/helpers/event";
3
+ import { AddressPointer, AddressPointerWithoutD, EventPointer } from "applesauce-core/helpers/pointers";
4
+ import { Observable } from "rxjs";
5
+ import { UpstreamPool } from "../types.js";
6
+ import { AddressLoaderOptions } from "./address-loader.js";
7
+ import { EventPointerLoaderOptions } from "./event-loader.js";
8
+ export type UnifiedEventLoaderOptions = Partial<EventPointerLoaderOptions & AddressLoaderOptions>;
9
+ export type UnifiedEventLoader = (pointer: EventPointer | AddressPointer | AddressPointerWithoutD) => Observable<NostrEvent>;
10
+ /**
11
+ * Create a unified event loader that can handle both EventPointer and AddressPointer types.
12
+ * Internally routes to createEventLoader for EventPointer and createAddressLoader for AddressPointer.
13
+ */
14
+ export declare function createUnifiedEventLoader(pool: UpstreamPool, opts?: UnifiedEventLoaderOptions): UnifiedEventLoader;
15
+ /**
16
+ * Creates a {@link UnifiedEventLoader} that will be used to load events that are not found in the store
17
+ * @returns The created loader
18
+ */
19
+ export declare function createEventLoaderForStore(store: IMissingEventLoader & Parameters<typeof filterDuplicateEvents>[0], pool: UpstreamPool, opts?: Omit<UnifiedEventLoaderOptions, "eventStore">): UnifiedEventLoader;
@@ -0,0 +1,46 @@
1
+ import { isEventPointer } from "applesauce-core/helpers/pointers";
2
+ import { createAddressLoader } from "./address-loader.js";
3
+ import { createEventLoader } from "./event-loader.js";
4
+ /**
5
+ * Create a unified event loader that can handle both EventPointer and AddressPointer types.
6
+ * Internally routes to createEventLoader for EventPointer and createAddressLoader for AddressPointer.
7
+ */
8
+ export function createUnifiedEventLoader(pool, opts) {
9
+ // Create both loaders with the appropriate options
10
+ const eventLoader = createEventLoader(pool, {
11
+ bufferTime: opts?.bufferTime,
12
+ bufferSize: opts?.bufferSize,
13
+ eventStore: opts?.eventStore,
14
+ cacheRequest: opts?.cacheRequest,
15
+ followRelayHints: opts?.followRelayHints,
16
+ extraRelays: opts?.extraRelays,
17
+ });
18
+ const addressLoader = createAddressLoader(pool, {
19
+ bufferTime: opts?.bufferTime,
20
+ bufferSize: opts?.bufferSize,
21
+ eventStore: opts?.eventStore,
22
+ cacheRequest: opts?.cacheRequest,
23
+ followRelayHints: opts?.followRelayHints,
24
+ extraRelays: opts?.extraRelays,
25
+ lookupRelays: opts?.lookupRelays,
26
+ });
27
+ // Return a unified loader that routes based on pointer type
28
+ return (pointer) => {
29
+ // Check if it's an EventPointer (has 'id' property)
30
+ if (isEventPointer(pointer)) {
31
+ return eventLoader(pointer);
32
+ }
33
+ else {
34
+ return addressLoader(pointer);
35
+ }
36
+ };
37
+ }
38
+ /**
39
+ * Creates a {@link UnifiedEventLoader} that will be used to load events that are not found in the store
40
+ * @returns The created loader
41
+ */
42
+ export function createEventLoaderForStore(store, pool, opts) {
43
+ const loader = createUnifiedEventLoader(pool, { ...opts, eventStore: store });
44
+ store.eventLoader = loader;
45
+ return loader;
46
+ }
@@ -1,6 +1,6 @@
1
1
  import { mapEventsToStore } from "applesauce-core";
2
- import { NostrEvent } from "nostr-tools";
3
- import { ProfilePointer } from "nostr-tools/nip19";
2
+ import { NostrEvent } from "applesauce-core/helpers/event";
3
+ import { ProfilePointer } from "applesauce-core/helpers/pointers";
4
4
  import { Observable } from "rxjs";
5
5
  import { CacheRequest, UpstreamPool } from "../types.js";
6
6
  /** A list of NIP-51 list kinds that most clients will use */
@@ -1,6 +1,6 @@
1
1
  import { mapEventsToStore } from "applesauce-core";
2
- import { mergeRelaySets } from "applesauce-core/helpers";
3
- import { kinds } from "nostr-tools";
2
+ import { kinds } from "applesauce-core/helpers/event";
3
+ import { mergeRelaySets } from "applesauce-core/helpers/relays";
4
4
  import { EMPTY, identity, merge } from "rxjs";
5
5
  import { makeCacheRequest } from "../helpers/cache.js";
6
6
  import { unwrap } from "../helpers/loaders.js";
@@ -1,4 +1,4 @@
1
- import { NostrEvent } from "nostr-tools";
1
+ import { NostrEvent } from "applesauce-core/helpers/event";
2
2
  import { Observable } from "rxjs";
3
3
  import { UpstreamPool } from "../types.js";
4
4
  import { TagValueLoaderOptions } from "./tag-value-loader.js";
@@ -1,5 +1,6 @@
1
- import { getReplaceableAddress, getSeenRelays, isReplaceable, mergeRelaySets } from "applesauce-core/helpers";
2
- import { kinds } from "nostr-tools";
1
+ import { getReplaceableAddress, isReplaceable, kinds } from "applesauce-core/helpers/event";
2
+ import { getSeenRelays, mergeRelaySets } from "applesauce-core/helpers/relays";
3
+ import { EMPTY } from "rxjs";
3
4
  import { wrapUpstreamPool } from "../helpers/upstream.js";
4
5
  import { createTagValueLoader } from "./tag-value-loader.js";
5
6
  /** Creates a loader that loads zap events for a given event */
@@ -11,8 +12,14 @@ export function createZapsLoader(pool, opts) {
11
12
  return (event, relays) => {
12
13
  if (opts?.useSeenRelays ?? true)
13
14
  relays = mergeRelaySets(relays, getSeenRelays(event));
14
- return isReplaceable(event.kind)
15
- ? addressableLoader({ value: getReplaceableAddress(event), relays })
16
- : eventLoader({ value: event.id, relays });
15
+ if (isReplaceable(event.kind)) {
16
+ const address = getReplaceableAddress(event);
17
+ if (!address)
18
+ return EMPTY;
19
+ return addressableLoader({ value: address, relays });
20
+ }
21
+ else {
22
+ return eventLoader({ value: event.id, relays });
23
+ }
17
24
  };
18
25
  }
package/dist/types.d.ts CHANGED
@@ -1,14 +1,13 @@
1
- import { Filter, NostrEvent } from "nostr-tools";
1
+ import { NostrEvent } from "applesauce-core/helpers/event";
2
+ import { Filter } from "applesauce-core/helpers/filter";
2
3
  import { Observable } from "rxjs";
3
4
  /** A flexible method for requesting events from a cache */
4
5
  export type CacheRequest = (filters: Filter[]) => Observable<NostrEvent> | Promise<NostrEvent | NostrEvent[]> | NostrEvent | NostrEvent[];
6
+ /** A method for requesting events from multiple relays */
7
+ export type NostrRequest = (relays: string[], filters: Filter[]) => Observable<NostrEvent>;
5
8
  /** A flexible type for the upstream relay pool */
6
9
  export type UpstreamPool = NostrRequest | {
7
10
  request: NostrRequest;
8
11
  };
9
- /** A method for requesting events from a relay or cache` */
10
- export type FilterRequest = (filters: Filter[]) => Observable<NostrEvent>;
11
- /** A method for requesting events from multiple relays */
12
- export type NostrRequest = (relays: string[], filters: Filter[]) => Observable<NostrEvent>;
13
12
  /** A filter that is does not have a since or until */
14
13
  export type TimelessFilter = Omit<Filter, "since" | "until">;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-loaders",
3
- "version": "4.0.0",
3
+ "version": "5.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",
@@ -52,17 +52,16 @@
52
52
  }
53
53
  },
54
54
  "dependencies": {
55
- "applesauce-core": "^4.0.0",
55
+ "applesauce-core": "^5.0.0",
56
56
  "nanoid": "^5.0.9",
57
- "nostr-tools": "~2.17",
58
57
  "rxjs": "^7.8.1"
59
58
  },
60
59
  "devDependencies": {
61
60
  "@hirez_io/observer-spy": "^2.2.0",
62
- "applesauce-signers": "^4.0.0",
61
+ "applesauce-signers": "^5.0.0",
63
62
  "rimraf": "^6.0.1",
64
63
  "typescript": "^5.8.3",
65
- "vitest": "^3.2.4",
64
+ "vitest": "^4.0.15",
66
65
  "vitest-websocket-mock": "^0.5.0"
67
66
  },
68
67
  "funding": {