nostr-idb 0.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # nostr-idb
2
2
 
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 9bbdbc7: Upgrade nostr-tools to v2
8
+
9
+ ### Minor Changes
10
+
11
+ - 9bbdbc7: Support ephemeral events
12
+ - 446cb1e: Support replaceable and parameterized replaceable events
13
+
3
14
  ## 0.2.0
4
15
 
5
16
  ### Minor Changes
@@ -1,7 +1,7 @@
1
1
  import { DeleteDBCallbacks, OpenDBCallbacks } from "idb";
2
2
  import { NostrIDB, Schema } from "./schema.js";
3
3
  export declare const NOSTR_IDB_NAME = "nostr-idb";
4
- export declare const NOSTR_IDB_VERSION = 1;
4
+ export declare const NOSTR_IDB_VERSION = 2;
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
7
  export declare function clearDB(db: NostrIDB): Promise<void>;
package/dist/database.js CHANGED
@@ -1,21 +1,25 @@
1
1
  import { deleteDB as idbDeleteDB, openDB as idbOpenDB, } from "idb";
2
2
  export const NOSTR_IDB_NAME = "nostr-idb";
3
- export const NOSTR_IDB_VERSION = 1;
3
+ export const NOSTR_IDB_VERSION = 2;
4
4
  export async function openDB(name = NOSTR_IDB_NAME, callbacks) {
5
5
  return await idbOpenDB(name, NOSTR_IDB_VERSION, {
6
6
  ...callbacks,
7
7
  upgrade(db, oldVersion, newVersion, transaction, event) {
8
- const events = db.createObjectStore("events", { keyPath: "event.id" });
8
+ if (oldVersion < 2) {
9
+ debugger;
10
+ console.log("resetting DB");
11
+ db.deleteObjectStore("events");
12
+ db.deleteObjectStore("used");
13
+ // @ts-ignore
14
+ db.deleteObjectStore("seen");
15
+ }
16
+ const events = db.createObjectStore("events");
9
17
  events.createIndex("id", "event.id", { unique: true });
10
18
  events.createIndex("pubkey", "event.pubkey");
11
19
  events.createIndex("kind", "event.kind");
12
20
  events.createIndex("created_at", "event.created_at");
13
21
  events.createIndex("tags", "tags", { multiEntry: true });
14
- events.createIndex("addressPointer", ["kind", "pubkey", "identifier"]);
15
- const seen = db.createObjectStore("seen", { keyPath: "id" });
16
- seen.createIndex("date", "date");
17
- seen.createIndex("relay", "relays", { multiEntry: true });
18
- const used = db.createObjectStore("used", { keyPath: "id" });
22
+ const used = db.createObjectStore("used", { keyPath: "uid" });
19
23
  used.createIndex("date", "date");
20
24
  if (callbacks?.upgrade)
21
25
  callbacks.upgrade(db, oldVersion, newVersion, transaction, event);
@@ -28,5 +32,4 @@ export async function deleteDB(name = NOSTR_IDB_NAME, callbacks) {
28
32
  export async function clearDB(db) {
29
33
  await db.clear("events");
30
34
  await db.clear("used");
31
- await db.clear("seen");
32
35
  }
package/dist/ingest.d.ts CHANGED
@@ -1,16 +1,10 @@
1
1
  import { IDBPTransaction } from "idb";
2
2
  import { Event } from "nostr-tools";
3
3
  import type { NostrIDB, Schema } from "./schema.js";
4
- /**
5
- * Returns an events tags as an array of string for indexing
6
- */
4
+ /** Returns an events tags as an array of string for indexing */
7
5
  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
+ /** returns the events Unique ID */
7
+ export declare function getEventUID(event: Event): string;
14
8
  export declare function addEvent(db: NostrIDB, event: Event, transaction?: IDBPTransaction<Schema, ["events"], "readwrite">): Promise<void>;
15
9
  export declare function addEvents(db: NostrIDB, events: Event[]): Promise<void>;
16
- export declare function updateUsed(db: NostrIDB, ids: string[]): Promise<void>;
10
+ export declare function updateUsed(db: NostrIDB, uids: Iterable<string>): Promise<void>;
package/dist/ingest.js CHANGED
@@ -1,28 +1,19 @@
1
- import { validateEvent } from "nostr-tools";
1
+ import { kinds, validateEvent } from "nostr-tools";
2
2
  import { GENERIC_TAGS } from "./common.js";
3
- /**
4
- * Returns an events tags as an array of string for indexing
5
- */
3
+ /** Returns an events tags as an array of string for indexing */
6
4
  export function getEventTags(event) {
7
5
  return event.tags
8
6
  .filter((t) => t.length >= 2 && t[0].length === 1 && GENERIC_TAGS.includes(t[0]))
9
7
  .map((t) => t[0] + t[1]);
10
8
  }
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 };
9
+ /** returns the events Unique ID */
10
+ export function getEventUID(event) {
11
+ if (kinds.isReplaceableKind(event.kind) ||
12
+ kinds.isParameterizedReplaceableKind(event.kind)) {
13
+ const d = event.tags.find((t) => t[0] === "d")?.[1];
14
+ return `${event.kind}:${event.pubkey}:${d ?? ""}`;
15
+ }
16
+ return event.id;
26
17
  }
27
18
  export async function addEvent(db, event, transaction) {
28
19
  if (!validateEvent(event))
@@ -31,8 +22,7 @@ export async function addEvent(db, event, transaction) {
31
22
  trans.objectStore("events").put({
32
23
  event,
33
24
  tags: getEventTags(event),
34
- identifier: getReplaceableEventAddress(event)?.identifier,
35
- });
25
+ }, getEventUID(event));
36
26
  if (!transaction)
37
27
  await trans.commit();
38
28
  }
@@ -43,12 +33,12 @@ export async function addEvents(db, events) {
43
33
  }
44
34
  await trans.commit();
45
35
  }
46
- export async function updateUsed(db, ids) {
36
+ export async function updateUsed(db, uids) {
47
37
  const trans = db.transaction("used", "readwrite");
48
38
  const nowUnix = Math.floor(new Date().valueOf() / 1000);
49
- for (const id of ids) {
39
+ for (const uid of uids) {
50
40
  trans.objectStore("used").put({
51
- id,
41
+ uid,
52
42
  date: nowUnix,
53
43
  });
54
44
  }
@@ -4,10 +4,10 @@ import { IndexCache } from "./index-cache";
4
4
  export declare function queryForPubkeys(db: NostrIDB, authors?: Filter["authors"], indexCache?: IndexCache): Set<string> | Promise<Set<string>>;
5
5
  export declare function queryForTag(db: NostrIDB, tag: string, values: string[], indexCache?: IndexCache): Set<string> | Promise<Set<string>>;
6
6
  export declare function queryForKinds(db: NostrIDB, kinds?: Filter["kinds"], indexCache?: IndexCache): Set<string> | Promise<Set<string>>;
7
- export declare function queryForTime(db: NostrIDB, since: number | undefined, until: number | undefined): Promise<Set<string>>;
7
+ export declare function queryForTime(db: NostrIDB, since: number | undefined, until: number | undefined): Promise<string[]>;
8
8
  export declare function getIdsForFilter(db: NostrIDB, filter: Filter, indexCache?: IndexCache): Promise<Set<string>>;
9
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>[]>;
10
+ export declare function getEventsForFilter(db: NostrIDB, filter: Filter, indexCache?: IndexCache): Promise<Event[]>;
11
+ export declare function getEventsForFilters(db: NostrIDB, filters: Filter[], indexCache?: IndexCache): Promise<Event[]>;
12
12
  export declare function countEventsForFilter(db: NostrIDB, filter: Filter, indexCache?: IndexCache): Promise<number>;
13
13
  export declare function countEventsForFilters(db: NostrIDB, filters: Filter[], indexCache?: IndexCache): Promise<number>;
@@ -107,8 +107,7 @@ export async function queryForTime(db, since, until) {
107
107
  range = IDBKeyRange.upperBound(until);
108
108
  else
109
109
  throw new Error("Missing since or until");
110
- const arr = await db.getAllKeysFromIndex("events", "created_at", range);
111
- const ids = new Set(arr);
110
+ const ids = (await db.getAllKeysFromIndex("events", "created_at", range)).reverse();
112
111
  return ids;
113
112
  }
114
113
  export async function getIdsForFilter(db, filter, indexCache) {
@@ -118,18 +117,22 @@ export async function getIdsForFilter(db, filter, indexCache) {
118
117
  if (filter.ids)
119
118
  return new Set(filter.ids);
120
119
  let ids = null;
121
- const and = (set) => {
120
+ const and = (iterable) => {
121
+ const set = iterable instanceof Set ? iterable : new Set(iterable);
122
122
  if (!ids)
123
123
  ids = set;
124
- for (const id of ids) {
125
- if (!set.has(id))
126
- ids.delete(id);
127
- }
124
+ else
125
+ for (const id of ids)
126
+ if (!set.has(id))
127
+ ids.delete(id);
128
128
  return ids;
129
129
  };
130
- // query for time first if both are set
131
- if (filter.since && filter.until)
132
- and(await queryForTime(db, filter.since, filter.until));
130
+ let timeFilterIds = null;
131
+ // query for time first if since is set
132
+ if (filter.since) {
133
+ timeFilterIds = await queryForTime(db, filter.since, filter.until);
134
+ and(timeFilterIds);
135
+ }
133
136
  for (const t of GENERIC_TAGS) {
134
137
  const key = `#${t}`;
135
138
  const values = filter[key];
@@ -140,10 +143,22 @@ export async function getIdsForFilter(db, filter, indexCache) {
140
143
  and(await queryForPubkeys(db, filter.authors, indexCache));
141
144
  if (filter.kinds)
142
145
  and(await queryForKinds(db, filter.kinds, indexCache));
143
- // query for time last if only one is set
144
- if ((filter.since === undefined && filter.until) ||
145
- (filter.since && filter.until === undefined))
146
- and(await queryForTime(db, filter.since, filter.until));
146
+ // query for time last if only until is set
147
+ if (filter.since === undefined && filter.until) {
148
+ timeFilterIds = await queryForTime(db, filter.since, filter.until);
149
+ and(timeFilterIds);
150
+ }
151
+ // if the filter queried on time and has a limit. truncate the ids now
152
+ if (filter.limit && timeFilterIds) {
153
+ const limitIds = new Set();
154
+ for (const id of timeFilterIds) {
155
+ if (limitIds.size >= filter.limit)
156
+ break;
157
+ if (ids.has(id))
158
+ limitIds.add(id);
159
+ }
160
+ return limitIds;
161
+ }
147
162
  if (ids === null)
148
163
  throw new Error("Empty filter");
149
164
  return ids;
@@ -165,15 +180,14 @@ export async function getIdsForFilters(db, filters, indexCache) {
165
180
  throw new Error("Empty filters");
166
181
  return ids;
167
182
  }
168
- async function loadEventsById(db, ids, filters) {
169
- const events = [];
183
+ async function loadEventsByUID(db, uids, filters) {
184
+ const eventBuffer = [];
170
185
  const trans = db.transaction("events", "readonly");
171
186
  const objectStore = trans.objectStore("events");
172
- const index = objectStore.index("id");
173
- const handleEntry = (e) => e && events.push(e.event);
174
- const promises = Array.from(ids).map((id) => index.get(id).then(handleEntry));
175
- const sorted = await Promise.all(promises).then(() => events.sort(sortByDate));
187
+ const handleEntry = (e) => e && eventBuffer.push(e.event);
188
+ const promises = Array.from(uids).map((uid) => objectStore.get(uid).then(handleEntry));
176
189
  trans.commit();
190
+ const sorted = await Promise.all(promises).then(() => eventBuffer.sort(sortByDate));
177
191
  let minLimit = Infinity;
178
192
  for (const filter of filters) {
179
193
  if (filter.limit && filter.limit < minLimit)
@@ -185,11 +199,11 @@ async function loadEventsById(db, ids, filters) {
185
199
  }
186
200
  export async function getEventsForFilter(db, filter, indexCache) {
187
201
  const ids = await getIdsForFilter(db, filter, indexCache);
188
- return await loadEventsById(db, Array.from(ids), [filter]);
202
+ return await loadEventsByUID(db, Array.from(ids), [filter]);
189
203
  }
190
204
  export async function getEventsForFilters(db, filters, indexCache) {
191
205
  const ids = await getIdsForFilters(db, filters, indexCache);
192
- return await loadEventsById(db, Array.from(ids), filters);
206
+ return await loadEventsByUID(db, Array.from(ids), filters);
193
207
  }
194
208
  export async function countEventsForFilter(db, filter, indexCache) {
195
209
  const ids = await getIdsForFilter(db, filter, indexCache);
@@ -1,7 +1,10 @@
1
1
  import type { Event } from "nostr-tools";
2
2
  import { NostrIDB } from "./schema.js";
3
- import { ReplaceableEventAddress } from "./ingest";
4
- export declare function getEventsFromAddressPointers(db: NostrIDB, pointers: ReplaceableEventAddress[]): Promise<Event<number>[]>;
3
+ export declare function getEventsFromAddressPointers(db: NostrIDB, pointers: {
4
+ kind: number;
5
+ pubkey: string;
6
+ identifier?: string;
7
+ }[]): Promise<Event[]>;
5
8
  export declare function countEventsByPubkeys(db: NostrIDB): Promise<Record<string, number>>;
6
9
  export declare function countEventsByKind(db: NostrIDB): Promise<Record<string, number>>;
7
10
  export declare function countEvents(db: NostrIDB): Promise<number>;
@@ -1,19 +1,18 @@
1
1
  export async function getEventsFromAddressPointers(db, pointers) {
2
2
  const trans = db.transaction("events", "readonly");
3
- const index = trans.objectStore("events").index("addressPointer");
3
+ const objectStore = trans.objectStore("events");
4
4
  const events = {};
5
5
  const promises = pointers.map(async (pointer) => {
6
- const key = [pointer.kind, pointer.pubkey, pointer.identifier];
7
- const row = await index.get(key);
6
+ const key = `${pointer.kind}:${pointer.pubkey}:${pointer.identifier ?? ""}`;
7
+ const row = await objectStore.get(key);
8
8
  if (row) {
9
- const keyStr = key.join(":");
10
- const existing = events[keyStr];
9
+ const existing = events[key];
11
10
  if (!existing || row.event.created_at > existing.created_at)
12
- events[keyStr] = row.event;
11
+ events[key] = row.event;
13
12
  }
14
13
  });
15
- const sorted = await Promise.all(promises).then(() => Object.values(events).sort((a, b) => b.created_at - a.created_at));
16
14
  trans.commit();
15
+ const sorted = await Promise.all(promises).then(() => Object.values(events).sort((a, b) => b.created_at - a.created_at));
17
16
  return sorted;
18
17
  }
19
18
  export async function countEventsByPubkeys(db) {
package/dist/relay.js CHANGED
@@ -1,4 +1,4 @@
1
- import { matchFilters } from "nostr-tools";
1
+ import { kinds, matchFilters } from "nostr-tools";
2
2
  import { WriteQueue } from "./write-queue";
3
3
  import { countEventsForFilters, getEventsForFilters } from "./query-filter";
4
4
  import { sortByDate } from "./utils";
@@ -41,8 +41,10 @@ export class CacheRelay {
41
41
  }
42
42
  }
43
43
  async publish(event) {
44
- this.writeQueue.addEvent(event);
45
- this.indexCache.addEventToIndexes(event);
44
+ if (!kinds.isEphemeralKind(event.kind)) {
45
+ this.writeQueue.addEvent(event);
46
+ this.indexCache.addEventToIndexes(event);
47
+ }
46
48
  let subs = 0;
47
49
  for (const { onevent, filters } of this.subscriptions) {
48
50
  if (onevent && matchFilters(filters, event)) {
@@ -57,7 +59,7 @@ export class CacheRelay {
57
59
  }
58
60
  async executeSubscription(sub) {
59
61
  // load any events from the write queue
60
- const eventsFromQueue = this.writeQueue.queue.filter((e) => matchFilters(sub.filters, e));
62
+ const eventsFromQueue = this.writeQueue.matchPending(sub.filters);
61
63
  // get events
62
64
  await getEventsForFilters(this.db, sub.filters, this.indexCache).then((filterEvents) => {
63
65
  if (sub.onevent) {
@@ -70,6 +72,7 @@ export class CacheRelay {
70
72
  : filterEvents;
71
73
  for (const event of events) {
72
74
  sub.onevent(event);
75
+ this.writeQueue.useEvent(event);
73
76
  }
74
77
  }
75
78
  if (sub.oneose)
package/dist/schema.d.ts CHANGED
@@ -3,11 +3,10 @@ import type { Event } from "nostr-tools";
3
3
  export type NostrIDB = IDBPDatabase<Schema>;
4
4
  export interface Schema extends DBSchema {
5
5
  events: {
6
- key: "id";
6
+ key: string;
7
7
  value: {
8
8
  event: Event;
9
9
  tags: string[];
10
- identifier?: string;
11
10
  };
12
11
  indexes: {
13
12
  id: string;
@@ -15,30 +14,16 @@ export interface Schema extends DBSchema {
15
14
  kind: number;
16
15
  created_at: number;
17
16
  tags: string;
18
- addressPointer: [number, string, string | undefined];
19
- };
20
- };
21
- seen: {
22
- key: "id";
23
- value: {
24
- id: string;
25
- date: number;
26
- relays: string[];
27
- };
28
- indexes: {
29
- id: string;
30
- date: string;
31
- relay: string;
32
17
  };
33
18
  };
34
19
  used: {
35
- key: "id";
20
+ key: string;
36
21
  value: {
37
- id: string;
22
+ uid: string;
38
23
  date: number;
39
24
  };
40
25
  indexes: {
41
- id: string;
26
+ uid: string;
42
27
  date: string;
43
28
  };
44
29
  };
@@ -1,11 +1,15 @@
1
- import type { Event } from "nostr-tools";
1
+ import { type Filter, type NostrEvent } from "nostr-tools";
2
2
  import { NostrIDB } from "./schema";
3
3
  export declare class WriteQueue {
4
4
  db: NostrIDB;
5
- queue: Event[];
5
+ private eventQueue;
6
+ private lastUsedQueue;
6
7
  constructor(db: NostrIDB);
7
- addEvent(event: Event): void;
8
- addEvents(events: Event[]): void;
9
- flush(count?: number): Promise<number>;
8
+ addEvent(event: NostrEvent): void;
9
+ addEvents(events: NostrEvent[]): void;
10
+ useEvent(event: NostrEvent): void;
11
+ useEvents(events: NostrEvent[]): void;
12
+ matchPending(filters: Filter[]): import("nostr-tools").Event[];
13
+ flush(count?: number): Promise<void>;
10
14
  clear(): void;
11
15
  }
@@ -1,29 +1,47 @@
1
- import { addEvents, updateUsed } from "./ingest";
1
+ import { matchFilters } from "nostr-tools";
2
+ import { addEvents, getEventUID, updateUsed } from "./ingest";
2
3
  export class WriteQueue {
3
4
  db;
4
- queue = [];
5
+ eventQueue = [];
6
+ lastUsedQueue = new Set();
5
7
  constructor(db) {
6
8
  this.db = db;
7
9
  }
8
10
  addEvent(event) {
9
- this.queue.push(event);
11
+ this.eventQueue.push(event);
10
12
  }
11
13
  addEvents(events) {
12
- this.queue.push(...events);
14
+ this.eventQueue.push(...events);
15
+ }
16
+ useEvent(event) {
17
+ this.lastUsedQueue.add(getEventUID(event));
18
+ }
19
+ useEvents(events) {
20
+ for (const event of events)
21
+ this.lastUsedQueue.add(getEventUID(event));
22
+ }
23
+ matchPending(filters) {
24
+ return this.eventQueue.filter((e) => matchFilters(filters, e));
13
25
  }
14
26
  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);
27
+ if (this.eventQueue.length > 0) {
28
+ const events = [];
29
+ for (let i = 0; i < count; i++) {
30
+ const event = this.eventQueue.shift();
31
+ if (!event)
32
+ break;
33
+ events.push(event);
34
+ }
35
+ await addEvents(this.db, events);
36
+ }
37
+ if (this.lastUsedQueue.size > 0) {
38
+ console.time("Save Used");
39
+ await updateUsed(this.db, this.lastUsedQueue);
40
+ console.timeEnd("Save Used");
41
+ this.lastUsedQueue.clear();
21
42
  }
22
- await addEvents(this.db, events);
23
- await updateUsed(this.db, events.map((e) => e.id));
24
- return events.length;
25
43
  }
26
44
  clear() {
27
- this.queue = [];
45
+ this.eventQueue = [];
28
46
  }
29
47
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nostr-idb",
3
- "version": "0.2.0",
3
+ "version": "1.0.0",
4
4
  "description": "A collection of helper methods for storing nostr events in IndexedDB",
5
5
  "author": {
6
6
  "name": "hzrd149"
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "idb": "^8.0.0",
27
- "nostr-tools": "^1.17.0"
27
+ "nostr-tools": "^2.1.3"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@changesets/cli": "^2.27.1",