tiny-idb-store 0.1.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 ADDED
@@ -0,0 +1,120 @@
1
+ # tiny-idb-store
2
+
3
+ IndexedDB object-store wrapper with promise-based CRUD, bulk, cursor, range, and update helpers.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install tiny-idb-store
9
+ ```
10
+
11
+ ## Scripts
12
+
13
+ - npm run test - run the Vitest suite with fake IndexedDB.
14
+ - npm run build - emit ESM JavaScript and TypeScript declarations to dist.
15
+ - npm run check - run tests and build.
16
+
17
+ ## Usage
18
+
19
+ ```ts
20
+ import { BaseStore } from 'tiny-idb-store'
21
+
22
+ interface Note {
23
+ id?: number
24
+ title: string
25
+ }
26
+
27
+ class NotesStore extends BaseStore {
28
+ constructor(db: IDBDatabase) {
29
+ super(db, 'notes')
30
+ }
31
+ }
32
+
33
+ class StorageService {
34
+ private static instance: StorageService | null = null
35
+
36
+ private db: IDBDatabase | null = null
37
+ private notesStore: NotesStore | null = null
38
+ private readonly name = 'notes-db'
39
+ private readonly version = 1
40
+
41
+ private constructor() {}
42
+
43
+ get notes(): NotesStore {
44
+ if (!this.notesStore) {
45
+ throw new Error('StorageService is not initialized.')
46
+ }
47
+
48
+ return this.notesStore
49
+ }
50
+
51
+ static async create(): Promise<StorageService> {
52
+ if (StorageService.instance) {
53
+ return StorageService.instance
54
+ }
55
+
56
+ const instance = new StorageService()
57
+ await instance.initialize()
58
+ StorageService.instance = instance
59
+
60
+ return instance
61
+ }
62
+
63
+ private async initialize(): Promise<void> {
64
+ this.db = await this.openConnection()
65
+ this.notesStore = new NotesStore(this.db)
66
+ }
67
+
68
+ private openConnection(): Promise<IDBDatabase> {
69
+ return new Promise<IDBDatabase>((resolve, reject) => {
70
+ const connection = indexedDB.open(this.name, this.version)
71
+
72
+ connection.onupgradeneeded = (event: IDBVersionChangeEvent) => {
73
+ const request = event.target as IDBOpenDBRequest
74
+ const db = request.result
75
+ const transaction = request.transaction
76
+
77
+ if (!transaction) {
78
+ reject(new Error('Missing upgrade transaction.'))
79
+ return
80
+ }
81
+
82
+ switch (event.oldVersion) {
83
+ case 0:
84
+ db.createObjectStore('notes', { keyPath: 'id', autoIncrement: true })
85
+ break
86
+ default:
87
+ break
88
+ }
89
+ }
90
+
91
+ connection.onsuccess = function () {
92
+ const db = connection.result
93
+ resolve(db)
94
+
95
+ db.onclose = function () {
96
+ alert('Database connection closed. Please reload the page.')
97
+ }
98
+ db.onversionchange = function () {
99
+ db.close()
100
+ alert('Database is outdated. Please reload the page.')
101
+ }
102
+ }
103
+
104
+ connection.onerror = function () {
105
+ reject(connection.error)
106
+ }
107
+ connection.onblocked = function () {
108
+ alert(
109
+ "Database is outdated. The newer version can't be loaded until you close other tabs. Please reload the page.",
110
+ )
111
+ }
112
+ })
113
+ }
114
+ }
115
+
116
+ const storage = await StorageService.create()
117
+ const noteId = await storage.notes.add<Note>({ title: 'My first note' })
118
+ const note = await storage.notes.get<Note>(noteId)
119
+ console.log(note)
120
+ ```
@@ -0,0 +1,37 @@
1
+ type TransactionOperation<T> = (store: IDBObjectStore) => Promise<T>;
2
+ export declare class BaseStore {
3
+ protected readonly db: IDBDatabase;
4
+ protected readonly storeName: string;
5
+ constructor(db: IDBDatabase, storeName: string);
6
+ protected createReadStore(): IDBObjectStore;
7
+ protected cursorValues<T>(): AsyncGenerator<T>;
8
+ protected createWriteTransaction(): IDBTransaction;
9
+ protected request<T>(request: IDBRequest<T>): Promise<T>;
10
+ protected write<T>(operation: TransactionOperation<T>): Promise<T>;
11
+ add<T>(record: T): Promise<IDBValidKey>;
12
+ bulkAdd<T>(records: T[]): Promise<IDBValidKey[]>;
13
+ put<T>(record: T): Promise<IDBValidKey>;
14
+ bulkPut<T>(records: T[]): Promise<IDBValidKey[]>;
15
+ get<T>(key: IDBValidKey): Promise<T | undefined>;
16
+ count(): Promise<number>;
17
+ getAll<T>(): Promise<T[]>;
18
+ getAllFromTo<T>(fromKey: IDBValidKey, toKey: IDBValidKey): Promise<T[]>;
19
+ getAllFrom<T>(fromKey: IDBValidKey): Promise<T[]>;
20
+ getAllKeysFrom(fromKey: IDBValidKey): Promise<IDBValidKey[]>;
21
+ getAllTo<T>(toKey: IDBValidKey): Promise<T[]>;
22
+ getAllKeysTo(toKey: IDBValidKey): Promise<IDBValidKey[]>;
23
+ mapAll<T, R>(mappers: Array<(value: T) => R>): AsyncGenerator<R>;
24
+ findAllBy<T>(predicate: (value: T) => boolean): AsyncGenerator<T>;
25
+ takeByLimit<T>(limit: number): AsyncGenerator<T>;
26
+ reduce<T, R>(predicate: (value: T) => boolean, mappers: Array<(value: T) => R>): AsyncGenerator<R>;
27
+ sliceBy<T>(start: number, stop: number, step?: number): AsyncGenerator<T>;
28
+ clear(): Promise<void>;
29
+ delete(key: IDBValidKey): Promise<void>;
30
+ bulkDelete(keys: IDBValidKey[]): Promise<unknown[]>;
31
+ update<T extends Record<string, unknown>>(key: IDBValidKey, patch: Partial<T>): Promise<boolean>;
32
+ updateByPath(key: IDBValidKey, path: string, value: unknown): Promise<boolean>;
33
+ updateAll<T extends Record<string, unknown>>(patch: Partial<T>): Promise<number>;
34
+ updateAllByPath(path: string, value: unknown): Promise<number>;
35
+ private updateAllRecords;
36
+ }
37
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,190 @@
1
+ import { setByPath } from "./utils";
2
+ export class BaseStore {
3
+ constructor(db, storeName) {
4
+ this.db = db;
5
+ this.storeName = storeName;
6
+ }
7
+ createReadStore() {
8
+ return this.db
9
+ .transaction(this.storeName, "readonly")
10
+ .objectStore(this.storeName);
11
+ }
12
+ async *cursorValues() {
13
+ const request = this.createReadStore().openCursor();
14
+ while (true) {
15
+ const cursor = await this.request(request);
16
+ if (!cursor) {
17
+ return;
18
+ }
19
+ yield cursor.value;
20
+ cursor.continue();
21
+ }
22
+ }
23
+ createWriteTransaction() {
24
+ return this.db.transaction(this.storeName, "readwrite");
25
+ }
26
+ request(request) {
27
+ return new Promise((resolve, reject) => {
28
+ request.onsuccess = () => resolve(request.result);
29
+ request.onerror = () => reject(request.error);
30
+ });
31
+ }
32
+ async write(operation) {
33
+ const transaction = this.createWriteTransaction();
34
+ const store = transaction.objectStore(this.storeName);
35
+ try {
36
+ const result = await operation(store);
37
+ await new Promise((resolve, reject) => {
38
+ transaction.oncomplete = () => resolve();
39
+ transaction.onerror = () => reject(transaction.error);
40
+ transaction.onabort = () => reject(transaction.error);
41
+ });
42
+ return result;
43
+ }
44
+ catch (error) {
45
+ if (transaction.error === null) {
46
+ transaction.abort();
47
+ }
48
+ throw error;
49
+ }
50
+ }
51
+ async add(record) {
52
+ return this.write((store) => this.request(store.add(record)));
53
+ }
54
+ async bulkAdd(records) {
55
+ return this.write((store) => Promise.all(records.map((record) => this.request(store.add(record)))));
56
+ }
57
+ async put(record) {
58
+ return this.write((store) => this.request(store.put(record)));
59
+ }
60
+ async bulkPut(records) {
61
+ return this.write((store) => Promise.all(records.map((record) => this.request(store.put(record)))));
62
+ }
63
+ async get(key) {
64
+ return this.request(this.createReadStore().get(key));
65
+ }
66
+ async count() {
67
+ return this.request(this.createReadStore().count());
68
+ }
69
+ async getAll() {
70
+ return this.request(this.createReadStore().getAll());
71
+ }
72
+ async getAllFromTo(fromKey, toKey) {
73
+ return this.request(this.createReadStore().getAll(IDBKeyRange.bound(fromKey, toKey)));
74
+ }
75
+ async getAllFrom(fromKey) {
76
+ return this.request(this.createReadStore().getAll(IDBKeyRange.lowerBound(fromKey, true)));
77
+ }
78
+ async getAllKeysFrom(fromKey) {
79
+ return this.request(this.createReadStore().getAllKeys(IDBKeyRange.lowerBound(fromKey, true)));
80
+ }
81
+ async getAllTo(toKey) {
82
+ return this.request(this.createReadStore().getAll(IDBKeyRange.upperBound(toKey, true)));
83
+ }
84
+ async getAllKeysTo(toKey) {
85
+ return this.request(this.createReadStore().getAllKeys(IDBKeyRange.upperBound(toKey, true)));
86
+ }
87
+ async *mapAll(mappers) {
88
+ for await (const value of this.cursorValues()) {
89
+ for (const mapper of mappers) {
90
+ yield mapper(value);
91
+ }
92
+ }
93
+ }
94
+ async *findAllBy(predicate) {
95
+ for await (const value of this.cursorValues()) {
96
+ if (predicate(value)) {
97
+ yield value;
98
+ }
99
+ }
100
+ }
101
+ async *takeByLimit(limit) {
102
+ let count = 0;
103
+ for await (const value of this.cursorValues()) {
104
+ if (count >= limit) {
105
+ return;
106
+ }
107
+ yield value;
108
+ count += 1;
109
+ }
110
+ }
111
+ async *reduce(predicate, mappers) {
112
+ for await (const value of this.cursorValues()) {
113
+ if (predicate(value)) {
114
+ for (const mapper of mappers) {
115
+ yield mapper(value);
116
+ }
117
+ }
118
+ }
119
+ }
120
+ async *sliceBy(start, stop, step = 1) {
121
+ let index = 0;
122
+ for await (const value of this.cursorValues()) {
123
+ if (index >= stop) {
124
+ return;
125
+ }
126
+ if (index >= start && (index - start) % step === 0) {
127
+ yield value;
128
+ }
129
+ index += 1;
130
+ }
131
+ }
132
+ async clear() {
133
+ await this.write((store) => this.request(store.clear()));
134
+ }
135
+ async delete(key) {
136
+ await this.write((store) => this.request(store.delete(key)));
137
+ }
138
+ async bulkDelete(keys) {
139
+ return this.write((store) => Promise.all(keys.map((key) => this.request(store.delete(key)))));
140
+ }
141
+ async update(key, patch) {
142
+ const current = await this.get(key);
143
+ if (!current) {
144
+ return false;
145
+ }
146
+ await this.put({ ...current, ...patch });
147
+ return true;
148
+ }
149
+ async updateByPath(key, path, value) {
150
+ const current = await this.get(key);
151
+ if (!current) {
152
+ return false;
153
+ }
154
+ setByPath(current, path, value);
155
+ await this.put(current);
156
+ return true;
157
+ }
158
+ async updateAll(patch) {
159
+ return this.updateAllRecords((record) => {
160
+ Object.assign(record, patch);
161
+ });
162
+ }
163
+ async updateAllByPath(path, value) {
164
+ return this.updateAllRecords((record) => {
165
+ setByPath(record, path, value);
166
+ });
167
+ }
168
+ async updateAllRecords(updateRecord) {
169
+ return this.write((store) => new Promise((resolve, reject) => {
170
+ const request = store.openCursor();
171
+ let updatedCount = 0;
172
+ request.onsuccess = () => {
173
+ const cursor = request.result;
174
+ if (!cursor) {
175
+ resolve(updatedCount);
176
+ return;
177
+ }
178
+ const record = cursor.value;
179
+ updateRecord(record);
180
+ const updateRequest = cursor.update(record);
181
+ updateRequest.onsuccess = () => {
182
+ updatedCount += 1;
183
+ cursor.continue();
184
+ };
185
+ updateRequest.onerror = () => reject(updateRequest.error);
186
+ };
187
+ request.onerror = () => reject(request.error);
188
+ }));
189
+ }
190
+ }
@@ -0,0 +1 @@
1
+ export declare const setByPath: (record: Record<string, unknown>, path: string, value: unknown) => void;
package/dist/utils.js ADDED
@@ -0,0 +1,14 @@
1
+ export const setByPath = (record, path, value) => {
2
+ let current = record;
3
+ const parts = path.split(".");
4
+ for (const [index, part] of parts.entries()) {
5
+ if (index === parts.length - 1) {
6
+ current[part] = value;
7
+ return;
8
+ }
9
+ if (typeof current[part] !== "object" || current[part] === null) {
10
+ current[part] = {};
11
+ }
12
+ current = current[part];
13
+ }
14
+ };
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "tiny-idb-store",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js"
10
+ }
11
+ },
12
+ "scripts": {
13
+ "build": "tsc -p tsconfig.json",
14
+ "test": "vitest run",
15
+ "check": "vitest run && tsc -p tsconfig.json"
16
+ },
17
+ "devDependencies": {
18
+ "fake-indexeddb": "^6.0.0",
19
+ "typescript": "~5.7.2",
20
+ "vitest": "^3.2.0"
21
+ },
22
+ "description": "Typed IndexedDB object-store wrapper with promise-based CRUD, bulk, cursor, range, and update helpers.",
23
+ "license": "MIT",
24
+ "files": [
25
+ "dist"
26
+ ]
27
+ }