nostr-idb 0.1.2 → 0.2.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/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ # nostr-idb
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f223129: Added `CacheRelay` class
8
+ - 6c04558: Add `IndexCache` for relay
package/README.md CHANGED
@@ -2,9 +2,47 @@
2
2
 
3
3
  A collection of helper methods for storing nostr events in IndexedDB
4
4
 
5
- # Methods
5
+ ## Features
6
6
 
7
- ## openDB
7
+ - Built directly on top of IndexedDB for the lowest latency
8
+ - CacheRelay class that has a similar API to to nostr-tool`s Relay class
9
+ - Caches indexes in memory
10
+
11
+ ## Using the CacheRelay class
12
+
13
+ The `CacheRelay` class is lightweight in-memory relay that syncs with the IndexedDB database.
14
+
15
+ There are a few benefits to using it instead of the underlying `getEventsForFilters` or `addEvents` methods
16
+
17
+ - Caches indexes in memory.
18
+ - Batches write transactions
19
+ - Almost interchangeable with nostr-tool's `Relay` class
20
+
21
+ ```javascript
22
+ import { openDB, CacheRelay } from "nostr-idb";
23
+ const db = await openDB("events");
24
+
25
+ const cacheRelay = new CacheRelay(db);
26
+
27
+ for (let event of events) {
28
+ cacheRelay.publish(event);
29
+ }
30
+
31
+ const sub = cacheRelay.subscribe([{ kinds: [1] }], {
32
+ onevent: (event) => {
33
+ console.log("got event", event);
34
+ },
35
+ oneose: () => {
36
+ console.log("no more events in cache");
37
+ },
38
+ });
39
+ ```
40
+
41
+ ## Methods
42
+
43
+ NOTE: all methods are async unless specified otherwise
44
+
45
+ ### openDB
8
46
 
9
47
  Opens a database with `name` and optional `callbacks`. see [openDB](https://www.npmjs.com/package/idb#opendb)
10
48
 
@@ -14,13 +52,13 @@ const db = await openDB("events");
14
52
  await addEvents(db, [...])
15
53
  ```
16
54
 
17
- ## deleteDB
55
+ ### deleteDB
18
56
 
19
57
  Calls `deleteDB` from idb under the hood
20
58
 
21
- ## getEventsForFilters
59
+ ### getEventsForFilter / getEventsForFilters
22
60
 
23
- Returns a sorted array of events that match the filters
61
+ Both methods returns a sorted array of events that match the filters
24
62
 
25
63
  ```javascript
26
64
  import { openDB, addEvents, getEventsForFilters } from "nostr-idb";
@@ -37,3 +75,47 @@ const events = await getEventsForFilters(db, [
37
75
  }
38
76
  ])
39
77
  ```
78
+
79
+ ### addEvent / addEvents
80
+
81
+ `addEvent` and `addEvents` methods can be used to add events to the database.
82
+
83
+ If possible its better to use `addEvents` and batch writes. since writing single events to the database can cause performance issues
84
+
85
+ ### getIdsForFilter / getIdsForFilters
86
+
87
+ Similar to `getEventsForFilters` but instead of loading the whole event it just returns the ids
88
+
89
+ ### countEventsForFilter / countEventsForFilters
90
+
91
+ Similar to `getEventsForFilters` but returns just the number of events
92
+
93
+ ## Index Cache
94
+
95
+ Normally this is created when you create a new `CacheRelay`. but if you want to maintain your own you can use the `IndexCache` class
96
+
97
+ ```javascript
98
+ import { openDB, addEvents, getEventsForFilters, IndexCache } from "nostr-idb";
99
+
100
+ const indexCache = new IndexCache()
101
+ const db = await openDB("events");
102
+
103
+ // add events to db
104
+ await addEvents(db, [...])
105
+
106
+ // if indexCache is passed in getEventsForFilters will check it first and save any indexes to it
107
+ const events = await getEventsForFilters(db, [
108
+ {
109
+ kinds: [1, 6],
110
+ limit: 30
111
+ }
112
+ ], indexCache)
113
+
114
+ // add more events
115
+ await addEvents(db, [...])
116
+ // NOTE: don't forget to add events to in-memory indexes
117
+ // otherwise your indexes will get out of sync
118
+ for(let event of events){
119
+ indexCache.addEventToIndexes(event)
120
+ }
121
+ ```
@@ -1,6 +1,7 @@
1
1
  import { DeleteDBCallbacks, OpenDBCallbacks } from "idb";
2
- import { Schema } from "./schema.js";
2
+ import { NostrIDB, Schema } from "./schema.js";
3
3
  export declare const NOSTR_IDB_NAME = "nostr-idb";
4
4
  export declare const NOSTR_IDB_VERSION = 1;
5
5
  export declare function openDB(name?: string, callbacks?: OpenDBCallbacks<Schema>): Promise<import("idb").IDBPDatabase<Schema>>;
6
6
  export declare function deleteDB(name?: string, callbacks?: DeleteDBCallbacks): Promise<void>;
7
+ export declare function clearDB(db: NostrIDB): Promise<void>;
package/dist/database.js CHANGED
@@ -9,8 +9,9 @@ export async function openDB(name = NOSTR_IDB_NAME, callbacks) {
9
9
  events.createIndex("id", "event.id", { unique: true });
10
10
  events.createIndex("pubkey", "event.pubkey");
11
11
  events.createIndex("kind", "event.kind");
12
- events.createIndex("create_at", "event.created_at");
12
+ events.createIndex("created_at", "event.created_at");
13
13
  events.createIndex("tags", "tags", { multiEntry: true });
14
+ events.createIndex("addressPointer", ["kind", "pubkey", "identifier"]);
14
15
  const seen = db.createObjectStore("seen", { keyPath: "id" });
15
16
  seen.createIndex("date", "date");
16
17
  seen.createIndex("relay", "relays", { multiEntry: true });
@@ -24,3 +25,8 @@ export async function openDB(name = NOSTR_IDB_NAME, callbacks) {
24
25
  export async function deleteDB(name = NOSTR_IDB_NAME, callbacks) {
25
26
  return await idbDeleteDB(name, callbacks);
26
27
  }
28
+ export async function clearDB(db) {
29
+ await db.clear("events");
30
+ await db.clear("used");
31
+ await db.clear("seen");
32
+ }
@@ -0,0 +1,18 @@
1
+ import { Event } from "nostr-tools";
2
+ export declare class IndexCache {
3
+ kinds: Map<number, Set<string>>;
4
+ pubkeys: Map<string, Set<string>>;
5
+ tags: Map<string, Set<string>>;
6
+ get count(): number;
7
+ max: number;
8
+ lastUsed: Set<string>[];
9
+ private useIndex;
10
+ getKindIndex(kind: number): Set<string> | undefined;
11
+ setKindIndex(kind: number, index: Set<string>): void;
12
+ getPubkeyIndex(pubkey: string): Set<string> | undefined;
13
+ setPubkeyIndex(pubkey: string, index: Set<string>): void;
14
+ getTagIndex(tagAndValue: string): Set<string> | undefined;
15
+ setTagIndex(tagAndValue: string, index: Set<string>): void;
16
+ addEventToIndexes(event: Event): void;
17
+ pruneIndexes(): void;
18
+ }
@@ -0,0 +1,63 @@
1
+ import { getEventTags } from "./ingest";
2
+ export class IndexCache {
3
+ kinds = new Map();
4
+ pubkeys = new Map();
5
+ tags = new Map();
6
+ get count() {
7
+ return this.kinds.size + this.pubkeys.size + this.tags.size;
8
+ }
9
+ max = 1000;
10
+ lastUsed = [];
11
+ useIndex(index) {
12
+ const i = this.lastUsed.indexOf(index);
13
+ if (i !== -1)
14
+ this.lastUsed.splice(i, i + 1);
15
+ this.lastUsed.push(index);
16
+ }
17
+ getKindIndex(kind) {
18
+ const index = this.kinds.get(kind);
19
+ if (index)
20
+ this.useIndex(index);
21
+ return index;
22
+ }
23
+ setKindIndex(kind, index) {
24
+ this.kinds.set(kind, index);
25
+ this.useIndex(index);
26
+ this.pruneIndexes();
27
+ }
28
+ getPubkeyIndex(pubkey) {
29
+ const index = this.pubkeys.get(pubkey);
30
+ if (index)
31
+ this.useIndex(index);
32
+ return index;
33
+ }
34
+ setPubkeyIndex(pubkey, index) {
35
+ this.pubkeys.set(pubkey, index);
36
+ this.useIndex(index);
37
+ this.pruneIndexes();
38
+ }
39
+ getTagIndex(tagAndValue) {
40
+ const index = this.tags.get(tagAndValue);
41
+ if (index)
42
+ this.useIndex(index);
43
+ return index;
44
+ }
45
+ setTagIndex(tagAndValue, index) {
46
+ this.tags.set(tagAndValue, index);
47
+ this.useIndex(index);
48
+ this.pruneIndexes();
49
+ }
50
+ addEventToIndexes(event) {
51
+ this.getKindIndex(event.kind)?.add(event.id);
52
+ this.getPubkeyIndex(event.pubkey)?.add(event.id);
53
+ const tags = getEventTags(event);
54
+ for (const tag of tags) {
55
+ this.getTagIndex(tag)?.add(event.id);
56
+ }
57
+ }
58
+ pruneIndexes() {
59
+ while (this.lastUsed.length > 0 && this.lastUsed.length > this.max) {
60
+ this.lastUsed.shift();
61
+ }
62
+ }
63
+ }
package/dist/index.d.ts CHANGED
@@ -2,3 +2,6 @@ export * from "./database.js";
2
2
  export * from "./query-filter.js";
3
3
  export * from "./query-misc.js";
4
4
  export * from "./ingest.js";
5
+ export * from "./relay.js";
6
+ export * from "./index-cache.js";
7
+ export * from "./write-queue.js";
package/dist/index.js CHANGED
@@ -2,3 +2,6 @@ export * from "./database.js";
2
2
  export * from "./query-filter.js";
3
3
  export * from "./query-misc.js";
4
4
  export * from "./ingest.js";
5
+ export * from "./relay.js";
6
+ export * from "./index-cache.js";
7
+ export * from "./write-queue.js";
package/dist/ingest.d.ts CHANGED
@@ -1,8 +1,16 @@
1
1
  import { IDBPTransaction } from "idb";
2
2
  import { Event } from "nostr-tools";
3
3
  import type { NostrIDB, Schema } from "./schema.js";
4
- export declare function createWriteTransaction(db: NostrIDB): IDBPTransaction<Schema, ["events"], "readwrite">;
4
+ /**
5
+ * Returns an events tags as an array of string for indexing
6
+ */
5
7
  export declare function getEventTags(event: Event): string[];
8
+ export declare function isReplaceable(kind: number): boolean;
9
+ export type ReplaceableEventAddress = {
10
+ kind: number;
11
+ pubkey: string;
12
+ identifier?: string;
13
+ };
6
14
  export declare function addEvent(db: NostrIDB, event: Event, transaction?: IDBPTransaction<Schema, ["events"], "readwrite">): Promise<void>;
7
15
  export declare function addEvents(db: NostrIDB, events: Event[]): Promise<void>;
8
16
  export declare function updateUsed(db: NostrIDB, ids: string[]): Promise<void>;
package/dist/ingest.js CHANGED
@@ -1,20 +1,37 @@
1
1
  import { validateEvent } from "nostr-tools";
2
2
  import { GENERIC_TAGS } from "./common.js";
3
- export function createWriteTransaction(db) {
4
- return db.transaction("events", "readwrite");
5
- }
3
+ /**
4
+ * Returns an events tags as an array of string for indexing
5
+ */
6
6
  export function getEventTags(event) {
7
7
  return event.tags
8
8
  .filter((t) => t.length >= 2 && t[0].length === 1 && GENERIC_TAGS.includes(t[0]))
9
9
  .map((t) => t[0] + t[1]);
10
10
  }
11
+ // based on replaceable kinds from https://github.com/nostr-protocol/nips/blob/master/01.md#kinds
12
+ export function isReplaceable(kind) {
13
+ return ((kind >= 30000 && kind < 40000) ||
14
+ (kind >= 10000 && kind < 20000) ||
15
+ kind === 0 ||
16
+ kind === 3 ||
17
+ kind === 41);
18
+ }
19
+ function getReplaceableEventAddress(event) {
20
+ // only create addresses for replaceable events
21
+ if (!isReplaceable(event.kind))
22
+ return undefined;
23
+ // get d tag
24
+ const identifier = event.tags.find((t) => t[0] === "d" && t[1])?.[1];
25
+ return { kind: event.kind, pubkey: event.pubkey, identifier };
26
+ }
11
27
  export async function addEvent(db, event, transaction) {
12
28
  if (!validateEvent(event))
13
29
  throw new Error("Invalid Event");
14
- const trans = transaction || createWriteTransaction(db);
30
+ const trans = transaction || db.transaction("events", "readwrite");
15
31
  trans.objectStore("events").put({
16
32
  event,
17
33
  tags: getEventTags(event),
34
+ identifier: getReplaceableEventAddress(event)?.identifier,
18
35
  });
19
36
  if (!transaction)
20
37
  await trans.commit();
@@ -1,12 +1,13 @@
1
1
  import type { Event, Filter } from "nostr-tools";
2
2
  import type { NostrIDB } from "./schema.js";
3
- export declare function queryForPubkeys(db: NostrIDB, authors?: Filter["authors"]): Promise<Set<string>>;
4
- export declare function queryForTag(db: NostrIDB, tag: string, values: string[]): Promise<Set<string>>;
5
- export declare function queryForKinds(db: NostrIDB, kinds?: Filter["kinds"]): Promise<Set<string>>;
3
+ import { IndexCache } from "./index-cache";
4
+ export declare function queryForPubkeys(db: NostrIDB, authors?: Filter["authors"], indexCache?: IndexCache): Set<string> | Promise<Set<string>>;
5
+ export declare function queryForTag(db: NostrIDB, tag: string, values: string[], indexCache?: IndexCache): Set<string> | Promise<Set<string>>;
6
+ export declare function queryForKinds(db: NostrIDB, kinds?: Filter["kinds"], indexCache?: IndexCache): Set<string> | Promise<Set<string>>;
6
7
  export declare function queryForTime(db: NostrIDB, since: number | undefined, until: number | undefined): Promise<Set<string>>;
7
- export declare function getIdsForFilter(db: NostrIDB, filter: Filter): Promise<Set<string>>;
8
- export declare function getIdsForFilters(db: NostrIDB, filters: Filter[]): Promise<Set<string>>;
9
- export declare function getEventsForFilter(db: NostrIDB, filter: Filter): Promise<Event<number>[]>;
10
- export declare function getEventsForFilters(db: NostrIDB, filters: Filter[]): Promise<Event<number>[]>;
11
- export declare function countEventsForFilter(db: NostrIDB, filter: Filter): Promise<number>;
12
- export declare function countEventsForFilters(db: NostrIDB, filters: Filter[]): Promise<number>;
8
+ export declare function getIdsForFilter(db: NostrIDB, filter: Filter, indexCache?: IndexCache): Promise<Set<string>>;
9
+ export declare function getIdsForFilters(db: NostrIDB, filters: Filter[], indexCache?: IndexCache): Promise<Set<string>>;
10
+ export declare function getEventsForFilter(db: NostrIDB, filter: Filter, indexCache?: IndexCache): Promise<Event<number>[]>;
11
+ export declare function getEventsForFilters(db: NostrIDB, filters: Filter[], indexCache?: IndexCache): Promise<Event<number>[]>;
12
+ export declare function countEventsForFilter(db: NostrIDB, filter: Filter, indexCache?: IndexCache): Promise<number>;
13
+ export declare function countEventsForFilters(db: NostrIDB, filters: Filter[], indexCache?: IndexCache): Promise<number>;
@@ -1,45 +1,101 @@
1
1
  import { GENERIC_TAGS } from "./common.js";
2
- export function queryForPubkeys(db, authors = []) {
2
+ import { sortByDate } from "./utils";
3
+ export function queryForPubkeys(db, authors = [], indexCache) {
4
+ const loaded = [];
3
5
  const ids = new Set();
6
+ if (indexCache) {
7
+ for (const pubkey of authors) {
8
+ const cached = indexCache.getPubkeyIndex(pubkey);
9
+ if (cached) {
10
+ for (const id of cached)
11
+ ids.add(id);
12
+ loaded.push(pubkey);
13
+ }
14
+ }
15
+ }
16
+ // all indexes where loaded from indexCache
17
+ if (loaded.length === authors.length)
18
+ return ids;
19
+ // load remaining indexes from db
4
20
  const trans = db.transaction("events", "readonly");
5
21
  const objectStore = trans.objectStore("events");
6
22
  const index = objectStore.index("pubkey");
7
- const handleEvents = (result) => {
23
+ const handleResults = (pubkey, result) => {
8
24
  for (const id of result)
9
25
  ids.add(id);
26
+ // add index to cache
27
+ if (indexCache)
28
+ indexCache.setPubkeyIndex(pubkey, new Set(result));
10
29
  };
11
- const promises = authors.map((pubkey) => index.getAllKeys(pubkey).then(handleEvents));
12
- const result = Promise.all(promises).then(() => ids);
30
+ const promises = authors
31
+ .filter((p) => !loaded.includes(p))
32
+ .map((pubkey) => index.getAllKeys(pubkey).then((r) => handleResults(pubkey, r)));
13
33
  trans.commit();
14
- return result;
34
+ return Promise.all(promises).then(() => ids);
15
35
  }
16
- export function queryForTag(db, tag, values) {
36
+ export function queryForTag(db, tag, values, indexCache) {
37
+ const loaded = [];
17
38
  const ids = new Set();
39
+ if (indexCache) {
40
+ for (const value of values) {
41
+ const cached = indexCache.getTagIndex(tag + value);
42
+ if (cached) {
43
+ for (const id of cached)
44
+ ids.add(id);
45
+ loaded.push(value);
46
+ }
47
+ }
48
+ }
49
+ // all indexes where loaded from indexCache
50
+ if (loaded.length === values.length)
51
+ return ids;
52
+ // load remaining indexes from db
18
53
  const trans = db.transaction("events", "readonly");
19
54
  const objectStore = trans.objectStore("events");
20
55
  const index = objectStore.index("tags");
21
- const handleEvents = (result) => {
56
+ const handleResults = (value, result) => {
22
57
  for (const id of result)
23
58
  ids.add(id);
59
+ // add index to cache
60
+ if (indexCache)
61
+ indexCache.setTagIndex(tag + value, new Set(result));
24
62
  };
25
- const promises = values.map((v) => index.getAllKeys(tag + v).then(handleEvents));
26
- const result = Promise.all(promises).then(() => ids);
63
+ const promises = values.map((v) => index.getAllKeys(tag + v).then((r) => handleResults(v, r)));
27
64
  trans.commit();
28
- return result;
65
+ return Promise.all(promises).then(() => ids);
29
66
  }
30
- export function queryForKinds(db, kinds = []) {
67
+ export function queryForKinds(db, kinds = [], indexCache) {
68
+ const loaded = [];
31
69
  const ids = new Set();
70
+ // load from indexCache
71
+ if (indexCache) {
72
+ for (const kind of kinds) {
73
+ const cached = indexCache.getKindIndex(kind);
74
+ if (cached) {
75
+ for (const id of cached)
76
+ ids.add(id);
77
+ loaded.push(kind);
78
+ }
79
+ }
80
+ }
81
+ // all indexes where loaded from indexCache
82
+ if (loaded.length === kinds.length)
83
+ return ids;
84
+ // load remaining indexes from db
32
85
  const trans = db.transaction("events", "readonly");
33
- const objectStore = trans.objectStore("events");
34
- const index = objectStore.index("kind");
35
- const handleEvents = (result) => {
86
+ const index = trans.objectStore("events").index("kind");
87
+ const handleResults = (kind, result) => {
36
88
  for (const id of result)
37
89
  ids.add(id);
90
+ // add index to cache
91
+ if (indexCache)
92
+ indexCache.setKindIndex(kind, new Set(result));
38
93
  };
39
- const promises = kinds.map((kind) => index.getAllKeys(kind).then(handleEvents));
40
- const result = Promise.all(promises).then(() => ids);
94
+ const promises = kinds
95
+ .filter((k) => !loaded.includes(k))
96
+ .map((kind) => index.getAllKeys(kind).then((r) => handleResults(kind, r)));
41
97
  trans.commit();
42
- return result;
98
+ return Promise.all(promises).then(() => ids);
43
99
  }
44
100
  export async function queryForTime(db, since, until) {
45
101
  let range;
@@ -51,11 +107,11 @@ export async function queryForTime(db, since, until) {
51
107
  range = IDBKeyRange.upperBound(until);
52
108
  else
53
109
  throw new Error("Missing since or until");
54
- const arr = await db.getAllKeysFromIndex("events", "create_at", range);
110
+ const arr = await db.getAllKeysFromIndex("events", "created_at", range);
55
111
  const ids = new Set(arr);
56
112
  return ids;
57
113
  }
58
- export async function getIdsForFilter(db, filter) {
114
+ export async function getIdsForFilter(db, filter, indexCache) {
59
115
  // search is not supported, return an empty set
60
116
  if (filter.search)
61
117
  return new Set();
@@ -78,12 +134,12 @@ export async function getIdsForFilter(db, filter) {
78
134
  const key = `#${t}`;
79
135
  const values = filter[key];
80
136
  if (values?.length)
81
- and(await queryForTag(db, t, values));
137
+ and(await queryForTag(db, t, values, indexCache));
82
138
  }
83
139
  if (filter.authors)
84
- and(await queryForPubkeys(db, filter.authors));
140
+ and(await queryForPubkeys(db, filter.authors, indexCache));
85
141
  if (filter.kinds)
86
- and(await queryForKinds(db, filter.kinds));
142
+ and(await queryForKinds(db, filter.kinds, indexCache));
87
143
  // query for time last if only one is set
88
144
  if ((filter.since === undefined && filter.until) ||
89
145
  (filter.since && filter.until === undefined))
@@ -92,12 +148,12 @@ export async function getIdsForFilter(db, filter) {
92
148
  throw new Error("Empty filter");
93
149
  return ids;
94
150
  }
95
- export async function getIdsForFilters(db, filters) {
151
+ export async function getIdsForFilters(db, filters, indexCache) {
96
152
  if (filters.length === 0)
97
153
  throw new Error("No Filters");
98
154
  let ids = null;
99
155
  for (const filter of filters) {
100
- const filterIds = await getIdsForFilter(db, filter);
156
+ const filterIds = await getIdsForFilter(db, filter, indexCache);
101
157
  if (!ids)
102
158
  ids = filterIds;
103
159
  else
@@ -116,7 +172,7 @@ async function loadEventsById(db, ids, filters) {
116
172
  const index = objectStore.index("id");
117
173
  const handleEntry = (e) => e && events.push(e.event);
118
174
  const promises = Array.from(ids).map((id) => index.get(id).then(handleEntry));
119
- const sorted = await Promise.all(promises).then(() => events.sort((a, b) => b.created_at - a.created_at));
175
+ const sorted = await Promise.all(promises).then(() => events.sort(sortByDate));
120
176
  trans.commit();
121
177
  let minLimit = Infinity;
122
178
  for (const filter of filters) {
@@ -127,19 +183,19 @@ async function loadEventsById(db, ids, filters) {
127
183
  sorted.length = minLimit;
128
184
  return sorted;
129
185
  }
130
- export async function getEventsForFilter(db, filter) {
131
- const ids = await getIdsForFilter(db, filter);
186
+ export async function getEventsForFilter(db, filter, indexCache) {
187
+ const ids = await getIdsForFilter(db, filter, indexCache);
132
188
  return await loadEventsById(db, Array.from(ids), [filter]);
133
189
  }
134
- export async function getEventsForFilters(db, filters) {
135
- const ids = await getIdsForFilters(db, filters);
190
+ export async function getEventsForFilters(db, filters, indexCache) {
191
+ const ids = await getIdsForFilters(db, filters, indexCache);
136
192
  return await loadEventsById(db, Array.from(ids), filters);
137
193
  }
138
- export async function countEventsForFilter(db, filter) {
139
- const ids = await getIdsForFilter(db, filter);
194
+ export async function countEventsForFilter(db, filter, indexCache) {
195
+ const ids = await getIdsForFilter(db, filter, indexCache);
140
196
  return ids.size;
141
197
  }
142
- export async function countEventsForFilters(db, filters) {
143
- const ids = await getIdsForFilters(db, filters);
198
+ export async function countEventsForFilters(db, filters, indexCache) {
199
+ const ids = await getIdsForFilters(db, filters, indexCache);
144
200
  return ids.size;
145
201
  }
@@ -1,4 +1,7 @@
1
+ import type { Event } from "nostr-tools";
1
2
  import { NostrIDB } from "./schema.js";
3
+ import { ReplaceableEventAddress } from "./ingest";
4
+ export declare function getEventsFromAddressPointers(db: NostrIDB, pointers: ReplaceableEventAddress[]): Promise<Event<number>[]>;
2
5
  export declare function countEventsByPubkeys(db: NostrIDB): Promise<Record<string, number>>;
3
6
  export declare function countEventsByKind(db: NostrIDB): Promise<Record<string, number>>;
4
7
  export declare function countEvents(db: NostrIDB): Promise<number>;
@@ -1,3 +1,21 @@
1
+ export async function getEventsFromAddressPointers(db, pointers) {
2
+ const trans = db.transaction("events", "readonly");
3
+ const index = trans.objectStore("events").index("addressPointer");
4
+ const events = {};
5
+ const promises = pointers.map(async (pointer) => {
6
+ const key = [pointer.kind, pointer.pubkey, pointer.identifier];
7
+ const row = await index.get(key);
8
+ if (row) {
9
+ const keyStr = key.join(":");
10
+ const existing = events[keyStr];
11
+ if (!existing || row.event.created_at > existing.created_at)
12
+ events[keyStr] = row.event;
13
+ }
14
+ });
15
+ const sorted = await Promise.all(promises).then(() => Object.values(events).sort((a, b) => b.created_at - a.created_at));
16
+ trans.commit();
17
+ return sorted;
18
+ }
1
19
  export async function countEventsByPubkeys(db) {
2
20
  let cursor = await db
3
21
  .transaction("events", "readonly")
@@ -0,0 +1,51 @@
1
+ import { Event, Filter } from "nostr-tools";
2
+ import { NostrIDB } from "./schema";
3
+ export interface SimpleRelay {
4
+ url: string;
5
+ publish(event: Event): Promise<string>;
6
+ connected: boolean;
7
+ connect(): Promise<void>;
8
+ close(): void;
9
+ count(filters: Filter[], params?: {
10
+ id?: string | null;
11
+ }): Promise<number>;
12
+ subscribe(filters: Filter[], options: SimpleSubscriptionOptions): SimpleSubscription;
13
+ }
14
+ export type SimpleSubscriptionOptions = {
15
+ onevent?: (event: Event) => void;
16
+ oneose?: () => void;
17
+ onclose?: (reason: string) => void;
18
+ };
19
+ export type SimpleSubscription = SimpleSubscriptionOptions & {
20
+ id: string;
21
+ filters: Filter[];
22
+ close(message?: string): void;
23
+ };
24
+ export type CacheRelayOptions = {
25
+ /** Defaults to 1000 */
26
+ batchWrite?: number;
27
+ /** Defaults to 100 */
28
+ writeInterval?: number;
29
+ /** number of indexes to cache in memory. defaults to 1000 */
30
+ cacheIndexes?: number;
31
+ };
32
+ export declare class CacheRelay implements SimpleRelay {
33
+ get url(): string;
34
+ get connected(): boolean;
35
+ private options;
36
+ private interval?;
37
+ private db;
38
+ private writeQueue;
39
+ private indexCache;
40
+ private nextId;
41
+ private subscriptions;
42
+ constructor(db: NostrIDB, opts?: CacheRelayOptions);
43
+ connect(): Promise<void>;
44
+ close(): Promise<void>;
45
+ publish(event: Event): Promise<string>;
46
+ count(filters: Filter[], params?: {
47
+ id?: string | null;
48
+ }): Promise<number>;
49
+ private executeSubscription;
50
+ subscribe(filters: Filter[], options: Partial<SimpleSubscriptionOptions>): SimpleSubscription;
51
+ }
package/dist/relay.js ADDED
@@ -0,0 +1,92 @@
1
+ import { matchFilters } from "nostr-tools";
2
+ import { WriteQueue } from "./write-queue";
3
+ import { countEventsForFilters, getEventsForFilters } from "./query-filter";
4
+ import { sortByDate } from "./utils";
5
+ import { IndexCache } from "./index-cache";
6
+ const defaultOptions = {
7
+ batchWrite: 1000,
8
+ writeInterval: 100,
9
+ cacheIndexes: 1000,
10
+ };
11
+ export class CacheRelay {
12
+ get url() {
13
+ return "[Internal]";
14
+ }
15
+ get connected() {
16
+ return !!this.interval;
17
+ }
18
+ options;
19
+ interval;
20
+ db;
21
+ writeQueue;
22
+ indexCache;
23
+ nextId = 0;
24
+ subscriptions = new Set();
25
+ constructor(db, opts = {}) {
26
+ this.db = db;
27
+ this.writeQueue = new WriteQueue(db);
28
+ this.options = { ...defaultOptions, ...opts };
29
+ this.indexCache = new IndexCache();
30
+ this.indexCache.max = this.options.cacheIndexes;
31
+ }
32
+ async connect() {
33
+ this.interval = window.setInterval(() => {
34
+ this.writeQueue.flush(this.options.batchWrite);
35
+ }, this.options.writeInterval);
36
+ }
37
+ async close() {
38
+ if (this.interval) {
39
+ window.clearInterval(this.interval);
40
+ this.interval = undefined;
41
+ }
42
+ }
43
+ async publish(event) {
44
+ this.writeQueue.addEvent(event);
45
+ this.indexCache.addEventToIndexes(event);
46
+ let subs = 0;
47
+ for (const { onevent, filters } of this.subscriptions) {
48
+ if (onevent && matchFilters(filters, event)) {
49
+ onevent(event);
50
+ subs++;
51
+ }
52
+ }
53
+ return `Sent to ${subs} subscriptions`;
54
+ }
55
+ async count(filters, params) {
56
+ return await countEventsForFilters(this.db, filters);
57
+ }
58
+ async executeSubscription(sub) {
59
+ // load any events from the write queue
60
+ const eventsFromQueue = this.writeQueue.queue.filter((e) => matchFilters(sub.filters, e));
61
+ // get events
62
+ await getEventsForFilters(this.db, sub.filters, this.indexCache).then((filterEvents) => {
63
+ if (sub.onevent) {
64
+ const idsFromQueue = new Set(eventsFromQueue.map((e) => e.id));
65
+ const events = eventsFromQueue.length > 0
66
+ ? [
67
+ ...filterEvents.filter((e) => !idsFromQueue.has(e.id)),
68
+ ...eventsFromQueue,
69
+ ].sort(sortByDate)
70
+ : filterEvents;
71
+ for (const event of events) {
72
+ sub.onevent(event);
73
+ }
74
+ }
75
+ if (sub.oneose)
76
+ sub.oneose();
77
+ });
78
+ }
79
+ subscribe(filters, options) {
80
+ const id = this.nextId++;
81
+ const sub = {
82
+ id: String(id),
83
+ filters,
84
+ close: () => this.subscriptions.delete(sub),
85
+ fire: () => this.executeSubscription(sub),
86
+ ...options,
87
+ };
88
+ this.subscriptions.add(sub);
89
+ this.executeSubscription(sub);
90
+ return sub;
91
+ }
92
+ }
package/dist/schema.d.ts CHANGED
@@ -7,13 +7,15 @@ export interface Schema extends DBSchema {
7
7
  value: {
8
8
  event: Event;
9
9
  tags: string[];
10
+ identifier?: string;
10
11
  };
11
12
  indexes: {
12
13
  id: string;
13
14
  pubkey: string;
14
15
  kind: number;
15
- create_at: number;
16
+ created_at: number;
16
17
  tags: string;
18
+ addressPointer: [number, string, string | undefined];
17
19
  };
18
20
  };
19
21
  seen: {
@@ -0,0 +1,2 @@
1
+ import { Event } from "nostr-tools";
2
+ export declare function sortByDate(a: Event, b: Event): number;
package/dist/utils.js ADDED
@@ -0,0 +1,3 @@
1
+ export function sortByDate(a, b) {
2
+ return b.created_at - a.created_at;
3
+ }
@@ -0,0 +1,11 @@
1
+ import type { Event } from "nostr-tools";
2
+ import { NostrIDB } from "./schema";
3
+ export declare class WriteQueue {
4
+ db: NostrIDB;
5
+ queue: Event[];
6
+ constructor(db: NostrIDB);
7
+ addEvent(event: Event): void;
8
+ addEvents(events: Event[]): void;
9
+ flush(count?: number): Promise<number>;
10
+ clear(): void;
11
+ }
@@ -0,0 +1,29 @@
1
+ import { addEvents, updateUsed } from "./ingest";
2
+ export class WriteQueue {
3
+ db;
4
+ queue = [];
5
+ constructor(db) {
6
+ this.db = db;
7
+ }
8
+ addEvent(event) {
9
+ this.queue.push(event);
10
+ }
11
+ addEvents(events) {
12
+ this.queue.push(...events);
13
+ }
14
+ async flush(count = 1000) {
15
+ const events = [];
16
+ for (let i = 0; i < count; i++) {
17
+ const event = this.queue.shift();
18
+ if (!event)
19
+ break;
20
+ events.push(event);
21
+ }
22
+ await addEvents(this.db, events);
23
+ await updateUsed(this.db, events.map((e) => e.id));
24
+ return events.length;
25
+ }
26
+ clear() {
27
+ this.queue = [];
28
+ }
29
+ }
package/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "nostr-idb",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "A collection of helper methods for storing nostr events in IndexedDB",
5
+ "author": {
6
+ "name": "hzrd149"
7
+ },
5
8
  "license": "MIT",
6
9
  "repository": {
7
10
  "type": "git",
@@ -24,7 +27,7 @@
24
27
  "nostr-tools": "^1.17.0"
25
28
  },
26
29
  "devDependencies": {
27
- "@types/debug": "^4.1.12",
30
+ "@changesets/cli": "^2.27.1",
28
31
  "@types/react": "^18.2.37",
29
32
  "@types/react-dom": "^18.2.15",
30
33
  "@vitejs/plugin-react-swc": "^3.5.0",
@@ -34,5 +37,9 @@
34
37
  "react-dom": "^18.2.0",
35
38
  "typescript": "^5.3.3",
36
39
  "vite": "^5.0.0"
40
+ },
41
+ "funding": {
42
+ "type": "lightning",
43
+ "url": "lightning:hzrd149@getalby.com"
37
44
  }
38
45
  }