1 | /* eslint-env browser */
|
2 |
|
3 | /**
|
4 | * Helpers to work with IndexedDB.
|
5 | *
|
6 | * @module indexeddb
|
7 | */
|
8 |
|
9 | import * as promise from './promise.js'
|
10 | import * as error from './error.js'
|
11 |
|
12 | /* istanbul ignore next */
|
13 | /**
|
14 | * IDB Request to Promise transformer
|
15 | *
|
16 | * @param {IDBRequest} request
|
17 | * @return {Promise<any>}
|
18 | */
|
19 | export const rtop = request => promise.create((resolve, reject) => {
|
20 | /* istanbul ignore next */
|
21 | // @ts-ignore
|
22 | request.onerror = event => reject(new Error(event.target.error))
|
23 | /* istanbul ignore next */
|
24 | // @ts-ignore
|
25 | request.onblocked = () => location.reload()
|
26 | // @ts-ignore
|
27 | request.onsuccess = event => resolve(event.target.result)
|
28 | })
|
29 |
|
30 | /* istanbul ignore next */
|
31 | /**
|
32 | * @param {string} name
|
33 | * @param {function(IDBDatabase):any} initDB Called when the database is first created
|
34 | * @return {Promise<IDBDatabase>}
|
35 | */
|
36 | export const openDB = (name, initDB) => promise.create((resolve, reject) => {
|
37 | const request = indexedDB.open(name)
|
38 | /**
|
39 | * @param {any} event
|
40 | */
|
41 | request.onupgradeneeded = event => initDB(event.target.result)
|
42 | /* istanbul ignore next */
|
43 | /**
|
44 | * @param {any} event
|
45 | */
|
46 | request.onerror = event => reject(error.create(event.target.error))
|
47 | /* istanbul ignore next */
|
48 | request.onblocked = () => location.reload()
|
49 | /**
|
50 | * @param {any} event
|
51 | */
|
52 | request.onsuccess = event => {
|
53 | /**
|
54 | * @type {IDBDatabase}
|
55 | */
|
56 | const db = event.target.result
|
57 | /* istanbul ignore next */
|
58 | db.onversionchange = () => { db.close() }
|
59 | /* istanbul ignore if */
|
60 | if (typeof addEventListener !== 'undefined') {
|
61 | addEventListener('unload', () => db.close())
|
62 | }
|
63 | resolve(db)
|
64 | }
|
65 | })
|
66 |
|
67 | /* istanbul ignore next */
|
68 | /**
|
69 | * @param {string} name
|
70 | */
|
71 | export const deleteDB = name => rtop(indexedDB.deleteDatabase(name))
|
72 |
|
73 | /* istanbul ignore next */
|
74 | /**
|
75 | * @param {IDBDatabase} db
|
76 | * @param {Array<Array<string>|Array<string|IDBObjectStoreParameters|undefined>>} definitions
|
77 | */
|
78 | export const createStores = (db, definitions) => definitions.forEach(d =>
|
79 | // @ts-ignore
|
80 | db.createObjectStore.apply(db, d)
|
81 | )
|
82 |
|
83 | /**
|
84 | * @param {IDBDatabase} db
|
85 | * @param {Array<string>} stores
|
86 | * @param {"readwrite"|"readonly"} [access]
|
87 | * @return {Array<IDBObjectStore>}
|
88 | */
|
89 | export const transact = (db, stores, access = 'readwrite') => {
|
90 | const transaction = db.transaction(stores, access)
|
91 | return stores.map(store => getStore(transaction, store))
|
92 | }
|
93 |
|
94 | /* istanbul ignore next */
|
95 | /**
|
96 | * @param {IDBObjectStore} store
|
97 | * @param {IDBKeyRange} [range]
|
98 | * @return {Promise<number>}
|
99 | */
|
100 | export const count = (store, range) =>
|
101 | rtop(store.count(range))
|
102 |
|
103 | /* istanbul ignore next */
|
104 | /**
|
105 | * @param {IDBObjectStore} store
|
106 | * @param {String | number | ArrayBuffer | Date | Array<any> } key
|
107 | * @return {Promise<String | number | ArrayBuffer | Date | Array<any>>}
|
108 | */
|
109 | export const get = (store, key) =>
|
110 | rtop(store.get(key))
|
111 |
|
112 | /* istanbul ignore next */
|
113 | /**
|
114 | * @param {IDBObjectStore} store
|
115 | * @param {String | number | ArrayBuffer | Date | IDBKeyRange | Array<any> } key
|
116 | */
|
117 | export const del = (store, key) =>
|
118 | rtop(store.delete(key))
|
119 |
|
120 | /* istanbul ignore next */
|
121 | /**
|
122 | * @param {IDBObjectStore} store
|
123 | * @param {String | number | ArrayBuffer | Date | boolean} item
|
124 | * @param {String | number | ArrayBuffer | Date | Array<any>} [key]
|
125 | */
|
126 | export const put = (store, item, key) =>
|
127 | rtop(store.put(item, key))
|
128 |
|
129 | /* istanbul ignore next */
|
130 | /**
|
131 | * @param {IDBObjectStore} store
|
132 | * @param {String | number | ArrayBuffer | Date | boolean} item
|
133 | * @param {String | number | ArrayBuffer | Date | Array<any>} key
|
134 | * @return {Promise<any>}
|
135 | */
|
136 | export const add = (store, item, key) =>
|
137 | rtop(store.add(item, key))
|
138 |
|
139 | /* istanbul ignore next */
|
140 | /**
|
141 | * @param {IDBObjectStore} store
|
142 | * @param {String | number | ArrayBuffer | Date} item
|
143 | * @return {Promise<number>} Returns the generated key
|
144 | */
|
145 | export const addAutoKey = (store, item) =>
|
146 | rtop(store.add(item))
|
147 |
|
148 | /* istanbul ignore next */
|
149 | /**
|
150 | * @param {IDBObjectStore} store
|
151 | * @param {IDBKeyRange} [range]
|
152 | * @return {Promise<Array<any>>}
|
153 | */
|
154 | export const getAll = (store, range) =>
|
155 | rtop(store.getAll(range))
|
156 |
|
157 | /* istanbul ignore next */
|
158 | /**
|
159 | * @param {IDBObjectStore} store
|
160 | * @param {IDBKeyRange} [range]
|
161 | * @return {Promise<Array<any>>}
|
162 | */
|
163 | export const getAllKeys = (store, range) =>
|
164 | rtop(store.getAllKeys(range))
|
165 |
|
166 | /**
|
167 | * @param {IDBObjectStore} store
|
168 | * @param {IDBKeyRange|null} query
|
169 | * @param {'next'|'prev'|'nextunique'|'prevunique'} direction
|
170 | * @return {Promise<any>}
|
171 | */
|
172 | export const queryFirst = (store, query, direction) => {
|
173 | /**
|
174 | * @type {any}
|
175 | */
|
176 | let first = null
|
177 | return iterateKeys(store, query, key => {
|
178 | first = key
|
179 | return false
|
180 | }, direction).then(() => first)
|
181 | }
|
182 |
|
183 | /**
|
184 | * @param {IDBObjectStore} store
|
185 | * @param {IDBKeyRange?} [range]
|
186 | * @return {Promise<any>}
|
187 | */
|
188 | export const getLastKey = (store, range = null) => queryFirst(store, range, 'prev')
|
189 |
|
190 | /**
|
191 | * @param {IDBObjectStore} store
|
192 | * @param {IDBKeyRange?} [range]
|
193 | * @return {Promise<any>}
|
194 | */
|
195 | export const getFirstKey = (store, range = null) => queryFirst(store, range, 'next')
|
196 |
|
197 | /**
|
198 | * @typedef KeyValuePair
|
199 | * @type {Object}
|
200 | * @property {any} k key
|
201 | * @property {any} v Value
|
202 | */
|
203 |
|
204 | /* istanbul ignore next */
|
205 | /**
|
206 | * @param {IDBObjectStore} store
|
207 | * @param {IDBKeyRange} [range]
|
208 | * @return {Promise<Array<KeyValuePair>>}
|
209 | */
|
210 | export const getAllKeysValues = (store, range) =>
|
211 | // @ts-ignore
|
212 | promise.all([getAllKeys(store, range), getAll(store, range)]).then(([ks, vs]) => ks.map((k, i) => ({ k, v: vs[i] })))
|
213 |
|
214 | /* istanbul ignore next */
|
215 | /**
|
216 | * @param {any} request
|
217 | * @param {function(IDBCursorWithValue):void|boolean} f
|
218 | * @return {Promise<void>}
|
219 | */
|
220 | const iterateOnRequest = (request, f) => promise.create((resolve, reject) => {
|
221 | /* istanbul ignore next */
|
222 | request.onerror = reject
|
223 | /**
|
224 | * @param {any} event
|
225 | */
|
226 | request.onsuccess = event => {
|
227 | const cursor = event.target.result
|
228 | if (cursor === null || f(cursor) === false) {
|
229 | return resolve()
|
230 | }
|
231 | cursor.continue()
|
232 | }
|
233 | })
|
234 |
|
235 | /* istanbul ignore next */
|
236 | /**
|
237 | * Iterate on keys and values
|
238 | * @param {IDBObjectStore} store
|
239 | * @param {IDBKeyRange|null} keyrange
|
240 | * @param {function(any,any):void|boolean} f Callback that receives (value, key)
|
241 | * @param {'next'|'prev'|'nextunique'|'prevunique'} direction
|
242 | */
|
243 | export const iterate = (store, keyrange, f, direction = 'next') =>
|
244 | iterateOnRequest(store.openCursor(keyrange, direction), cursor => f(cursor.value, cursor.key))
|
245 |
|
246 | /* istanbul ignore next */
|
247 | /**
|
248 | * Iterate on the keys (no values)
|
249 | *
|
250 | * @param {IDBObjectStore} store
|
251 | * @param {IDBKeyRange|null} keyrange
|
252 | * @param {function(any):void|boolean} f callback that receives the key
|
253 | * @param {'next'|'prev'|'nextunique'|'prevunique'} direction
|
254 | */
|
255 | export const iterateKeys = (store, keyrange, f, direction = 'next') =>
|
256 | iterateOnRequest(store.openKeyCursor(keyrange, direction), cursor => f(cursor.key))
|
257 |
|
258 | /* istanbul ignore next */
|
259 | /**
|
260 | * Open store from transaction
|
261 | * @param {IDBTransaction} t
|
262 | * @param {String} store
|
263 | * @returns {IDBObjectStore}
|
264 | */
|
265 | export const getStore = (t, store) => t.objectStore(store)
|
266 |
|
267 | /* istanbul ignore next */
|
268 | /**
|
269 | * @param {any} lower
|
270 | * @param {any} upper
|
271 | * @param {boolean} lowerOpen
|
272 | * @param {boolean} upperOpen
|
273 | */
|
274 | export const createIDBKeyRangeBound = (lower, upper, lowerOpen, upperOpen) => IDBKeyRange.bound(lower, upper, lowerOpen, upperOpen)
|
275 |
|
276 | /* istanbul ignore next */
|
277 | /**
|
278 | * @param {any} upper
|
279 | * @param {boolean} upperOpen
|
280 | */
|
281 | export const createIDBKeyRangeUpperBound = (upper, upperOpen) => IDBKeyRange.upperBound(upper, upperOpen)
|
282 |
|
283 | /* istanbul ignore next */
|
284 | /**
|
285 | * @param {any} lower
|
286 | * @param {boolean} lowerOpen
|
287 | */
|
288 | export const createIDBKeyRangeLowerBound = (lower, lowerOpen) => IDBKeyRange.lowerBound(lower, lowerOpen)
|