// db/src/factory.ts import { DataLayerProvider } from './core/interfaces'; import { BaseUser } from './impl/common'; import { logger } from './util/logger'; import { StaticCourseManifest } from './util/packer/types'; import { initializeNavigatorRegistry } from './core/navigators'; const NOT_SET = 'NOT_SET' as const; interface DBEnv { COUCHDB_SERVER_URL: string; // URL of CouchDB server COUCHDB_SERVER_PROTOCOL: string; // Protocol of CouchDB server (http or https) COUCHDB_USERNAME?: string; COUCHDB_PASSWORD?: string; LOCAL_STORAGE_PREFIX: string; // Prefix for IndexedDB storage names } export const ENV: DBEnv = { COUCHDB_SERVER_PROTOCOL: NOT_SET, COUCHDB_SERVER_URL: NOT_SET, LOCAL_STORAGE_PREFIX: '', }; export { NOT_SET }; // Configuration type for data layer initialization export interface DataLayerConfig { type: 'couch' | 'static'; options: { staticContentPath?: string; // Path to static content JSON files localStoragePrefix?: string; // Prefix for IndexedDB storage names manifests?: Record; // Course manifests for static mode COUCHDB_SERVER_URL?: string; COUCHDB_SERVER_PROTOCOL?: string; COUCHDB_USERNAME?: string; COUCHDB_PASSWORD?: string; COURSE_IDS?: string[]; /** * Per-app tuning for the CouchDB→PouchDB course sync. Only applies when * `type === 'couch'` and the course has `localSync.enabled === true`. * See CourseSyncService.ReplicationOptions for defaults. */ courseSync?: { replication?: { batchSize?: number; batchesLimit?: number; }; }; }; } // Singleton instance let dataLayerInstance: DataLayerProvider | null = null; /** * Initialize the data layer with the specified configuration */ export async function initializeDataLayer(config: DataLayerConfig): Promise { if (dataLayerInstance) { logger.warn('Data layer already initialized. Returning existing instance.'); return dataLayerInstance; } // Initialize the navigator registry before creating the data layer. // This ensures all built-in navigators are available for pipeline assembly. await initializeNavigatorRegistry(); if (config.options.localStoragePrefix) { ENV.LOCAL_STORAGE_PREFIX = config.options.localStoragePrefix; } if (config.type === 'couch') { if (!config.options.COUCHDB_SERVER_URL || !config.options.COUCHDB_SERVER_PROTOCOL) { throw new Error('Missing CouchDB server URL or protocol'); } ENV.COUCHDB_SERVER_PROTOCOL = config.options.COUCHDB_SERVER_PROTOCOL; ENV.COUCHDB_SERVER_URL = config.options.COUCHDB_SERVER_URL; ENV.COUCHDB_USERNAME = config.options.COUCHDB_USERNAME; ENV.COUCHDB_PASSWORD = config.options.COUCHDB_PASSWORD; if (config.options.courseSync) { const { CourseSyncService } = await import('./impl/couch/CourseSyncService'); CourseSyncService.getInstance().configure(config.options.courseSync); } if ( config.options.COUCHDB_PASSWORD && config.options.COUCHDB_USERNAME && typeof window !== 'undefined' ) { // Dynamic import to avoid loading both implementations when only one is needed const { CouchDBSyncStrategy } = await import('./impl/couch/CouchDBSyncStrategy'); // Create a sync strategy instance and authenticate const syncStrategy = new CouchDBSyncStrategy(); const user = await BaseUser.instance(syncStrategy, config.options.COUCHDB_USERNAME); const authResult = await user.login( config.options.COUCHDB_USERNAME, config.options.COUCHDB_PASSWORD ); if (authResult.ok) { logger.info(`Successfully authenticated as ${config.options.COUCHDB_USERNAME}`); } else { logger.warn(`Authentication failed: ${authResult.error}`); } } // Dynamic import to avoid loading both implementations when only one is needed const { CouchDataLayerProvider } = await import('./impl/couch/PouchDataLayerProvider'); dataLayerInstance = new CouchDataLayerProvider(config.options.COURSE_IDS); } else if (config.type === 'static') { const { StaticDataLayerProvider } = await import('./impl/static/StaticDataLayerProvider'); dataLayerInstance = new StaticDataLayerProvider(config.options); } else { throw new Error(`Unknown data layer type: ${config.type}`); } await dataLayerInstance.initialize(); return dataLayerInstance; } /** * Get the initialized data layer instance * @throws Error if not initialized */ export function getDataLayer(): DataLayerProvider { if (!dataLayerInstance) { throw new Error('Data layer not initialized. Call initializeDataLayer first.'); } return dataLayerInstance; } /** * Reset the data layer (primarily for testing) */ export async function _resetDataLayer(): Promise { if (dataLayerInstance) { await dataLayerInstance.teardown(); } dataLayerInstance = null; }