/** * @file privatri.js * @description WCO component for managing private, thread-based messages in IndexedDB. * @version 1.0 */ ((window) => { 'use strict'; // --- Component Definition --- const privatri = {}; // --- Private State --- let _db = null; // Holds the single, persistent IndexedDB connection const DB_NAME = 'privatriDB'; const DB_VERSION = 1; /** * Logs messages to the console with a consistent prefix. * @param {string} level - The log level (e.g., 'log', 'error', 'warn'). * @param {...any} args - The messages to log. */ const _log = (level, ...args) => { console[level]('[privatri]', ...args); }; /** * Opens and initializes the IndexedDB database. * This function is called by init() and handles the connection and schema upgrades. * @param {string[]} initialStoreNames - An array of store names (thread IDs) to ensure exist. * @returns {Promise} A promise that resolves with the database connection. */ const _openDatabase = (initialStoreNames = []) => { return new Promise((resolve, reject) => { _log('log', `Opening database "${DB_NAME}" with version ${DB_VERSION}.`); const request = indexedDB.open(DB_NAME, DB_VERSION); // This event is only triggered for new databases or version changes. request.onupgradeneeded = (event) => { const db = event.target.result; _log('log', 'Database upgrade needed. Current stores:', [...db.objectStoreNames]); initialStoreNames.forEach(storeName => { if (!db.objectStoreNames.contains(storeName)) { _log('log', `Creating new object store: "${storeName}"`); db.createObjectStore(storeName, { keyPath: 'key' }); } }); }; request.onsuccess = (event) => { _log('log', 'Database opened successfully.'); _db = event.target.result; // Generic error handler for the connection _db.onerror = (event) => { _log('error', 'Database error:', event.target.error); }; resolve(_db); }; request.onerror = (event) => { _log('error', 'Failed to open database:', event.target.error); reject(event.target.error); }; }); }; // --- Public API --- /** * Initializes the privatri component. * Opens the database connection and ensures initial object stores are created. * @param {object} config - Configuration object. * @param {string[]} [config.threads=[]] - An array of initial thread IDs (store names) to create. * @returns {Promise} */ privatri.init = async (config = {}) => { if (_db) { _log('warn', 'privatri component already initialized.'); return; } const threads = config.threads || []; await _openDatabase(threads); }; /** * Retrieves a value from a specific store (thread). * @param {string} storeName - The name of the store (thread ID). * @param {string} key - The key of the item to retrieve. * @returns {Promise} A promise that resolves with the value or null if not found. */ privatri.getValue = (storeName, key) => { return new Promise((resolve, reject) => { if (!_db || !_db.objectStoreNames.contains(storeName)) { _log('warn', `Store "${storeName}" does not exist.`); return resolve(null); } const transaction = _db.transaction(storeName, 'readonly'); const store = transaction.objectStore(storeName); const request = store.get(key); request.onsuccess = () => resolve(request.result ? request.result.value : null); request.onerror = (e) => { _log('error', `Error getting value for key "${key}" from store "${storeName}":`, e.target.error); reject(e.target.error); }; }); }; /** * Adds or updates a key-value pair in a specific store (thread). * @param {string} storeName - The name of the store (thread ID). * @param {string} key - The key of the item to set. * @param {any} value - The value to store. * @returns {Promise} */ privatri.setValue = (storeName, key, value) => { return new Promise((resolve, reject) => { if (!_db || !_db.objectStoreNames.contains(storeName)) { _log('error', `Cannot set value. Store "${storeName}" does not exist.`); return reject(new Error(`Store "${storeName}" not found.`)); } const transaction = _db.transaction(storeName, 'readwrite'); const store = transaction.objectStore(storeName); const request = store.put({ key: key, value: value }); request.onsuccess = () => resolve(); request.onerror = (e) => { _log('error', `Error setting value for key "${key}" in store "${storeName}":`, e.target.error); reject(e.target.error); }; }); }; /** * Removes a key-value pair from a specific store (thread). * @param {string} storeName - The name of the store (thread ID). * @param {string} key - The key of the item to remove. * @returns {Promise} */ privatri.removeKey = (storeName, key) => { return new Promise((resolve, reject) => { if (!_db || !_db.objectStoreNames.contains(storeName)) { _log('warn', `Cannot remove key. Store "${storeName}" does not exist.`); return resolve(); // Resolve peacefully if store doesn't exist } const transaction = _db.transaction(storeName, 'readwrite'); const store = transaction.objectStore(storeName); const request = store.delete(key); request.onsuccess = () => resolve(); request.onerror = (e) => { _log('error', `Error removing key "${key}" from store "${storeName}":`, e.target.error); reject(e.target.error); }; }); }; /** * A utility function to save a batch of messages, demonstrating how to use the component. * @param {object} threadsObj - An object where keys are thread IDs and values are message objects. * @example * const messages = { * "thread-123": { "1678886400": { text: "Hello" } }, * "thread-456": { "1678886401": { text: "Hi there" } } * }; * await privatri.storeMessages(messages); */ privatri.storeMessages = async (threadsObj = {}) => { if (!_db) { _log('error', 'Database not initialized. Please call privatri.init() first.'); return; } _log('log', 'Storing messages in IndexedDB...'); for (const [uuid, threadObj] of Object.entries(threadsObj)) { // Ensure the object store exists before trying to write to it. if (!_db.objectStoreNames.contains(uuid)) { _log('warn', `Store "${uuid}" not found during message storage. You may need to re-init with this thread.`); continue; } for (const [timestamp, messageObj] of Object.entries(threadObj)) { await privatri.setValue(uuid, timestamp, messageObj); } } _log('log', 'Finished storing messages.'); }; // Expose the component to the global window object window.privatri = privatri; })(window);