@firtoz/drizzle-indexeddb 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.
@@ -0,0 +1,36 @@
1
+ import { useContext } from "react";
2
+ import {
3
+ DrizzleIndexedDBContext,
4
+ useIndexedDBCollection,
5
+ type DrizzleIndexedDBContextValue,
6
+ } from "./DrizzleIndexedDBProvider";
7
+
8
+ export type UseDrizzleIndexedDBContextReturn<
9
+ TSchema extends Record<string, unknown>,
10
+ > = {
11
+ useCollection: <TTableName extends keyof TSchema & string>(
12
+ tableName: TTableName,
13
+ ) => ReturnType<typeof useIndexedDBCollection<TSchema, TTableName>>;
14
+ indexedDB: IDBDatabase | null;
15
+ };
16
+
17
+ export function useDrizzleIndexedDB<
18
+ TSchema extends Record<string, unknown>,
19
+ >(): UseDrizzleIndexedDBContextReturn<TSchema> {
20
+ const context = useContext(
21
+ DrizzleIndexedDBContext,
22
+ ) as DrizzleIndexedDBContextValue<TSchema> | null;
23
+
24
+ if (!context) {
25
+ throw new Error(
26
+ "useDrizzleIndexedDBContext must be used within a DrizzleIndexedDBProvider",
27
+ );
28
+ }
29
+
30
+ return {
31
+ useCollection: <TTableName extends keyof TSchema & string>(
32
+ tableName: TTableName,
33
+ ) => useIndexedDBCollection(context, tableName),
34
+ indexedDB: context.indexedDB,
35
+ };
36
+ }
@@ -0,0 +1,234 @@
1
+ // IndexedDB migrator that executes generated migration functions
2
+
3
+ export type IndexedDBMigrationFunction = (
4
+ db: IDBDatabase,
5
+ transaction: IDBTransaction,
6
+ ) => Promise<void>;
7
+
8
+ interface MigrationRecord {
9
+ id: number;
10
+ appliedAt: number;
11
+ }
12
+
13
+ const MIGRATIONS_STORE = "__drizzle_migrations";
14
+
15
+ /**
16
+ * Runs IndexedDB migrations using generated migration functions
17
+ *
18
+ * Example usage:
19
+ * ```typescript
20
+ * import { migrations } from './drizzle/indexeddb-migrations';
21
+ * import { migrateIndexedDBWithFunctions } from '@firtoz/drizzle-indexeddb';
22
+ *
23
+ * const db = await migrateIndexedDBWithFunctions('my-db', migrations, true);
24
+ * ```
25
+ */
26
+ export async function migrateIndexedDBWithFunctions(
27
+ dbName: string,
28
+ migrations: IndexedDBMigrationFunction[],
29
+ debug: boolean = false,
30
+ ): Promise<IDBDatabase> {
31
+ if (debug) {
32
+ console.log(
33
+ `[${new Date().toISOString()}] [PERF] IndexedDB migrator start for ${dbName}`,
34
+ );
35
+ }
36
+
37
+ // First, open the database to check which migrations have been applied
38
+ const currentDb = await openDatabaseForMigrationCheck(dbName);
39
+
40
+ const appliedMigrations = await getAppliedMigrations(currentDb);
41
+
42
+ const latestAppliedIdx =
43
+ appliedMigrations.length > 0
44
+ ? Math.max(...appliedMigrations.map((m) => m.id))
45
+ : -1;
46
+
47
+ if (debug) {
48
+ console.log(
49
+ `[${new Date().toISOString()}] [PERF] Latest applied migration index: ${latestAppliedIdx} (checked ${appliedMigrations.length} migrations)`,
50
+ );
51
+ }
52
+
53
+ // Determine which migrations need to be applied
54
+ const pendingMigrations = migrations
55
+ .map((fn, idx) => ({ fn, idx }))
56
+ .filter(({ idx }) => idx > latestAppliedIdx);
57
+
58
+ if (pendingMigrations.length === 0) {
59
+ if (debug) {
60
+ console.log(
61
+ `[${new Date().toISOString()}] [PERF] No pending migrations - database is up to date`,
62
+ );
63
+ }
64
+ currentDb.close();
65
+ // Re-open with correct version (migrations.length + 1 because version starts at 1)
66
+ const db = await openDatabase(dbName, migrations.length + 1);
67
+ if (debug) {
68
+ console.log(
69
+ `[${new Date().toISOString()}] [PERF] Migrator complete (no migrations needed)`,
70
+ );
71
+ }
72
+ return db;
73
+ }
74
+
75
+ if (debug) {
76
+ console.log(
77
+ `[${new Date().toISOString()}] [PERF] Found ${pendingMigrations.length} pending migrations to apply`,
78
+ );
79
+ }
80
+
81
+ currentDb.close();
82
+
83
+ // Calculate the target version (number of total migrations)
84
+ const targetVersion = migrations.length;
85
+
86
+ // Open database with version upgrade to trigger migration
87
+ const db = await new Promise<IDBDatabase>((resolve, reject) => {
88
+ // Use +1 here because first version is 1...
89
+ const request = indexedDB.open(dbName, targetVersion + 1);
90
+
91
+ request.onerror = () => reject(request.error);
92
+ request.onsuccess = () => {
93
+ resolve(request.result);
94
+ };
95
+
96
+ request.onupgradeneeded = async (event) => {
97
+ const db = (event.target as IDBOpenDBRequest).result;
98
+ const transaction = (event.target as IDBOpenDBRequest).transaction;
99
+
100
+ if (!transaction) {
101
+ reject(new Error("Transaction not available during upgrade"));
102
+ return;
103
+ }
104
+
105
+ if (debug) {
106
+ console.log(
107
+ `[${new Date().toISOString()}] [PERF] Upgrade started: v${event.oldVersion} → v${event.newVersion}`,
108
+ );
109
+ }
110
+
111
+ try {
112
+ // Ensure migrations store exists
113
+ if (!db.objectStoreNames.contains(MIGRATIONS_STORE)) {
114
+ const migrationStore = db.createObjectStore(MIGRATIONS_STORE, {
115
+ keyPath: "id",
116
+ autoIncrement: false,
117
+ });
118
+ migrationStore.createIndex("appliedAt", "appliedAt", {
119
+ unique: false,
120
+ });
121
+ if (debug) {
122
+ console.log(
123
+ `[${new Date().toISOString()}] [PERF] Created migrations tracking store`,
124
+ );
125
+ }
126
+ }
127
+
128
+ // Apply each pending migration
129
+ for (const { fn, idx } of pendingMigrations) {
130
+ if (debug) {
131
+ console.log(
132
+ `[${new Date().toISOString()}] [PERF] Applying migration ${idx}...`,
133
+ );
134
+ }
135
+
136
+ // Execute the migration function
137
+ await fn(db, transaction);
138
+
139
+ // Record the migration
140
+ const migrationStore = transaction.objectStore(MIGRATIONS_STORE);
141
+ migrationStore.add({
142
+ id: idx,
143
+ appliedAt: Date.now(),
144
+ });
145
+
146
+ if (debug) {
147
+ console.log(
148
+ `[${new Date().toISOString()}] [PERF] Migration ${idx} complete`,
149
+ );
150
+ }
151
+ }
152
+
153
+ if (debug) {
154
+ console.log(
155
+ `[${new Date().toISOString()}] [PERF] All ${pendingMigrations.length} migrations applied successfully`,
156
+ );
157
+ }
158
+ } catch (error) {
159
+ console.error("[IndexedDBMigrator] Migration failed:", error);
160
+ transaction.abort();
161
+ reject(error);
162
+ }
163
+ };
164
+ });
165
+
166
+ if (debug) {
167
+ console.log(
168
+ `[${new Date().toISOString()}] [PERF] Migrator complete - database ready`,
169
+ );
170
+ }
171
+
172
+ return db;
173
+ }
174
+
175
+ /**
176
+ * Opens database for checking migrations (without triggering upgrade)
177
+ */
178
+ async function openDatabaseForMigrationCheck(
179
+ dbName: string,
180
+ ): Promise<IDBDatabase> {
181
+ return new Promise((resolve, reject) => {
182
+ const request = indexedDB.open(dbName);
183
+ request.onerror = () => {
184
+ reject(request.error);
185
+ };
186
+ request.onsuccess = () => {
187
+ resolve(request.result);
188
+ };
189
+ });
190
+ }
191
+
192
+ /**
193
+ * Opens database with a specific version
194
+ */
195
+ async function openDatabase(
196
+ dbName: string,
197
+ version: number,
198
+ ): Promise<IDBDatabase> {
199
+ return new Promise((resolve, reject) => {
200
+ const request = indexedDB.open(dbName, version);
201
+ request.onerror = () => {
202
+ reject(request.error);
203
+ };
204
+ request.onsuccess = () => {
205
+ resolve(request.result);
206
+ };
207
+ });
208
+ }
209
+
210
+ /**
211
+ * Gets the list of applied migrations from the database
212
+ */
213
+ async function getAppliedMigrations(
214
+ db: IDBDatabase,
215
+ ): Promise<MigrationRecord[]> {
216
+ if (!db.objectStoreNames.contains(MIGRATIONS_STORE)) {
217
+ return [];
218
+ }
219
+
220
+ return new Promise((resolve, reject) => {
221
+ const transaction = db.transaction(MIGRATIONS_STORE, "readonly");
222
+
223
+ const store = transaction.objectStore(MIGRATIONS_STORE);
224
+
225
+ const request = store.getAll();
226
+
227
+ request.onerror = () => {
228
+ reject(request.error);
229
+ };
230
+ request.onsuccess = () => {
231
+ resolve(request.result);
232
+ };
233
+ });
234
+ }
package/src/index.ts ADDED
@@ -0,0 +1,30 @@
1
+ export {
2
+ migrateIndexedDB,
3
+ type IndexedDBMigrationConfig,
4
+ } from "./snapshot-migrator";
5
+
6
+ export {
7
+ migrateIndexedDBWithFunctions,
8
+ type IndexedDBMigrationFunction,
9
+ } from "./function-migrator";
10
+
11
+ export { deleteIndexedDB } from "./utils";
12
+
13
+ export {
14
+ indexedDBCollectionOptions,
15
+ type IndexedDBCollectionConfig,
16
+ type IndexedDBSyncItem,
17
+ } from "./collections/indexeddb-collection";
18
+
19
+ // IndexedDB Provider
20
+ export {
21
+ DrizzleIndexedDBProvider,
22
+ DrizzleIndexedDBContext,
23
+ useIndexedDBCollection,
24
+ type DrizzleIndexedDBContextValue,
25
+ } from "./context/DrizzleIndexedDBProvider";
26
+
27
+ export {
28
+ useDrizzleIndexedDB,
29
+ type UseDrizzleIndexedDBContextReturn,
30
+ } from "./context/useDrizzleIndexedDB";
@@ -0,0 +1,420 @@
1
+ // IndexedDB migrator that uses Drizzle snapshot files to create object stores and indexes
2
+
3
+ import type { Journal, Snapshot } from "@firtoz/drizzle-utils";
4
+
5
+ // ============================================================================
6
+ // Migration Config Types
7
+ // ============================================================================
8
+
9
+ export interface IndexedDBMigrationConfig {
10
+ journal: Journal;
11
+ snapshots: Record<string, Snapshot>;
12
+ }
13
+
14
+ interface MigrationRecord {
15
+ id: number;
16
+ tag: string;
17
+ when: number;
18
+ appliedAt: number;
19
+ }
20
+
21
+ const MIGRATIONS_STORE = "__drizzle_migrations";
22
+
23
+ /**
24
+ * Opens an IndexedDB database and runs migrations if needed
25
+ */
26
+ export async function migrateIndexedDB(
27
+ dbName: string,
28
+ config: IndexedDBMigrationConfig,
29
+ debug: boolean = false,
30
+ ): Promise<IDBDatabase> {
31
+ if (debug) {
32
+ console.log(
33
+ `[${new Date().toISOString()}] [PERF] IndexedDB snapshot migrator start for ${dbName}`,
34
+ );
35
+ }
36
+
37
+ // First, open the database to check which migrations have been applied
38
+
39
+ const currentDb = await openDatabaseForMigrationCheck(dbName);
40
+
41
+ const appliedMigrations = await getAppliedMigrations(currentDb);
42
+
43
+ const latestAppliedIdx =
44
+ appliedMigrations.length > 0
45
+ ? Math.max(...appliedMigrations.map((m) => m.id))
46
+ : -1;
47
+
48
+ if (debug) {
49
+ console.log(
50
+ `[${new Date().toISOString()}] [PERF] Latest applied migration index: ${latestAppliedIdx} (checked ${appliedMigrations.length} migrations)`,
51
+ );
52
+ }
53
+
54
+ // Determine which migrations need to be applied
55
+ const pendingMigrations = config.journal.entries.filter(
56
+ (entry) => entry.idx > latestAppliedIdx,
57
+ );
58
+
59
+ if (pendingMigrations.length === 0) {
60
+ if (debug) {
61
+ console.log(
62
+ `[${new Date().toISOString()}] [PERF] No pending migrations - database is up to date`,
63
+ );
64
+ }
65
+ currentDb.close();
66
+ // Re-open with correct version
67
+
68
+ const db = await openDatabase(dbName, config.journal.entries.length);
69
+
70
+ if (debug) {
71
+ console.log(
72
+ `[${new Date().toISOString()}] [PERF] Migrator complete (no migrations needed)`,
73
+ );
74
+ }
75
+ return db;
76
+ }
77
+
78
+ if (debug) {
79
+ console.log(
80
+ `[${new Date().toISOString()}] [PERF] Found ${pendingMigrations.length} pending migrations to apply:`,
81
+ pendingMigrations.map((m) => m.tag),
82
+ );
83
+ }
84
+
85
+ currentDb.close();
86
+
87
+ // Calculate the target version (number of total migrations)
88
+ const targetVersion = config.journal.entries.length;
89
+
90
+ // Open database with version upgrade to trigger migration
91
+
92
+ const db = await new Promise<IDBDatabase>((resolve, reject) => {
93
+ const request = indexedDB.open(dbName, targetVersion);
94
+
95
+ request.onerror = () => reject(request.error);
96
+ request.onsuccess = () => {
97
+ resolve(request.result);
98
+ };
99
+
100
+ request.onupgradeneeded = (event) => {
101
+ const db = (event.target as IDBOpenDBRequest).result;
102
+ const transaction = (event.target as IDBOpenDBRequest).transaction;
103
+
104
+ if (!transaction) {
105
+ reject(new Error("Transaction not available during upgrade"));
106
+ return;
107
+ }
108
+
109
+ if (debug) {
110
+ console.log(
111
+ `[${new Date().toISOString()}] [PERF] Upgrade started: v${event.oldVersion} → v${event.newVersion}`,
112
+ );
113
+ }
114
+
115
+ try {
116
+ // Ensure migrations store exists
117
+
118
+ if (!db.objectStoreNames.contains(MIGRATIONS_STORE)) {
119
+ const migrationStore = db.createObjectStore(MIGRATIONS_STORE, {
120
+ keyPath: "id",
121
+ autoIncrement: false,
122
+ });
123
+ migrationStore.createIndex("tag", "tag", { unique: true });
124
+ migrationStore.createIndex("when", "when", { unique: false });
125
+ if (debug) {
126
+ console.log(
127
+ `[${new Date().toISOString()}] [PERF] Created migrations tracking store`,
128
+ );
129
+ }
130
+ }
131
+
132
+ // Apply each pending migration
133
+ for (const journalEntry of pendingMigrations) {
134
+ const snapshotKey = `m${journalEntry.idx.toString().padStart(4, "0")}`;
135
+ const snapshot = config.snapshots[snapshotKey];
136
+
137
+ if (!snapshot) {
138
+ throw new Error(`Missing snapshot: ${snapshotKey}`);
139
+ }
140
+
141
+ // Get previous snapshot for comparison (if this isn't the first migration)
142
+ const previousSnapshot =
143
+ journalEntry.idx > 0
144
+ ? config.snapshots[
145
+ `m${(journalEntry.idx - 1).toString().padStart(4, "0")}`
146
+ ]
147
+ : null;
148
+
149
+ if (debug) {
150
+ console.log(
151
+ `[${new Date().toISOString()}] [PERF] Applying migration ${journalEntry.idx}: ${journalEntry.tag}`,
152
+ );
153
+ }
154
+
155
+ applySnapshot(
156
+ db,
157
+ snapshot,
158
+ previousSnapshot,
159
+ transaction,
160
+ debug,
161
+ dbName,
162
+ journalEntry.idx,
163
+ );
164
+
165
+ // Record the migration
166
+ const migrationStore = transaction.objectStore(MIGRATIONS_STORE);
167
+ migrationStore.add({
168
+ id: journalEntry.idx,
169
+ tag: journalEntry.tag,
170
+ when: journalEntry.when,
171
+ appliedAt: Date.now(),
172
+ });
173
+
174
+ if (debug) {
175
+ console.log(
176
+ `[${new Date().toISOString()}] [PERF] Migration ${journalEntry.idx} complete`,
177
+ );
178
+ }
179
+ }
180
+
181
+ if (debug) {
182
+ console.log(
183
+ `[${new Date().toISOString()}] [PERF] All ${pendingMigrations.length} migrations applied successfully`,
184
+ );
185
+ }
186
+ } catch (error) {
187
+ console.error("[IndexedDBMigrator] Migration failed:", error);
188
+ transaction.abort();
189
+ reject(error);
190
+ }
191
+ };
192
+ });
193
+
194
+ if (debug) {
195
+ console.log(
196
+ `[${new Date().toISOString()}] [PERF] Migrator complete - database ready`,
197
+ );
198
+ }
199
+
200
+ return db;
201
+ }
202
+
203
+ /**
204
+ * Opens database for checking migrations (without triggering upgrade)
205
+ */
206
+ async function openDatabaseForMigrationCheck(
207
+ dbName: string,
208
+ ): Promise<IDBDatabase> {
209
+ return new Promise((resolve, reject) => {
210
+ const request = indexedDB.open(dbName);
211
+ request.onerror = () => reject(request.error);
212
+ request.onsuccess = () => resolve(request.result);
213
+ });
214
+ }
215
+
216
+ /**
217
+ * Opens database with a specific version
218
+ */
219
+ async function openDatabase(
220
+ dbName: string,
221
+ version: number,
222
+ ): Promise<IDBDatabase> {
223
+ return new Promise((resolve, reject) => {
224
+ const request = indexedDB.open(dbName, version);
225
+ request.onerror = () => reject(request.error);
226
+ request.onsuccess = () => resolve(request.result);
227
+ });
228
+ }
229
+
230
+ /**
231
+ * Gets the list of applied migrations from the database
232
+ */
233
+ async function getAppliedMigrations(
234
+ db: IDBDatabase,
235
+ ): Promise<MigrationRecord[]> {
236
+ if (!db.objectStoreNames.contains(MIGRATIONS_STORE)) {
237
+ return [];
238
+ }
239
+
240
+ return new Promise((resolve, reject) => {
241
+ const transaction = db.transaction(MIGRATIONS_STORE, "readonly");
242
+ const store = transaction.objectStore(MIGRATIONS_STORE);
243
+ const request = store.getAll();
244
+
245
+ request.onerror = () => reject(request.error);
246
+ request.onsuccess = () => resolve(request.result);
247
+ });
248
+ }
249
+
250
+ /**
251
+ * Applies a snapshot to the database by creating/updating object stores and indexes
252
+ */
253
+ function applySnapshot(
254
+ db: IDBDatabase,
255
+ snapshot: Snapshot,
256
+ previousSnapshot: Snapshot | null,
257
+ transaction: IDBTransaction,
258
+ debug: boolean,
259
+ _dbName: string,
260
+ _migrationIdx: number,
261
+ ): void {
262
+ // Process each table in the snapshot
263
+ for (const [tableName, tableDefinition] of Object.entries(snapshot.tables)) {
264
+ const storeExists = db.objectStoreNames.contains(tableName);
265
+ let objectStore: IDBObjectStore;
266
+
267
+ if (!storeExists) {
268
+ // Create new object store
269
+
270
+ if (debug) {
271
+ console.log(
272
+ `[${new Date().toISOString()}] [PERF] Creating object store: ${tableName}`,
273
+ );
274
+ }
275
+
276
+ // Find the primary key column (optional in IndexedDB)
277
+ const primaryKeyColumn = Object.entries(tableDefinition.columns).find(
278
+ ([_, col]) => col.primaryKey,
279
+ );
280
+
281
+ if (primaryKeyColumn) {
282
+ // In-line key: Use the primary key column as keyPath
283
+ const keyPath = primaryKeyColumn[0];
284
+ const autoIncrement = primaryKeyColumn[1].autoincrement;
285
+
286
+ objectStore = db.createObjectStore(tableName, {
287
+ keyPath,
288
+ autoIncrement,
289
+ });
290
+ } else {
291
+ // Out-of-line key: No keyPath, can use auto-increment or external keys
292
+ if (debug) {
293
+ console.log(
294
+ `[${new Date().toISOString()}] [PERF] Creating object store ${tableName} with out-of-line keys (no keyPath)`,
295
+ );
296
+ }
297
+ objectStore = db.createObjectStore(tableName, {
298
+ autoIncrement: true, // Auto-generate keys by default
299
+ });
300
+ }
301
+ } else {
302
+ // Get existing object store
303
+ objectStore = transaction.objectStore(tableName);
304
+
305
+ // Check if the primary key or autoIncrement has changed
306
+ const primaryKeyColumn = Object.entries(tableDefinition.columns).find(
307
+ ([_, col]) => col.primaryKey,
308
+ );
309
+
310
+ // Determine expected key configuration
311
+ const expectedKeyPath = primaryKeyColumn ? primaryKeyColumn[0] : null;
312
+ const expectedAutoIncrement = primaryKeyColumn
313
+ ? primaryKeyColumn[1].autoincrement
314
+ : true; // Default for out-of-line keys
315
+
316
+ // Normalize keyPath for comparison (null, undefined, or empty string are all "no keyPath")
317
+ const currentKeyPath = objectStore.keyPath || null;
318
+ const normalizedExpectedKeyPath = expectedKeyPath || null;
319
+
320
+ // Warn if key structure changed - this requires manual migration
321
+ if (
322
+ currentKeyPath !== normalizedExpectedKeyPath ||
323
+ objectStore.autoIncrement !== expectedAutoIncrement
324
+ ) {
325
+ const message =
326
+ `[IndexedDBMigrator] Primary key structure changed for ${tableName}. ` +
327
+ `This requires manual migration. ` +
328
+ `Old keyPath: ${currentKeyPath || "(none)"}, new keyPath: ${normalizedExpectedKeyPath || "(none)"}. ` +
329
+ `Old autoIncrement: ${objectStore.autoIncrement}, new autoIncrement: ${expectedAutoIncrement}. ` +
330
+ `Consider exporting data, deleting the database, and re-importing.`;
331
+
332
+ console.error(message);
333
+ throw new Error(message);
334
+ }
335
+ }
336
+
337
+ const currentIndexes = tableDefinition.indexes;
338
+
339
+ // Remove indexes that no longer exist in the schema
340
+ const existingIndexNames = Array.from(objectStore.indexNames);
341
+ for (const existingIndexName of existingIndexNames) {
342
+ if (!currentIndexes[existingIndexName]) {
343
+ if (debug) {
344
+ console.log(
345
+ `[${new Date().toISOString()}] [PERF] Deleting index: ${existingIndexName} from ${tableName}`,
346
+ );
347
+ }
348
+ objectStore.deleteIndex(existingIndexName);
349
+ }
350
+ }
351
+
352
+ // Add or update indexes
353
+ for (const [indexName, indexDef] of Object.entries(currentIndexes)) {
354
+ const indexExists = objectStore.indexNames.contains(indexName);
355
+
356
+ // IndexedDB indexes can only be on a single column or use an array keyPath
357
+ const keyPath =
358
+ indexDef.columns.length === 1 ? indexDef.columns[0] : indexDef.columns;
359
+
360
+ if (indexExists) {
361
+ // Check if index definition has changed
362
+ const existingIndex = objectStore.index(indexName);
363
+ const keyPathChanged = Array.isArray(keyPath)
364
+ ? JSON.stringify(existingIndex.keyPath) !== JSON.stringify(keyPath)
365
+ : existingIndex.keyPath !== keyPath;
366
+ const uniqueChanged = existingIndex.unique !== indexDef.isUnique;
367
+
368
+ if (keyPathChanged || uniqueChanged) {
369
+ if (debug) {
370
+ console.log(
371
+ `[${new Date().toISOString()}] [PERF] Recreating index: ${indexName} on ${tableName}`,
372
+ );
373
+ }
374
+ // Recreate the index
375
+ objectStore.deleteIndex(indexName);
376
+ objectStore.createIndex(indexName, keyPath, {
377
+ unique: indexDef.isUnique,
378
+ });
379
+ }
380
+ } else {
381
+ // Create new index
382
+
383
+ if (debug) {
384
+ console.log(
385
+ `[${new Date().toISOString()}] [PERF] Creating index: ${indexName} on ${tableName}`,
386
+ );
387
+ }
388
+
389
+ try {
390
+ objectStore.createIndex(indexName, keyPath, {
391
+ unique: indexDef.isUnique,
392
+ });
393
+ } catch (error) {
394
+ console.error(
395
+ `[${new Date().toISOString()}] [IndexedDBMigrator] failed to create index ${indexName}:`,
396
+ error,
397
+ );
398
+ throw error;
399
+ }
400
+ }
401
+ }
402
+ }
403
+
404
+ // Handle deleted tables
405
+ if (previousSnapshot) {
406
+ for (const tableName of Object.keys(previousSnapshot.tables)) {
407
+ if (
408
+ !snapshot.tables[tableName] &&
409
+ db.objectStoreNames.contains(tableName)
410
+ ) {
411
+ if (debug) {
412
+ console.log(
413
+ `[${new Date().toISOString()}] [PERF] Deleting object store: ${tableName}`,
414
+ );
415
+ }
416
+ db.deleteObjectStore(tableName);
417
+ }
418
+ }
419
+ }
420
+ }