You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
218 lines
5.8 KiB
218 lines
5.8 KiB
/** |
|
* Settings store using IndexedDB for persistent client-side storage |
|
* Stores: auto-save, user.name, user.email, theme, messagingPreferences |
|
*/ |
|
|
|
// Lazy import logger to avoid initialization order issues |
|
import type { Logger } from '../types/logger.js'; |
|
|
|
let loggerCache: Logger | null = null; |
|
const getLogger = async (): Promise<Logger> => { |
|
if (!loggerCache) { |
|
const loggerModule = await import('./logger.js'); |
|
loggerCache = loggerModule.default; |
|
} |
|
return loggerCache; |
|
}; |
|
|
|
const DB_NAME = 'gitrepublic_settings'; |
|
const DB_VERSION = 1; |
|
const STORE_SETTINGS = 'settings'; |
|
|
|
import type { MessagingPreferences } from './messaging/preferences-types.js'; |
|
|
|
interface Settings { |
|
autoSave: boolean; |
|
userName: string; |
|
userEmail: string; |
|
theme: 'gitrepublic-light' | 'gitrepublic-dark' | 'gitrepublic-black'; |
|
defaultBranch: string; |
|
messagingPreferences?: MessagingPreferences; |
|
} |
|
|
|
const DEFAULT_SETTINGS: Settings = { |
|
autoSave: false, |
|
userName: '', |
|
userEmail: '', |
|
theme: 'gitrepublic-dark', |
|
defaultBranch: 'master', |
|
messagingPreferences: undefined |
|
}; |
|
|
|
export class SettingsStore { |
|
private db: IDBDatabase | null = null; |
|
private initPromise: Promise<void> | null = null; |
|
private settingsCache: Settings | null = null; |
|
|
|
constructor() { |
|
this.init(); |
|
} |
|
|
|
/** |
|
* Initialize IndexedDB |
|
*/ |
|
private async init(): Promise<void> { |
|
if (this.initPromise) { |
|
return this.initPromise; |
|
} |
|
|
|
if (typeof window === 'undefined' || !window.indexedDB) { |
|
const logger = await getLogger(); |
|
logger.debug('IndexedDB not available, using in-memory cache only'); |
|
return; |
|
} |
|
|
|
this.initPromise = (async () => { |
|
const logger = await getLogger(); |
|
return new Promise<void>((resolve, reject) => { |
|
const request = indexedDB.open(DB_NAME, DB_VERSION); |
|
|
|
request.onerror = () => { |
|
logger.error('Failed to open settings IndexedDB'); |
|
reject(new Error('Failed to open settings IndexedDB')); |
|
}; |
|
|
|
request.onsuccess = () => { |
|
this.db = request.result; |
|
resolve(); |
|
}; |
|
|
|
request.onupgradeneeded = (event) => { |
|
const db = (event.target as IDBOpenDBRequest).result; |
|
|
|
// Settings store - stores all settings as a single object |
|
if (!db.objectStoreNames.contains(STORE_SETTINGS)) { |
|
db.createObjectStore(STORE_SETTINGS, { keyPath: 'id' }); |
|
} |
|
}; |
|
}); |
|
})(); |
|
|
|
return this.initPromise; |
|
} |
|
|
|
/** |
|
* Get all settings |
|
*/ |
|
async getSettings(): Promise<Settings> { |
|
await this.init(); |
|
|
|
// Return cached settings if available |
|
if (this.settingsCache) { |
|
return this.settingsCache; |
|
} |
|
|
|
if (!this.db) { |
|
return { ...DEFAULT_SETTINGS }; |
|
} |
|
|
|
try { |
|
const store = this.db.transaction([STORE_SETTINGS], 'readonly').objectStore(STORE_SETTINGS); |
|
const request = store.get('main'); |
|
|
|
const result = await new Promise<Settings>((resolve, reject) => { |
|
request.onsuccess = () => { |
|
const data = request.result; |
|
if (data && data.settings) { |
|
// Merge with defaults to ensure all fields exist |
|
const merged = { ...DEFAULT_SETTINGS, ...data.settings }; |
|
resolve(merged); |
|
} else { |
|
resolve({ ...DEFAULT_SETTINGS }); |
|
} |
|
}; |
|
request.onerror = () => reject(request.error); |
|
}); |
|
|
|
// Cache the result |
|
this.settingsCache = result; |
|
return result; |
|
} catch (error) { |
|
const logger = await getLogger(); |
|
logger.error({ error }, 'Error reading settings from IndexedDB'); |
|
return { ...DEFAULT_SETTINGS }; |
|
} |
|
} |
|
|
|
/** |
|
* Update settings (partial update) |
|
*/ |
|
async updateSettings(updates: Partial<Settings>): Promise<void> { |
|
await this.init(); |
|
|
|
const logger = await getLogger(); |
|
|
|
if (!this.db) { |
|
logger.debug('IndexedDB not available, cannot save settings'); |
|
return; |
|
} |
|
|
|
try { |
|
// Get current settings |
|
const current = await this.getSettings(); |
|
|
|
// Merge with updates |
|
const updated = { ...current, ...updates }; |
|
|
|
// Save to IndexedDB |
|
const store = this.db.transaction([STORE_SETTINGS], 'readwrite').objectStore(STORE_SETTINGS); |
|
await new Promise<void>((resolve, reject) => { |
|
const request = store.put({ id: 'main', settings: updated }); |
|
request.onsuccess = () => resolve(); |
|
request.onerror = () => reject(request.error); |
|
}); |
|
|
|
// Update cache |
|
this.settingsCache = updated; |
|
|
|
logger.debug({ updates }, 'Settings updated'); |
|
} catch (error) { |
|
logger.error({ error, updates }, 'Error updating settings'); |
|
throw error; |
|
} |
|
} |
|
|
|
/** |
|
* Get a specific setting |
|
*/ |
|
async getSetting<K extends keyof Settings>(key: K): Promise<Settings[K]> { |
|
const settings = await this.getSettings(); |
|
return settings[key]; |
|
} |
|
|
|
/** |
|
* Set a specific setting |
|
*/ |
|
async setSetting<K extends keyof Settings>(key: K, value: Settings[K]): Promise<void> { |
|
await this.updateSettings({ [key]: value } as Partial<Settings>); |
|
} |
|
|
|
/** |
|
* Clear all settings (reset to defaults) |
|
*/ |
|
async clear(): Promise<void> { |
|
await this.init(); |
|
|
|
if (!this.db) { |
|
return; |
|
} |
|
|
|
try { |
|
const store = this.db.transaction([STORE_SETTINGS], 'readwrite').objectStore(STORE_SETTINGS); |
|
await new Promise<void>((resolve, reject) => { |
|
const request = store.delete('main'); |
|
request.onsuccess = () => resolve(); |
|
request.onerror = () => reject(request.error); |
|
}); |
|
|
|
// Clear cache |
|
this.settingsCache = null; |
|
} catch (error) { |
|
const logger = await getLogger(); |
|
logger.error({ error }, 'Error clearing settings'); |
|
} |
|
} |
|
} |
|
|
|
// Singleton instance |
|
export const settingsStore = new SettingsStore();
|
|
|