Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 4 additions & 60 deletions hub-client/src/services/projectStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,70 +4,16 @@
* This module provides CRUD operations for project entries and integrates
* with the schema versioning/migration system.
*/
import { openDB } from 'idb';
import type { IDBPDatabase } from 'idb';
import type { ProjectEntry } from '../types/project';
import type { ExportData, UserSettings } from './storage/types';
import {
DB_NAME,
STORES,
CURRENT_DB_VERSION,
CURRENT_SCHEMA_VERSION,
getStructuralMigrations,
runMigrations,
getDb,
resetDbPromise,
getSchemaVersion,
} from './storage';

/**
* Cached database promise.
* Reset to null if database needs to be reopened.
*/
let dbPromise: Promise<IDBPDatabase> | null = null;

/**
* Get or open the database, running migrations as needed.
*
* This function:
* 1. Opens the database with the current version
* 2. Runs structural migrations (store/index creation) in the upgrade callback
* 3. Runs data transformation migrations after the database is open
*/
async function getDb(): Promise<IDBPDatabase> {
if (!dbPromise) {
dbPromise = (async () => {
// Open the database with structural migrations in the upgrade callback
const db = await openDB(DB_NAME, CURRENT_DB_VERSION, {
upgrade(db, oldVersion, _newVersion, transaction) {
// Create projects store if this is a fresh database
if (!db.objectStoreNames.contains(STORES.PROJECTS)) {
const store = db.createObjectStore(STORES.PROJECTS, { keyPath: 'id' });
store.createIndex('indexDocId', 'indexDocId', { unique: true });
store.createIndex('lastAccessed', 'lastAccessed');
}

// Run structural migrations for version upgrades
// oldVersion is 0 for new databases, so we start from 1
const fromVersion = oldVersion || 1;
const structuralMigrations = getStructuralMigrations(fromVersion);

for (const migration of structuralMigrations) {
if (migration.structural) {
console.log(`Running structural migration v${migration.version}: ${migration.description}`);
migration.structural(db, transaction);
}
}
},
});

// Run data transformation migrations after the database is open
await runMigrations(db);

return db;
})();
}
return dbPromise;
}

/**
* Generate a unique ID for a new project entry.
*/
Expand Down Expand Up @@ -279,8 +225,6 @@ export async function getDatabaseSchemaVersion(): Promise<number> {
* Call this when you need to force a reconnection (e.g., after schema changes).
*/
export function closeDatabase(): void {
if (dbPromise) {
dbPromise.then(db => db.close()).catch(() => {});
dbPromise = null;
}
getDb().then(db => db.close()).catch(() => {});
resetDbPromise();
}
71 changes: 71 additions & 0 deletions hub-client/src/services/storage/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Shared database initialization.
*
* Both projectStorage and userSettings import getDb from here,
* ensuring the upgrade callback always runs when the database is first created.
*/

import { openDB } from 'idb';
import type { IDBPDatabase } from 'idb';
import { DB_NAME, STORES } from './types';
import { CURRENT_DB_VERSION, getStructuralMigrations } from './migrations';
import { runMigrations } from './migrationRunner';

/**
* Cached database promise.
* Reset to null if database needs to be reopened.
*/
let dbPromise: Promise<IDBPDatabase> | null = null;

/**
* Get or open the database, running migrations as needed.
*
* This function:
* 1. Opens the database with the current version
* 2. Runs structural migrations (store/index creation) in the upgrade callback
* 3. Runs data transformation migrations after the database is open
*
* The database instance is cached — subsequent calls return the same promise.
*/
export async function getDb(): Promise<IDBPDatabase> {
if (!dbPromise) {
dbPromise = (async () => {
const db = await openDB(DB_NAME, CURRENT_DB_VERSION, {
upgrade(db, oldVersion, _newVersion, transaction) {
// Create projects store if this is a fresh database
if (!db.objectStoreNames.contains(STORES.PROJECTS)) {
const store = db.createObjectStore(STORES.PROJECTS, { keyPath: 'id' });
store.createIndex('indexDocId', 'indexDocId', { unique: true });
store.createIndex('lastAccessed', 'lastAccessed');
}

// Run structural migrations for version upgrades
// oldVersion is 0 for new databases, so we start from 1
const fromVersion = oldVersion || 1;
const structuralMigrations = getStructuralMigrations(fromVersion);

for (const migration of structuralMigrations) {
if (migration.structural) {
console.log(`Running structural migration v${migration.version}: ${migration.description}`);
migration.structural(db, transaction);
}
}
},
});

// Run data transformation migrations after the database is open
await runMigrations(db);

return db;
})();
}
return dbPromise;
}

/**
* Reset the cached database promise.
* Call this if the database connection is lost or needs to be reopened.
*/
export function resetDbPromise(): void {
dbPromise = null;
}
3 changes: 3 additions & 0 deletions hub-client/src/services/storage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ export {
MigrationFailedError,
} from './migrationRunner';

// Database initialization
export { getDb, resetDbPromise } from './db';

// Utilities
export {
generateColorFromId,
Expand Down
20 changes: 1 addition & 19 deletions hub-client/src/services/userSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,16 @@
* User identity is used for presence features (cursor colors, display names).
*/

import { openDB } from 'idb';
import type { IDBPDatabase } from 'idb';
import type { UserSettings } from './storage/types';
import {
DB_NAME,
STORES,
CURRENT_DB_VERSION,
getDb,
generateColorFromId,
generateAnonymousName,
isValidHexColor,
isValidUserName,
} from './storage';

/**
* Get the database instance.
* Note: This opens the DB independently to avoid circular dependencies with projectStorage.
* The DB version and migration system ensures consistency.
*/
async function getDb(): Promise<IDBPDatabase> {
return openDB(DB_NAME, CURRENT_DB_VERSION);
}

/**
* Get the current user identity.
*
Expand All @@ -35,12 +23,6 @@ async function getDb(): Promise<IDBPDatabase> {
*/
export async function getUserIdentity(): Promise<UserSettings> {
const db = await getDb();

// Check if store exists
if (!db.objectStoreNames.contains(STORES.USER_SETTINGS)) {
throw new Error('User settings store not found. Database may not be fully initialized.');
}

const settings = await db.get(STORES.USER_SETTINGS, 'identity');

if (settings) {
Expand Down
Loading
Loading