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 +8 -0
- package/README.md +87 -5
- package/dist/database.d.ts +2 -1
- package/dist/database.js +7 -1
- package/dist/index-cache.d.ts +18 -0
- package/dist/index-cache.js +63 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/ingest.d.ts +9 -1
- package/dist/ingest.js +21 -4
- package/dist/query-filter.d.ts +10 -9
- package/dist/query-filter.js +89 -33
- package/dist/query-misc.d.ts +3 -0
- package/dist/query-misc.js +18 -0
- package/dist/relay.d.ts +51 -0
- package/dist/relay.js +92 -0
- package/dist/schema.d.ts +3 -1
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +3 -0
- package/dist/write-queue.d.ts +11 -0
- package/dist/write-queue.js +29 -0
- package/package.json +9 -2
package/CHANGELOG.md
ADDED
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
|
-
|
|
5
|
+
## Features
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
55
|
+
### deleteDB
|
|
18
56
|
|
|
19
57
|
Calls `deleteDB` from idb under the hood
|
|
20
58
|
|
|
21
|
-
|
|
59
|
+
### getEventsForFilter / getEventsForFilters
|
|
22
60
|
|
|
23
|
-
|
|
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
|
+
```
|
package/dist/database.d.ts
CHANGED
|
@@ -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("
|
|
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
package/dist/index.js
CHANGED
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
|
-
|
|
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
|
-
|
|
4
|
-
|
|
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 ||
|
|
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();
|
package/dist/query-filter.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { Event, Filter } from "nostr-tools";
|
|
2
2
|
import type { NostrIDB } from "./schema.js";
|
|
3
|
-
|
|
4
|
-
export declare function
|
|
5
|
-
export declare function
|
|
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>;
|
package/dist/query-filter.js
CHANGED
|
@@ -1,45 +1,101 @@
|
|
|
1
1
|
import { GENERIC_TAGS } from "./common.js";
|
|
2
|
-
|
|
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
|
|
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
|
|
12
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
34
|
-
const
|
|
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
|
|
40
|
-
|
|
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
|
|
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", "
|
|
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(
|
|
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
|
}
|
package/dist/query-misc.d.ts
CHANGED
|
@@ -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>;
|
package/dist/query-misc.js
CHANGED
|
@@ -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")
|
package/dist/relay.d.ts
ADDED
|
@@ -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
|
-
|
|
16
|
+
created_at: number;
|
|
16
17
|
tags: string;
|
|
18
|
+
addressPointer: [number, string, string | undefined];
|
|
17
19
|
};
|
|
18
20
|
};
|
|
19
21
|
seen: {
|
package/dist/utils.d.ts
ADDED
package/dist/utils.js
ADDED
|
@@ -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.
|
|
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
|
-
"@
|
|
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
|
}
|