| (function (root) { |
| "use strict"; |
| const DB_NAME = "voiceofml-reader"; |
| const STORE_NAME = "entries"; |
| const BOOKMARK_STORE_NAME = "bookmarks"; |
| const MAX_ENTRIES = 200; |
| let databasePromise = null; |
|
|
| function openDatabase() { |
| if (databasePromise) return databasePromise; |
| const opening = new Promise((resolve, reject) => { |
| const request = indexedDB.open(DB_NAME, 2); |
| request.onupgradeneeded = () => { |
| const database = request.result; |
| if (!database.objectStoreNames.contains(STORE_NAME)) { |
| const store = database.createObjectStore(STORE_NAME, { keyPath: "url" }); |
| store.createIndex("lastReadAt", "lastReadAt"); |
| } |
| if (!database.objectStoreNames.contains(BOOKMARK_STORE_NAME)) { |
| const bookmarks = database.createObjectStore(BOOKMARK_STORE_NAME, { keyPath: "id" }); |
| bookmarks.createIndex("url", "url"); |
| bookmarks.createIndex("createdAt", "createdAt"); |
| } |
| }; |
| request.onsuccess = () => { |
| const database = request.result; |
| database.onversionchange = () => { database.close(); if (databasePromise === opening) databasePromise = null; }; |
| if (databasePromise !== opening) { database.close(); return; } |
| resolve(database); |
| }; |
| request.onblocked = () => { if (databasePromise === opening) databasePromise = null; reject(new Error("Reader database upgrade was blocked by another tab")); }; |
| request.onerror = () => { if (databasePromise === opening) databasePromise = null; reject(request.error); }; |
| }); |
| databasePromise = opening; |
| return databasePromise; |
| } |
|
|
| async function transaction(mode, callback) { |
| const database = await openDatabase(); |
| return new Promise((resolve, reject) => { |
| const tx = database.transaction(STORE_NAME, mode); |
| const result = callback(tx.objectStore(STORE_NAME)); |
| tx.oncomplete = () => resolve(result); |
| tx.onerror = () => reject(tx.error); |
| tx.onabort = () => reject(tx.error); |
| }); |
| } |
|
|
| async function get(url) { |
| const database = await openDatabase(); |
| return new Promise((resolve, reject) => { |
| const tx = database.transaction(STORE_NAME, "readonly"); |
| const request = tx.objectStore(STORE_NAME).get(url); |
| request.onsuccess = () => resolve(request.result || null); |
| request.onerror = () => reject(request.error); |
| }); |
| } |
|
|
| async function put(entry) { |
| const database = await openDatabase(); |
| await new Promise((resolve, reject) => { |
| const tx = database.transaction(STORE_NAME, "readwrite"); |
| const store = tx.objectStore(STORE_NAME); |
| const existing = store.get(entry.url); |
| existing.onsuccess = () => { |
| if (existing.result) { |
| store.put(entry); |
| return; |
| } |
| const count = store.count(); |
| count.onsuccess = () => { |
| if (count.result < MAX_ENTRIES) { |
| store.put(entry); |
| return; |
| } |
| const oldest = store.index("lastReadAt").openCursor(); |
| oldest.onsuccess = () => { |
| if (oldest.result) oldest.result.delete(); |
| store.put(entry); |
| }; |
| }; |
| }; |
| tx.oncomplete = resolve; |
| tx.onerror = () => reject(tx.error); |
| tx.onabort = () => reject(tx.error); |
| }); |
| } |
|
|
| async function list(limit = MAX_ENTRIES) { |
| const database = await openDatabase(); |
| return new Promise((resolve, reject) => { |
| const entries = []; |
| const tx = database.transaction(STORE_NAME, "readonly"); |
| const request = tx.objectStore(STORE_NAME).index("lastReadAt").openCursor(null, "prev"); |
| request.onsuccess = () => { |
| const cursor = request.result; |
| if (!cursor || entries.length >= limit) return resolve(entries); |
| entries.push(cursor.value); cursor.continue(); |
| }; |
| request.onerror = () => reject(request.error); |
| }); |
| } |
|
|
| async function remove(url) { await transaction("readwrite", (store) => store.delete(url)); } |
| async function clearHistory() { await transaction("readwrite", (store) => store.clear()); } |
| async function putBookmark(entry) { |
| const database = await openDatabase(); |
| await new Promise((resolve, reject) => { |
| const tx = database.transaction(BOOKMARK_STORE_NAME, "readwrite"); |
| tx.objectStore(BOOKMARK_STORE_NAME).put(entry); |
| tx.oncomplete = resolve; tx.onerror = () => reject(tx.error); tx.onabort = () => reject(tx.error); |
| }); |
| } |
| async function listBookmarks(url) { |
| const database = await openDatabase(); |
| return new Promise((resolve, reject) => { |
| const entries = []; |
| const tx = database.transaction(BOOKMARK_STORE_NAME, "readonly"); |
| const request = tx.objectStore(BOOKMARK_STORE_NAME).index("url").openCursor(IDBKeyRange.only(url)); |
| request.onsuccess = () => { |
| const cursor = request.result; |
| if (!cursor) return resolve(entries.sort((a, b) => b.createdAt - a.createdAt)); |
| entries.push(cursor.value); cursor.continue(); |
| }; |
| request.onerror = () => reject(request.error); |
| }); |
| } |
| async function listAllBookmarks() { |
| const database = await openDatabase(); |
| return new Promise((resolve, reject) => { |
| const entries = []; |
| const tx = database.transaction(BOOKMARK_STORE_NAME, "readonly"); |
| const request = tx.objectStore(BOOKMARK_STORE_NAME).index("createdAt").openCursor(null, "prev"); |
| request.onsuccess = () => { |
| const cursor = request.result; |
| if (!cursor) return resolve(entries); |
| entries.push(cursor.value); cursor.continue(); |
| }; |
| request.onerror = () => reject(request.error); |
| }); |
| } |
| async function removeBookmark(id) { |
| const database = await openDatabase(); |
| await new Promise((resolve, reject) => { |
| const tx = database.transaction(BOOKMARK_STORE_NAME, "readwrite"); |
| tx.objectStore(BOOKMARK_STORE_NAME).delete(id); |
| tx.oncomplete = resolve; tx.onerror = () => reject(tx.error); tx.onabort = () => reject(tx.error); |
| }); |
| } |
| root.VoiceOfMLReaderStore = Object.freeze({ DB_NAME, MAX_ENTRIES, get, put, list, remove, clearHistory, putBookmark, listBookmarks, listAllBookmarks, removeBookmark }); |
| })(typeof self !== "undefined" ? self : window); |
|
|