(function (global) { const APP_PREFIX = "bearesponsecare:v2"; const DATABASE_NAME = "bearesponsecare-workspace-v2"; const DATABASE_VERSION = 3; const ENTRY_STORE = "entries"; const APP_KV_PROFILE = "__application__"; const APP_KV_CATEGORY = "app-kv"; const CACHE_PREFIX = "bearesponsecare"; const DOCUMENT_CACHE_NAME = `${CACHE_PREFIX}-documents-v1`; const DOCUMENT_CACHE_ORIGIN = "https://bearesponsecare.local"; let databasePromise = null; let initializationPromise = null; const persistentMirror = new Map(); const sessionMirror = new Map(); const pendingWrites = new Set(); const changeChannel = typeof global.BroadcastChannel === "function" ? new global.BroadcastChannel("bearesponsecare-storage-v2") : null; function announceChange(action, scope, name) { changeChannel?.postMessage({ action, scope, name, occurredAt: new Date().toISOString() }); } function buildKey(name) { return `${APP_PREFIX}:${name}`; } function supportsChromeExtensionStorage() { return Boolean(global.chrome?.runtime?.id && global.chrome?.storage?.local); } function extensionGetAll() { if (!supportsChromeExtensionStorage()) return Promise.resolve({}); return new Promise((resolve) => global.chrome.storage.local.get(null, (items) => resolve(items || {}))); } function extensionSet(name, value) { if (!supportsChromeExtensionStorage()) return Promise.resolve(false); return new Promise((resolve) => global.chrome.storage.local.set({ [buildKey(name)]: value }, () => resolve(true))); } function extensionRemove(name) { if (!supportsChromeExtensionStorage()) return Promise.resolve(false); return new Promise((resolve) => global.chrome.storage.local.remove(buildKey(name), () => resolve(true))); } function trackWrite(promise) { pendingWrites.add(promise); promise.finally(() => pendingWrites.delete(promise)); return promise; } async function flush() { await Promise.allSettled([...pendingWrites]); return true; } function persistApplicationValue(name, value) { const durableWrite = putDurable(APP_KV_PROFILE, APP_KV_CATEGORY, name, value).catch((error) => { console.warn("BeaReponseCare IndexedDB application write failed", error); }); const extensionWrite = extensionSet(name, value).catch((error) => console.warn("Chrome extension storage write failed", error)); trackWrite(Promise.allSettled([durableWrite, extensionWrite])); } async function initialize() { if (initializationPromise) return initializationPromise; initializationPromise = (async () => { if (supportsIndexedDb()) { const entries = await listDurable(APP_KV_PROFILE, APP_KV_CATEGORY).catch(() => []); entries.forEach((entry) => persistentMirror.set(entry.id, entry.value)); } const extensionValues = await extensionGetAll().catch(() => ({})); Object.entries(extensionValues).forEach(([key, value]) => { if (key.startsWith(buildKey(""))) persistentMirror.set(key.slice(buildKey("").length), value); }); // One-time migration from legacy Web Storage into IndexedDB/extension storage. [global.localStorage, global.sessionStorage].forEach((legacyStore) => { if (!legacyStore) return; const keys = []; for (let index = 0; index < legacyStore.length; index += 1) { const key = legacyStore.key(index); if (key?.startsWith(buildKey(""))) keys.push(key); } keys.forEach((key) => { const name = key.slice(buildKey("").length); try { const value = JSON.parse(legacyStore.getItem(key)); if (name.startsWith("dataset:")) { sessionMirror.set(name, value); } else if (!persistentMirror.has(name)) { persistentMirror.set(name, value); persistApplicationValue(name, value); } } catch (error) { console.warn("Legacy browser storage migration skipped an invalid entry", error); } legacyStore.removeItem(key); }); }); await flush(); return { indexedDb: supportsIndexedDb(), chromeExtension: supportsChromeExtensionStorage(), migrated: true }; })(); return initializationPromise; } function read(name, fallbackValue) { return persistentMirror.has(name) ? persistentMirror.get(name) : fallbackValue; } function write(name, value) { persistentMirror.set(name, value); persistApplicationValue(name, value); announceChange("write", supportsChromeExtensionStorage() ? "chrome.storage.local + IndexedDB" : "IndexedDB", name); return value; } function remove(name) { const existed = persistentMirror.delete(name); const durableDelete = deleteDurable(APP_KV_PROFILE, APP_KV_CATEGORY, name).catch((error) => console.warn("IndexedDB application delete failed", error)); const extensionDelete = extensionRemove(name).catch((error) => console.warn("Chrome extension storage delete failed", error)); trackWrite(Promise.allSettled([durableDelete, extensionDelete])); announceChange("remove", supportsChromeExtensionStorage() ? "chrome.storage.local + IndexedDB" : "IndexedDB", name); return existed; } function readSession(name, fallbackValue) { return sessionMirror.has(name) ? sessionMirror.get(name) : fallbackValue; } function writeSession(name, value) { sessionMirror.set(name, value); announceChange("write", "Memory", name); return value; } function removeSession(name) { const existed = sessionMirror.delete(name); announceChange("remove", "Memory", name); return existed; } function profileKey(profileId, name) { return `profile:${profileId}:${name}`; } function readProfile(profileId, name, fallbackValue) { return read(profileKey(profileId, name), fallbackValue); } function writeProfile(profileId, name, value) { const result = write(profileKey(profileId, name), value); putDurable(profileId, "profile-cache", name, value).catch((error) => { console.warn("BeaReponseCare IndexedDB mirror failed", error); }); return result; } function removeProfile(profileId, name) { const result = remove(profileKey(profileId, name)); deleteDurable(profileId, "profile-cache", name).catch((error) => { console.warn("BeaReponseCare IndexedDB mirror delete failed", error); }); return result; } function listEntries(store, namePrefix) { const entries = []; store.forEach((value, name) => { if (!name.startsWith(namePrefix)) return; const rawValue = JSON.stringify(value); entries.push({ name, scopedName: name.slice(namePrefix.length), bytes: new Blob([rawValue]).size }); }); return entries.sort((left, right) => left.name.localeCompare(right.name)); } function listProfileEntries(profileId) { return listEntries(persistentMirror, `${profileKey(profileId, "")}`); } function listSessionEntries() { return listEntries(sessionMirror, "dataset:"); } function clearProfileEntries(profileId) { const entries = listProfileEntries(profileId); const removed = entries.filter((entry) => remove(entry.name)).length; clearDurableProfile(profileId).catch((error) => { console.warn("BeaReponseCare IndexedDB profile clear failed", error); }); return { found: entries.length, removed, remaining: listProfileEntries(profileId).length }; } function clearSessionEntries() { const entries = listSessionEntries(); const removed = entries.filter((entry) => removeSession(entry.name)).length; return { found: entries.length, removed, remaining: listSessionEntries().length }; } function clearAllAppEntries() { const localEntries = listEntries(persistentMirror, ""); const sessionEntries = listEntries(sessionMirror, ""); const localRemoved = localEntries.filter((entry) => remove(entry.name)).length; const sessionRemoved = sessionEntries.filter((entry) => removeSession(entry.name)).length; clearDurableAll().catch((error) => { console.warn("BeaReponseCare IndexedDB clear failed", error); }); return { found: localEntries.length + sessionEntries.length, removed: localRemoved + sessionRemoved, remaining: persistentMirror.size + sessionMirror.size }; } function supportsIndexedDb() { return Boolean(global.indexedDB); } function supportsCacheStorage() { return Boolean(global.caches); } function documentCacheUrl(profileId, requestId, documentId) { return `${DOCUMENT_CACHE_ORIGIN}/documents/${encodeURIComponent(profileId)}/${encodeURIComponent(requestId)}/${encodeURIComponent(documentId)}`; } function decodeDocumentHeader(response, name) { const value = response.headers.get(name) || ""; try { return decodeURIComponent(value); } catch (error) { return value; } } function cachedDocumentMetadata(response, cacheUrl) { return { documentId: response.headers.get("x-brc-document-id") || "", requestId: response.headers.get("x-brc-request-id") || "", profileId: response.headers.get("x-brc-profile-id") || "", label: decodeDocumentHeader(response, "x-brc-document-label"), fileName: decodeDocumentHeader(response, "x-brc-file-name"), documentType: decodeDocumentHeader(response, "x-brc-document-type"), contentType: response.headers.get("content-type") || "application/octet-stream", size: Number(response.headers.get("x-brc-file-size") || 0), uploadedAt: response.headers.get("x-brc-uploaded-at") || "", cacheUrl }; } async function putCachedDocument(profileId, requestId, label, file, documentType = "Supporting document") { if (!supportsCacheStorage()) { throw new Error("Cache API is not available in this browser."); } if (!profileId || !requestId || !label || !file) { throw new Error("Profile, request, label, and file are required."); } const documentId = `DOC-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const cacheUrl = documentCacheUrl(profileId, requestId, documentId); const uploadedAt = new Date().toISOString(); const headers = new Headers({ "content-type": file.type || "application/octet-stream", "content-length": String(file.size || 0), "x-brc-document-id": documentId, "x-brc-request-id": requestId, "x-brc-profile-id": profileId, "x-brc-document-label": encodeURIComponent(label), "x-brc-file-name": encodeURIComponent(file.name || "document"), "x-brc-document-type": encodeURIComponent(documentType), "x-brc-file-size": String(file.size || 0), "x-brc-uploaded-at": uploadedAt }); const cache = await global.caches.open(DOCUMENT_CACHE_NAME); await cache.put(new Request(cacheUrl), new Response(file, { headers })); announceChange("write", "CacheStorage", `${requestId}:${documentId}`); return { documentId, requestId, profileId, label, fileName: file.name || "document", documentType, contentType: file.type || "application/octet-stream", size: file.size || 0, uploadedAt, cacheUrl }; } async function listCachedDocuments(profileId, requestId = "") { if (!supportsCacheStorage()) { return []; } const cache = await global.caches.open(DOCUMENT_CACHE_NAME); const prefix = requestId ? `${DOCUMENT_CACHE_ORIGIN}/documents/${encodeURIComponent(profileId)}/${encodeURIComponent(requestId)}/` : `${DOCUMENT_CACHE_ORIGIN}/documents/${encodeURIComponent(profileId)}/`; const requests = (await cache.keys()).filter((request) => request.url.startsWith(prefix)); const documents = await Promise.all(requests.map(async (request) => { const response = await cache.match(request); return response ? cachedDocumentMetadata(response, request.url) : null; })); return documents.filter(Boolean).sort((left, right) => String(right.uploadedAt).localeCompare(String(left.uploadedAt))); } async function getCachedDocument(profileId, requestId, documentId) { if (!supportsCacheStorage()) { return null; } const cacheUrl = documentCacheUrl(profileId, requestId, documentId); const cache = await global.caches.open(DOCUMENT_CACHE_NAME); const response = await cache.match(cacheUrl); return response ? { metadata: cachedDocumentMetadata(response, cacheUrl), response } : null; } async function deleteCachedDocument(profileId, requestId, documentId) { if (!supportsCacheStorage()) { return false; } const cache = await global.caches.open(DOCUMENT_CACHE_NAME); const removed = await cache.delete(documentCacheUrl(profileId, requestId, documentId)); if (removed) announceChange("remove", "CacheStorage", `${requestId}:${documentId}`); return removed; } async function clearCachedDocuments(profileId) { if (!supportsCacheStorage()) { return 0; } const cache = await global.caches.open(DOCUMENT_CACHE_NAME); const prefix = `${DOCUMENT_CACHE_ORIGIN}/documents/${encodeURIComponent(profileId)}/`; const requests = (await cache.keys()).filter((request) => request.url.startsWith(prefix)); const results = await Promise.all(requests.map((request) => cache.delete(request))); return results.filter(Boolean).length; } function openDatabase() { if (!supportsIndexedDb()) { return Promise.reject(new Error("IndexedDB is not available in this browser.")); } if (databasePromise) { return databasePromise; } databasePromise = new Promise((resolve, reject) => { const request = global.indexedDB.open(DATABASE_NAME, DATABASE_VERSION); request.onupgradeneeded = () => { const database = request.result; const store = database.objectStoreNames.contains(ENTRY_STORE) ? request.transaction.objectStore(ENTRY_STORE) : database.createObjectStore(ENTRY_STORE, { keyPath: "key" }); if (!store.indexNames.contains("profileCategory")) { store.createIndex("profileCategory", ["profileId", "category"], { unique: false }); } if (!store.indexNames.contains("profileId")) { store.createIndex("profileId", "profileId", { unique: false }); } }; request.onsuccess = () => { const database = request.result; database.onversionchange = () => { database.close(); databasePromise = null; }; resolve(database); }; request.onerror = () => { databasePromise = null; reject(request.error || new Error("IndexedDB could not be opened.")); }; request.onblocked = () => { databasePromise = null; reject(new Error("IndexedDB upgrade is blocked by another tab. Close other BeaReponseCare tabs and retry.")); }; }); return databasePromise; } function durableKey(profileId, category, id) { return `${profileId}:${category}:${id}`; } async function runIndexedTransaction(mode, handler) { const database = await openDatabase(); return new Promise((resolve, reject) => { const transaction = database.transaction(ENTRY_STORE, mode); const store = transaction.objectStore(ENTRY_STORE); let result; try { result = handler(store, transaction); } catch (error) { transaction.abort(); reject(error); return; } transaction.oncomplete = () => resolve(result); transaction.onerror = () => reject(transaction.error || new Error("IndexedDB transaction failed.")); transaction.onabort = () => reject(transaction.error || new Error("IndexedDB transaction was aborted.")); }); } async function putDurable(profileId, category, id, value) { const entry = { key: durableKey(profileId, category, id), profileId, category, id, value, updatedAt: new Date().toISOString() }; await runIndexedTransaction("readwrite", (store) => store.put(entry)); return entry; } async function getDurable(profileId, category, id, fallbackValue = null) { const database = await openDatabase(); return new Promise((resolve, reject) => { const request = database.transaction(ENTRY_STORE, "readonly") .objectStore(ENTRY_STORE) .get(durableKey(profileId, category, id)); request.onsuccess = () => resolve(request.result ? request.result.value : fallbackValue); request.onerror = () => reject(request.error || new Error("IndexedDB record read failed.")); }); } async function listDurable(profileId, category) { const database = await openDatabase(); return new Promise((resolve, reject) => { const index = database.transaction(ENTRY_STORE, "readonly") .objectStore(ENTRY_STORE) .index("profileCategory"); const request = index.getAll(global.IDBKeyRange.only([profileId, category])); request.onsuccess = () => resolve(request.result.sort((left, right) => String(right.updatedAt).localeCompare(String(left.updatedAt)))); request.onerror = () => reject(request.error || new Error("IndexedDB collection read failed.")); }); } async function listDurableProfile(profileId) { const database = await openDatabase(); return new Promise((resolve, reject) => { const request = database.transaction(ENTRY_STORE, "readonly") .objectStore(ENTRY_STORE) .index("profileId") .getAll(global.IDBKeyRange.only(profileId)); request.onsuccess = () => resolve(request.result.sort((left, right) => String(right.updatedAt).localeCompare(String(left.updatedAt)))); request.onerror = () => reject(request.error || new Error("IndexedDB profile inventory failed.")); }); } async function deleteDurable(profileId, category, id) { await runIndexedTransaction("readwrite", (store) => store.delete(durableKey(profileId, category, id))); return true; } async function deleteEntriesByIndex(indexName, key) { const database = await openDatabase(); return new Promise((resolve, reject) => { const transaction = database.transaction(ENTRY_STORE, "readwrite"); const store = transaction.objectStore(ENTRY_STORE); const request = store.index(indexName).openKeyCursor(global.IDBKeyRange.only(key)); let removed = 0; request.onsuccess = () => { const cursor = request.result; if (!cursor) { return; } store.delete(cursor.primaryKey); removed += 1; cursor.continue(); }; request.onerror = () => reject(request.error || new Error("IndexedDB clear cursor failed.")); transaction.oncomplete = () => resolve(removed); transaction.onerror = () => reject(transaction.error || new Error("IndexedDB clear transaction failed.")); }); } async function clearDurableProfile(profileId) { if (!supportsIndexedDb()) { return 0; } return deleteEntriesByIndex("profileId", profileId); } async function clearDurableAll() { if (!supportsIndexedDb()) { return 0; } await runIndexedTransaction("readwrite", (store) => store.clear()); return true; } async function clearProfileEntriesAsync(profileId) { const localResult = clearProfileEntries(profileId); const indexedRemoved = await clearDurableProfile(profileId).catch(() => 0); const cachedDocumentsRemoved = await clearCachedDocuments(profileId).catch(() => 0); return { ...localResult, indexedRemoved, cachedDocumentsRemoved }; } async function clearAllAppEntriesAsync() { const localResult = clearAllAppEntries(); await clearDurableAll().catch(() => false); if (supportsChromeExtensionStorage()) { const extensionValues = await extensionGetAll().catch(() => ({})); const appKeys = Object.keys(extensionValues).filter((key) => key.startsWith(buildKey(""))); if (appKeys.length) await new Promise((resolve) => global.chrome.storage.local.remove(appKeys, resolve)); } const cacheNames = global.caches ? await global.caches.keys().catch(() => []) : []; const appCaches = cacheNames.filter((name) => name.toLowerCase().startsWith(CACHE_PREFIX)); if (global.caches) await Promise.all(appCaches.map((name) => global.caches.delete(name))); const indexedRemaining = supportsIndexedDb() ? await countDurableEntries().catch(() => -1) : 0; const cacheRemaining = global.caches ? (await global.caches.keys()).filter((name) => name.toLowerCase().startsWith(CACHE_PREFIX)).length : 0; const extensionRemaining = supportsChromeExtensionStorage() ? Object.keys(await extensionGetAll().catch(() => ({}))).filter((key) => key.startsWith(buildKey(""))).length : 0; return { ...localResult, indexedCleared: indexedRemaining === 0, indexedRemaining, cacheRemoved: appCaches.length, cacheRemaining, extensionRemaining, verified: localResult.remaining === 0 && indexedRemaining === 0 && cacheRemaining === 0 && extensionRemaining === 0 }; } async function countDurableEntries() { if (!supportsIndexedDb()) return 0; const database = await openDatabase(); return new Promise((resolve, reject) => { const request = database.transaction(ENTRY_STORE, "readonly").objectStore(ENTRY_STORE).count(); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error || new Error("IndexedDB count failed.")); }); } async function getStorageInventory(profileId) { const durableEntries = supportsIndexedDb() ? await listDurableProfile(profileId).catch(() => []) : []; const cacheNames = global.caches ? await global.caches.keys().catch(() => []) : []; return [ { layer: "Memory", contents: "Active page state and session-only remote credential", retention: "Until reload or sign out", count: null }, { layer: "Memory cache", contents: "Published dataset cache", retention: "Current page session", count: sessionMirror.size }, { layer: supportsChromeExtensionStorage() ? "Chrome extension storage" : "IndexedDB application store", contents: "Local profiles, drafts, additions, overrides, and activity index", retention: "Until user deletion", count: persistentMirror.size }, { layer: "IndexedDB", contents: "Application state, durable records, drafts, queue operations, receipts, and conflict drafts", retention: "Until profile or application deletion", count: durableEntries.length }, { layer: "Cache Storage", contents: "Application-owned response caches only", retention: "Until application deletion", count: cacheNames.filter((name) => name.toLowerCase().startsWith(CACHE_PREFIX)).length }, { layer: "Shared service", contents: "Submitted operation envelope and remote acknowledgement", retention: "Controlled by the remote service", count: null } ]; } async function getPersistenceStatus(profileId) { if (!supportsIndexedDb()) { return { indexedDb: false, durableEntries: 0, mode: supportsChromeExtensionStorage() ? "Chrome extension storage" : "Memory only" }; } const database = await openDatabase(); const durableEntries = await new Promise((resolve, reject) => { const request = database.transaction(ENTRY_STORE, "readonly") .objectStore(ENTRY_STORE) .index("profileId") .count(global.IDBKeyRange.only(profileId)); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error || new Error("IndexedDB status check failed.")); }); return { indexedDb: true, durableEntries, mode: supportsChromeExtensionStorage() ? "Chrome extension storage + IndexedDB" : "IndexedDB", schemaVersion: database.version }; } global.BeaStorageService = Object.freeze({ initialize, flush, read, write, remove, readSession, writeSession, removeSession, readProfile, writeProfile, removeProfile, listProfileEntries, listSessionEntries, clearProfileEntries, clearSessionEntries, clearAllAppEntries, clearProfileEntriesAsync, clearAllAppEntriesAsync, supportsIndexedDb, supportsCacheStorage, supportsChromeExtensionStorage, putCachedDocument, listCachedDocuments, getCachedDocument, deleteCachedDocument, clearCachedDocuments, putDurable, getDurable, listDurable, listDurableProfile, deleteDurable, clearDurableProfile, clearDurableAll, countDurableEntries, getStorageInventory, getPersistenceStatus }); })(window);