File size: 6,263 Bytes
4d7e26e
 
 
 
8554abc
4d7e26e
 
 
 
 
8554abc
 
4d7e26e
8554abc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4d7e26e
8554abc
 
4d7e26e
8554abc
4d7e26e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8554abc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4d7e26e
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
(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);