@firtoz/drizzle-indexeddb 0.2.0 → 0.4.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 +70 -26
- package/README.md +274 -51
- package/package.json +14 -6
- package/src/bin/generate-migrations.ts +288 -0
- package/src/collections/indexeddb-collection.ts +97 -397
- package/src/context/useDrizzleIndexedDB.ts +2 -1
- package/src/function-migrator.ts +191 -170
- package/src/idb-interceptor.ts +75 -0
- package/src/idb-operations.ts +41 -0
- package/src/idb-types.ts +135 -0
- package/src/index.ts +57 -9
- package/src/instrumented-idb-database.ts +188 -0
- package/src/native-idb-database.ts +355 -0
- package/src/proxy/idb-proxy-client.ts +345 -0
- package/src/proxy/idb-proxy-server.ts +313 -0
- package/src/proxy/idb-proxy-transport.ts +174 -0
- package/src/proxy/idb-proxy-types.ts +77 -0
- package/src/proxy/idb-sync-adapter.ts +95 -0
- package/src/proxy/index.ts +37 -0
- package/src/snapshot-migrator.ts +0 -420
- package/src/utils.ts +0 -15
package/src/index.ts
CHANGED
|
@@ -1,21 +1,42 @@
|
|
|
1
|
-
export {
|
|
2
|
-
migrateIndexedDB,
|
|
3
|
-
type IndexedDBMigrationConfig,
|
|
4
|
-
} from "./snapshot-migrator";
|
|
5
|
-
|
|
6
1
|
export {
|
|
7
2
|
migrateIndexedDBWithFunctions,
|
|
8
|
-
type
|
|
3
|
+
type Migration,
|
|
4
|
+
type MigrationOperation,
|
|
5
|
+
type CreateTableOperation,
|
|
6
|
+
type DeleteTableOperation,
|
|
7
|
+
type CreateIndexOperation,
|
|
8
|
+
type DeleteIndexOperation,
|
|
9
9
|
} from "./function-migrator";
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
// IDB Types
|
|
12
|
+
export type {
|
|
13
|
+
IDBCreator,
|
|
14
|
+
IDBOpenOptions,
|
|
15
|
+
IDBDatabaseLike,
|
|
16
|
+
IDBDeleter,
|
|
17
|
+
IndexInfo,
|
|
18
|
+
CreateStoreOptions,
|
|
19
|
+
CreateIndexOptions,
|
|
20
|
+
KeyRangeSpec,
|
|
21
|
+
} from "./idb-types";
|
|
22
|
+
|
|
23
|
+
// IDB Interceptor (for testing/debugging)
|
|
24
|
+
export type { IDBInterceptor, IDBOperation } from "./idb-interceptor";
|
|
25
|
+
|
|
26
|
+
// IDB Operations
|
|
27
|
+
export { openIndexedDb, deleteIndexedDB } from "./idb-operations";
|
|
12
28
|
|
|
29
|
+
// Native IDB Implementation
|
|
30
|
+
export { defaultIDBCreator } from "./native-idb-database";
|
|
31
|
+
|
|
32
|
+
// Instrumented IDB (for testing)
|
|
33
|
+
export { createInstrumentedDbCreator } from "./instrumented-idb-database";
|
|
34
|
+
|
|
35
|
+
// Collection
|
|
13
36
|
export {
|
|
14
37
|
indexedDBCollectionOptions,
|
|
15
38
|
type IndexedDBCollectionConfig,
|
|
16
39
|
type IndexedDBSyncItem,
|
|
17
|
-
type IDBInterceptor,
|
|
18
|
-
type IDBOperation,
|
|
19
40
|
} from "./collections/indexeddb-collection";
|
|
20
41
|
|
|
21
42
|
// IndexedDB Provider
|
|
@@ -30,3 +51,30 @@ export {
|
|
|
30
51
|
useDrizzleIndexedDB,
|
|
31
52
|
type UseDrizzleIndexedDBContextReturn,
|
|
32
53
|
} from "./context/useDrizzleIndexedDB";
|
|
54
|
+
|
|
55
|
+
// IDB Proxy (for Chrome extension, messaging-based IDB access)
|
|
56
|
+
export {
|
|
57
|
+
// Types
|
|
58
|
+
type IDBProxyRequest,
|
|
59
|
+
type IDBProxyRequestBody,
|
|
60
|
+
type IDBProxyResponse,
|
|
61
|
+
type IDBProxySyncMessage,
|
|
62
|
+
generateRequestId,
|
|
63
|
+
generateClientId,
|
|
64
|
+
// Transport
|
|
65
|
+
type IDBProxyClientTransport,
|
|
66
|
+
type IDBProxyServerTransport,
|
|
67
|
+
createInMemoryTransport,
|
|
68
|
+
createMultiClientTransport,
|
|
69
|
+
// Client
|
|
70
|
+
IDBProxyClient,
|
|
71
|
+
createProxyDbCreator,
|
|
72
|
+
type SyncHandler,
|
|
73
|
+
// Server
|
|
74
|
+
IDBProxyServer,
|
|
75
|
+
createProxyServer,
|
|
76
|
+
type IDBProxyServerOptions,
|
|
77
|
+
// Sync adapter (connects proxy sync to collection)
|
|
78
|
+
createCollectionSyncHandler,
|
|
79
|
+
combineSyncHandlers,
|
|
80
|
+
} from "./proxy";
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IDBDatabaseLike,
|
|
3
|
+
IDBCreator,
|
|
4
|
+
IDBOpenOptions,
|
|
5
|
+
IndexInfo,
|
|
6
|
+
CreateStoreOptions,
|
|
7
|
+
CreateIndexOptions,
|
|
8
|
+
KeyRangeSpec,
|
|
9
|
+
} from "./idb-types";
|
|
10
|
+
import type { IDBInterceptor } from "./idb-interceptor";
|
|
11
|
+
import { defaultIDBCreator } from "./native-idb-database";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A database wrapper that intercepts operations and reports them to an interceptor.
|
|
15
|
+
* Useful for testing to verify what IndexedDB operations are actually performed.
|
|
16
|
+
*/
|
|
17
|
+
class InstrumentedIDBDatabase implements IDBDatabaseLike {
|
|
18
|
+
constructor(
|
|
19
|
+
private db: IDBDatabaseLike,
|
|
20
|
+
private interceptor: IDBInterceptor,
|
|
21
|
+
) {}
|
|
22
|
+
|
|
23
|
+
get version(): number {
|
|
24
|
+
return this.db.version;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Schema operations (pass through without interception)
|
|
28
|
+
hasStore(storeName: string): boolean {
|
|
29
|
+
return this.db.hasStore(storeName);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getStoreNames(): string[] {
|
|
33
|
+
return this.db.getStoreNames();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
createStore(storeName: string, options?: CreateStoreOptions): void {
|
|
37
|
+
this.db.createStore(storeName, options);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
deleteStore(storeName: string): void {
|
|
41
|
+
this.db.deleteStore(storeName);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
createIndex(
|
|
45
|
+
storeName: string,
|
|
46
|
+
indexName: string,
|
|
47
|
+
keyPath: string | string[],
|
|
48
|
+
options?: CreateIndexOptions,
|
|
49
|
+
): void {
|
|
50
|
+
this.db.createIndex(storeName, indexName, keyPath, options);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
deleteIndex(storeName: string, indexName: string): void {
|
|
54
|
+
this.db.deleteIndex(storeName, indexName);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getStoreIndexes(storeName: string): IndexInfo[] {
|
|
58
|
+
return this.db.getStoreIndexes(storeName);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Data operations (intercepted)
|
|
62
|
+
async getAll<T = unknown>(storeName: string): Promise<T[]> {
|
|
63
|
+
const items = await this.db.getAll<T>(storeName);
|
|
64
|
+
|
|
65
|
+
this.interceptor.onOperation?.({
|
|
66
|
+
type: "getAll",
|
|
67
|
+
storeName,
|
|
68
|
+
itemsReturned: items,
|
|
69
|
+
itemCount: items.length,
|
|
70
|
+
timestamp: Date.now(),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return items;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async getAllByIndex<T = unknown>(
|
|
77
|
+
storeName: string,
|
|
78
|
+
indexName: string,
|
|
79
|
+
keyRange?: KeyRangeSpec,
|
|
80
|
+
): Promise<T[]> {
|
|
81
|
+
const items = await this.db.getAllByIndex<T>(
|
|
82
|
+
storeName,
|
|
83
|
+
indexName,
|
|
84
|
+
keyRange,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
this.interceptor.onOperation?.({
|
|
88
|
+
type: "index-getAll",
|
|
89
|
+
storeName,
|
|
90
|
+
indexName,
|
|
91
|
+
keyRange: keyRange as unknown as IDBKeyRange | undefined,
|
|
92
|
+
itemsReturned: items,
|
|
93
|
+
itemCount: items.length,
|
|
94
|
+
timestamp: Date.now(),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return items;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async get<T = unknown>(
|
|
101
|
+
storeName: string,
|
|
102
|
+
key: IDBValidKey,
|
|
103
|
+
): Promise<T | undefined> {
|
|
104
|
+
const item = await this.db.get<T>(storeName, key);
|
|
105
|
+
|
|
106
|
+
this.interceptor.onOperation?.({
|
|
107
|
+
type: "get",
|
|
108
|
+
storeName,
|
|
109
|
+
key,
|
|
110
|
+
itemReturned: item,
|
|
111
|
+
timestamp: Date.now(),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return item;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async add(storeName: string, items: unknown[]): Promise<void> {
|
|
118
|
+
await this.db.add(storeName, items);
|
|
119
|
+
|
|
120
|
+
this.interceptor.onOperation?.({
|
|
121
|
+
type: "add",
|
|
122
|
+
storeName,
|
|
123
|
+
items,
|
|
124
|
+
itemCount: items.length,
|
|
125
|
+
timestamp: Date.now(),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async put(storeName: string, items: unknown[]): Promise<void> {
|
|
130
|
+
await this.db.put(storeName, items);
|
|
131
|
+
|
|
132
|
+
this.interceptor.onOperation?.({
|
|
133
|
+
type: "put",
|
|
134
|
+
storeName,
|
|
135
|
+
items,
|
|
136
|
+
itemCount: items.length,
|
|
137
|
+
timestamp: Date.now(),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async delete(storeName: string, keys: IDBValidKey[]): Promise<void> {
|
|
142
|
+
await this.db.delete(storeName, keys);
|
|
143
|
+
|
|
144
|
+
this.interceptor.onOperation?.({
|
|
145
|
+
type: "delete",
|
|
146
|
+
storeName,
|
|
147
|
+
keys,
|
|
148
|
+
keyCount: keys.length,
|
|
149
|
+
timestamp: Date.now(),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async clear(storeName: string): Promise<void> {
|
|
154
|
+
await this.db.clear(storeName);
|
|
155
|
+
|
|
156
|
+
this.interceptor.onOperation?.({
|
|
157
|
+
type: "clear",
|
|
158
|
+
storeName,
|
|
159
|
+
timestamp: Date.now(),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
close(): void {
|
|
164
|
+
this.db.close();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Creates an instrumented database creator that wraps operations with interception.
|
|
170
|
+
* Use this for testing to verify what IndexedDB operations are performed.
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* const interceptor = { onOperation: (op) => console.log(op) };
|
|
174
|
+
* const dbCreator = createInstrumentedDbCreator(interceptor);
|
|
175
|
+
*
|
|
176
|
+
* <DrizzleIndexedDBProvider dbCreator={dbCreator} ... />
|
|
177
|
+
*/
|
|
178
|
+
export function createInstrumentedDbCreator(
|
|
179
|
+
interceptor: IDBInterceptor,
|
|
180
|
+
baseCreator?: IDBCreator,
|
|
181
|
+
): IDBCreator {
|
|
182
|
+
const creator = baseCreator ?? defaultIDBCreator;
|
|
183
|
+
|
|
184
|
+
return async (name: string, options?: IDBOpenOptions) => {
|
|
185
|
+
const db = await creator(name, options);
|
|
186
|
+
return new InstrumentedIDBDatabase(db, interceptor);
|
|
187
|
+
};
|
|
188
|
+
}
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IDBDatabaseLike,
|
|
3
|
+
IDBCreator,
|
|
4
|
+
IDBOpenOptions,
|
|
5
|
+
IndexInfo,
|
|
6
|
+
CreateStoreOptions,
|
|
7
|
+
CreateIndexOptions,
|
|
8
|
+
KeyRangeSpec,
|
|
9
|
+
} from "./idb-types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates a KeyRange from a KeyRangeSpec
|
|
13
|
+
*/
|
|
14
|
+
function createKeyRange(spec: KeyRangeSpec): IDBKeyRange {
|
|
15
|
+
switch (spec.type) {
|
|
16
|
+
case "only":
|
|
17
|
+
return IDBKeyRange.only(spec.value);
|
|
18
|
+
case "lowerBound":
|
|
19
|
+
return IDBKeyRange.lowerBound(spec.lower, spec.lowerOpen);
|
|
20
|
+
case "upperBound":
|
|
21
|
+
return IDBKeyRange.upperBound(spec.upper, spec.upperOpen);
|
|
22
|
+
case "bound":
|
|
23
|
+
return IDBKeyRange.bound(
|
|
24
|
+
spec.lower,
|
|
25
|
+
spec.upper,
|
|
26
|
+
spec.lowerOpen,
|
|
27
|
+
spec.upperOpen,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Default implementation that wraps native IndexedDB
|
|
34
|
+
*/
|
|
35
|
+
class NativeIDBDatabase implements IDBDatabaseLike {
|
|
36
|
+
constructor(private db: IDBDatabase) {
|
|
37
|
+
// Listen for version change events - close connection when another tab/process
|
|
38
|
+
// wants to upgrade the database. This prevents blocking issues.
|
|
39
|
+
this.db.onversionchange = () => {
|
|
40
|
+
this.db.close();
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get version(): number {
|
|
45
|
+
return this.db.version;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
hasStore(storeName: string): boolean {
|
|
49
|
+
return this.db.objectStoreNames.contains(storeName);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
getStoreNames(): string[] {
|
|
53
|
+
return Array.from(this.db.objectStoreNames);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
createStore(storeName: string, options?: CreateStoreOptions): void {
|
|
57
|
+
this.db.createObjectStore(storeName, options);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
deleteStore(storeName: string): void {
|
|
61
|
+
this.db.deleteObjectStore(storeName);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
createIndex(
|
|
65
|
+
storeName: string,
|
|
66
|
+
indexName: string,
|
|
67
|
+
keyPath: string | string[],
|
|
68
|
+
options?: CreateIndexOptions,
|
|
69
|
+
): void {
|
|
70
|
+
const transaction = this.db.transaction(storeName, "readonly");
|
|
71
|
+
const store = transaction.objectStore(storeName);
|
|
72
|
+
store.createIndex(indexName, keyPath, options);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
deleteIndex(storeName: string, indexName: string): void {
|
|
76
|
+
const transaction = this.db.transaction(storeName, "readonly");
|
|
77
|
+
const store = transaction.objectStore(storeName);
|
|
78
|
+
store.deleteIndex(indexName);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
getStoreIndexes(storeName: string): IndexInfo[] {
|
|
82
|
+
if (!this.hasStore(storeName)) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const transaction = this.db.transaction(storeName, "readonly");
|
|
87
|
+
const store = transaction.objectStore(storeName);
|
|
88
|
+
const indexes: IndexInfo[] = [];
|
|
89
|
+
|
|
90
|
+
for (const indexName of Array.from(store.indexNames)) {
|
|
91
|
+
const index = store.index(indexName);
|
|
92
|
+
indexes.push({
|
|
93
|
+
name: indexName,
|
|
94
|
+
keyPath: index.keyPath,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return indexes;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async getAll<T = unknown>(storeName: string): Promise<T[]> {
|
|
102
|
+
if (!this.hasStore(storeName)) {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return new Promise((resolve, reject) => {
|
|
107
|
+
const transaction = this.db.transaction(storeName, "readonly");
|
|
108
|
+
const store = transaction.objectStore(storeName);
|
|
109
|
+
const request = store.getAll();
|
|
110
|
+
|
|
111
|
+
request.onsuccess = () => resolve(request.result as T[]);
|
|
112
|
+
request.onerror = () => reject(request.error);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async getAllByIndex<T = unknown>(
|
|
117
|
+
storeName: string,
|
|
118
|
+
indexName: string,
|
|
119
|
+
keyRange?: KeyRangeSpec,
|
|
120
|
+
): Promise<T[]> {
|
|
121
|
+
return new Promise((resolve, reject) => {
|
|
122
|
+
const transaction = this.db.transaction(storeName, "readonly");
|
|
123
|
+
const store = transaction.objectStore(storeName);
|
|
124
|
+
const index = store.index(indexName);
|
|
125
|
+
const range = keyRange ? createKeyRange(keyRange) : undefined;
|
|
126
|
+
const request = index.getAll(range);
|
|
127
|
+
|
|
128
|
+
request.onsuccess = () => resolve(request.result as T[]);
|
|
129
|
+
request.onerror = () => reject(request.error);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async get<T = unknown>(
|
|
134
|
+
storeName: string,
|
|
135
|
+
key: IDBValidKey,
|
|
136
|
+
): Promise<T | undefined> {
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
const transaction = this.db.transaction(storeName, "readonly");
|
|
139
|
+
const store = transaction.objectStore(storeName);
|
|
140
|
+
const request = store.get(key);
|
|
141
|
+
|
|
142
|
+
request.onsuccess = () => resolve(request.result as T | undefined);
|
|
143
|
+
request.onerror = () => reject(request.error);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async add(storeName: string, items: unknown[]): Promise<void> {
|
|
148
|
+
return new Promise((resolve, reject) => {
|
|
149
|
+
const transaction = this.db.transaction(storeName, "readwrite");
|
|
150
|
+
const store = transaction.objectStore(storeName);
|
|
151
|
+
|
|
152
|
+
for (const item of items) {
|
|
153
|
+
store.add(item);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
transaction.oncomplete = () => resolve();
|
|
157
|
+
transaction.onerror = () => reject(transaction.error);
|
|
158
|
+
transaction.onabort = () => reject(new Error("Transaction aborted"));
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async put(storeName: string, items: unknown[]): Promise<void> {
|
|
163
|
+
return new Promise((resolve, reject) => {
|
|
164
|
+
const transaction = this.db.transaction(storeName, "readwrite");
|
|
165
|
+
const store = transaction.objectStore(storeName);
|
|
166
|
+
|
|
167
|
+
for (const item of items) {
|
|
168
|
+
store.put(item);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
transaction.oncomplete = () => resolve();
|
|
172
|
+
transaction.onerror = () => reject(transaction.error);
|
|
173
|
+
transaction.onabort = () => reject(new Error("Transaction aborted"));
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async delete(storeName: string, keys: IDBValidKey[]): Promise<void> {
|
|
178
|
+
return new Promise((resolve, reject) => {
|
|
179
|
+
const transaction = this.db.transaction(storeName, "readwrite");
|
|
180
|
+
const store = transaction.objectStore(storeName);
|
|
181
|
+
|
|
182
|
+
for (const key of keys) {
|
|
183
|
+
store.delete(key);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
transaction.oncomplete = () => resolve();
|
|
187
|
+
transaction.onerror = () => reject(transaction.error);
|
|
188
|
+
transaction.onabort = () => reject(new Error("Transaction aborted"));
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async clear(storeName: string): Promise<void> {
|
|
193
|
+
return new Promise((resolve, reject) => {
|
|
194
|
+
const transaction = this.db.transaction(storeName, "readwrite");
|
|
195
|
+
const store = transaction.objectStore(storeName);
|
|
196
|
+
const request = store.clear();
|
|
197
|
+
|
|
198
|
+
request.onsuccess = () => resolve();
|
|
199
|
+
request.onerror = () => reject(request.error);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
close(): void {
|
|
204
|
+
this.db.close();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Upgrade-mode database wrapper used during version changes.
|
|
210
|
+
* Provides IDBDatabaseLike interface with schema modification capabilities.
|
|
211
|
+
*/
|
|
212
|
+
class UpgradeModeDatabase implements IDBDatabaseLike {
|
|
213
|
+
private createdStores: Map<string, IDBObjectStore> = new Map();
|
|
214
|
+
|
|
215
|
+
constructor(
|
|
216
|
+
private db: IDBDatabase,
|
|
217
|
+
private transaction: IDBTransaction,
|
|
218
|
+
) {}
|
|
219
|
+
|
|
220
|
+
get version(): number {
|
|
221
|
+
return this.db.version;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
hasStore(storeName: string): boolean {
|
|
225
|
+
return this.db.objectStoreNames.contains(storeName);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
getStoreNames(): string[] {
|
|
229
|
+
return Array.from(this.db.objectStoreNames);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
createStore(storeName: string, options?: CreateStoreOptions): void {
|
|
233
|
+
const store = this.db.createObjectStore(storeName, options);
|
|
234
|
+
this.createdStores.set(storeName, store);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
deleteStore(storeName: string): void {
|
|
238
|
+
this.db.deleteObjectStore(storeName);
|
|
239
|
+
this.createdStores.delete(storeName);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
createIndex(
|
|
243
|
+
storeName: string,
|
|
244
|
+
indexName: string,
|
|
245
|
+
keyPath: string | string[],
|
|
246
|
+
options?: CreateIndexOptions,
|
|
247
|
+
): void {
|
|
248
|
+
let store = this.createdStores.get(storeName);
|
|
249
|
+
if (!store) {
|
|
250
|
+
try {
|
|
251
|
+
store = this.transaction.objectStore(storeName);
|
|
252
|
+
} catch {
|
|
253
|
+
throw new Error(`Cannot create index - store "${storeName}" not found`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
store.createIndex(indexName, keyPath, options);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
deleteIndex(storeName: string, indexName: string): void {
|
|
260
|
+
let store = this.createdStores.get(storeName);
|
|
261
|
+
if (!store) {
|
|
262
|
+
try {
|
|
263
|
+
store = this.transaction.objectStore(storeName);
|
|
264
|
+
} catch {
|
|
265
|
+
throw new Error(`Cannot delete index - store "${storeName}" not found`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
store.deleteIndex(indexName);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
getStoreIndexes(storeName: string): IndexInfo[] {
|
|
272
|
+
if (!this.hasStore(storeName)) return [];
|
|
273
|
+
let store = this.createdStores.get(storeName);
|
|
274
|
+
if (!store) {
|
|
275
|
+
try {
|
|
276
|
+
store = this.transaction.objectStore(storeName);
|
|
277
|
+
} catch {
|
|
278
|
+
return [];
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return Array.from(store.indexNames).map((name) => ({
|
|
282
|
+
name,
|
|
283
|
+
keyPath: store.index(name).keyPath,
|
|
284
|
+
}));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Data operations not available during upgrade
|
|
288
|
+
async getAll<T = unknown>(): Promise<T[]> {
|
|
289
|
+
throw new Error("getAll not available during upgrade");
|
|
290
|
+
}
|
|
291
|
+
async getAllByIndex<T = unknown>(): Promise<T[]> {
|
|
292
|
+
throw new Error("getAllByIndex not available during upgrade");
|
|
293
|
+
}
|
|
294
|
+
async get<T = unknown>(): Promise<T | undefined> {
|
|
295
|
+
throw new Error("get not available during upgrade");
|
|
296
|
+
}
|
|
297
|
+
async add(): Promise<void> {
|
|
298
|
+
throw new Error("add not available during upgrade");
|
|
299
|
+
}
|
|
300
|
+
async put(): Promise<void> {
|
|
301
|
+
throw new Error("put not available during upgrade");
|
|
302
|
+
}
|
|
303
|
+
async delete(): Promise<void> {
|
|
304
|
+
throw new Error("delete not available during upgrade");
|
|
305
|
+
}
|
|
306
|
+
async clear(): Promise<void> {
|
|
307
|
+
throw new Error("clear not available during upgrade");
|
|
308
|
+
}
|
|
309
|
+
close(): void {
|
|
310
|
+
this.db.close();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Default IDB creator that uses the native IndexedDB API.
|
|
316
|
+
*/
|
|
317
|
+
export const defaultIDBCreator: IDBCreator = (
|
|
318
|
+
name: string,
|
|
319
|
+
options?: IDBOpenOptions,
|
|
320
|
+
): Promise<IDBDatabaseLike> => {
|
|
321
|
+
return new Promise((resolve, reject) => {
|
|
322
|
+
const request = options?.version
|
|
323
|
+
? indexedDB.open(name, options.version)
|
|
324
|
+
: indexedDB.open(name);
|
|
325
|
+
|
|
326
|
+
request.onerror = () => reject(request.error);
|
|
327
|
+
|
|
328
|
+
request.onblocked = () => {
|
|
329
|
+
setTimeout(() => {
|
|
330
|
+
reject(new Error("Database upgrade blocked - close other tabs"));
|
|
331
|
+
}, 3000);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
request.onupgradeneeded = (event) => {
|
|
335
|
+
if (options?.onUpgrade) {
|
|
336
|
+
const db = request.result;
|
|
337
|
+
const transaction = (event.target as IDBOpenDBRequest).transaction;
|
|
338
|
+
if (!transaction) {
|
|
339
|
+
reject(new Error("No transaction during upgrade"));
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
// Create an upgrade-mode database wrapper
|
|
343
|
+
const upgradeDb = new UpgradeModeDatabase(db, transaction);
|
|
344
|
+
try {
|
|
345
|
+
options.onUpgrade(upgradeDb);
|
|
346
|
+
} catch (error) {
|
|
347
|
+
transaction.abort();
|
|
348
|
+
reject(error);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
request.onsuccess = () => resolve(new NativeIDBDatabase(request.result));
|
|
354
|
+
});
|
|
355
|
+
};
|