UNPKG

11.7 kBJavaScriptView Raw
1const instanceOfAny = (object, constructors) => constructors.some((c) => object instanceof c);
2
3let idbProxyableTypes;
4let cursorAdvanceMethods;
5// This is a function to prevent it throwing up in node environments.
6function getIdbProxyableTypes() {
7 return (idbProxyableTypes ||
8 (idbProxyableTypes = [
9 IDBDatabase,
10 IDBObjectStore,
11 IDBIndex,
12 IDBCursor,
13 IDBTransaction,
14 ]));
15}
16// This is a function to prevent it throwing up in node environments.
17function getCursorAdvanceMethods() {
18 return (cursorAdvanceMethods ||
19 (cursorAdvanceMethods = [
20 IDBCursor.prototype.advance,
21 IDBCursor.prototype.continue,
22 IDBCursor.prototype.continuePrimaryKey,
23 ]));
24}
25const transactionDoneMap = new WeakMap();
26const transformCache = new WeakMap();
27const reverseTransformCache = new WeakMap();
28function promisifyRequest(request) {
29 const promise = new Promise((resolve, reject) => {
30 const unlisten = () => {
31 request.removeEventListener('success', success);
32 request.removeEventListener('error', error);
33 };
34 const success = () => {
35 resolve(wrap(request.result));
36 unlisten();
37 };
38 const error = () => {
39 reject(request.error);
40 unlisten();
41 };
42 request.addEventListener('success', success);
43 request.addEventListener('error', error);
44 });
45 // This mapping exists in reverseTransformCache but doesn't doesn't exist in transformCache. This
46 // is because we create many promises from a single IDBRequest.
47 reverseTransformCache.set(promise, request);
48 return promise;
49}
50function cacheDonePromiseForTransaction(tx) {
51 // Early bail if we've already created a done promise for this transaction.
52 if (transactionDoneMap.has(tx))
53 return;
54 const done = new Promise((resolve, reject) => {
55 const unlisten = () => {
56 tx.removeEventListener('complete', complete);
57 tx.removeEventListener('error', error);
58 tx.removeEventListener('abort', error);
59 };
60 const complete = () => {
61 resolve();
62 unlisten();
63 };
64 const error = () => {
65 reject(tx.error || new DOMException('AbortError', 'AbortError'));
66 unlisten();
67 };
68 tx.addEventListener('complete', complete);
69 tx.addEventListener('error', error);
70 tx.addEventListener('abort', error);
71 });
72 // Cache it for later retrieval.
73 transactionDoneMap.set(tx, done);
74}
75let idbProxyTraps = {
76 get(target, prop, receiver) {
77 if (target instanceof IDBTransaction) {
78 // Special handling for transaction.done.
79 if (prop === 'done')
80 return transactionDoneMap.get(target);
81 // Make tx.store return the only store in the transaction, or undefined if there are many.
82 if (prop === 'store') {
83 return receiver.objectStoreNames[1]
84 ? undefined
85 : receiver.objectStore(receiver.objectStoreNames[0]);
86 }
87 }
88 // Else transform whatever we get back.
89 return wrap(target[prop]);
90 },
91 set(target, prop, value) {
92 target[prop] = value;
93 return true;
94 },
95 has(target, prop) {
96 if (target instanceof IDBTransaction &&
97 (prop === 'done' || prop === 'store')) {
98 return true;
99 }
100 return prop in target;
101 },
102};
103function replaceTraps(callback) {
104 idbProxyTraps = callback(idbProxyTraps);
105}
106function wrapFunction(func) {
107 // Due to expected object equality (which is enforced by the caching in `wrap`), we
108 // only create one new func per func.
109 // Cursor methods are special, as the behaviour is a little more different to standard IDB. In
110 // IDB, you advance the cursor and wait for a new 'success' on the IDBRequest that gave you the
111 // cursor. It's kinda like a promise that can resolve with many values. That doesn't make sense
112 // with real promises, so each advance methods returns a new promise for the cursor object, or
113 // undefined if the end of the cursor has been reached.
114 if (getCursorAdvanceMethods().includes(func)) {
115 return function (...args) {
116 // Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use
117 // the original object.
118 func.apply(unwrap(this), args);
119 return wrap(this.request);
120 };
121 }
122 return function (...args) {
123 // Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use
124 // the original object.
125 return wrap(func.apply(unwrap(this), args));
126 };
127}
128function transformCachableValue(value) {
129 if (typeof value === 'function')
130 return wrapFunction(value);
131 // This doesn't return, it just creates a 'done' promise for the transaction,
132 // which is later returned for transaction.done (see idbObjectHandler).
133 if (value instanceof IDBTransaction)
134 cacheDonePromiseForTransaction(value);
135 if (instanceOfAny(value, getIdbProxyableTypes()))
136 return new Proxy(value, idbProxyTraps);
137 // Return the same value back if we're not going to transform it.
138 return value;
139}
140function wrap(value) {
141 // We sometimes generate multiple promises from a single IDBRequest (eg when cursoring), because
142 // IDB is weird and a single IDBRequest can yield many responses, so these can't be cached.
143 if (value instanceof IDBRequest)
144 return promisifyRequest(value);
145 // If we've already transformed this value before, reuse the transformed value.
146 // This is faster, but it also provides object equality.
147 if (transformCache.has(value))
148 return transformCache.get(value);
149 const newValue = transformCachableValue(value);
150 // Not all types are transformed.
151 // These may be primitive types, so they can't be WeakMap keys.
152 if (newValue !== value) {
153 transformCache.set(value, newValue);
154 reverseTransformCache.set(newValue, value);
155 }
156 return newValue;
157}
158const unwrap = (value) => reverseTransformCache.get(value);
159
160/**
161 * Open a database.
162 *
163 * @param name Name of the database.
164 * @param version Schema version.
165 * @param callbacks Additional callbacks.
166 */
167function openDB(name, version, { blocked, upgrade, blocking, terminated } = {}) {
168 const request = indexedDB.open(name, version);
169 const openPromise = wrap(request);
170 if (upgrade) {
171 request.addEventListener('upgradeneeded', (event) => {
172 upgrade(wrap(request.result), event.oldVersion, event.newVersion, wrap(request.transaction), event);
173 });
174 }
175 if (blocked) {
176 request.addEventListener('blocked', (event) => blocked(
177 // Casting due to https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1405
178 event.oldVersion, event.newVersion, event));
179 }
180 openPromise
181 .then((db) => {
182 if (terminated)
183 db.addEventListener('close', () => terminated());
184 if (blocking) {
185 db.addEventListener('versionchange', (event) => blocking(event.oldVersion, event.newVersion, event));
186 }
187 })
188 .catch(() => { });
189 return openPromise;
190}
191/**
192 * Delete a database.
193 *
194 * @param name Name of the database.
195 */
196function deleteDB(name, { blocked } = {}) {
197 const request = indexedDB.deleteDatabase(name);
198 if (blocked) {
199 request.addEventListener('blocked', (event) => blocked(
200 // Casting due to https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1405
201 event.oldVersion, event));
202 }
203 return wrap(request).then(() => undefined);
204}
205
206const readMethods = ['get', 'getKey', 'getAll', 'getAllKeys', 'count'];
207const writeMethods = ['put', 'add', 'delete', 'clear'];
208const cachedMethods = new Map();
209function getMethod(target, prop) {
210 if (!(target instanceof IDBDatabase &&
211 !(prop in target) &&
212 typeof prop === 'string')) {
213 return;
214 }
215 if (cachedMethods.get(prop))
216 return cachedMethods.get(prop);
217 const targetFuncName = prop.replace(/FromIndex$/, '');
218 const useIndex = prop !== targetFuncName;
219 const isWrite = writeMethods.includes(targetFuncName);
220 if (
221 // Bail if the target doesn't exist on the target. Eg, getAll isn't in Edge.
222 !(targetFuncName in (useIndex ? IDBIndex : IDBObjectStore).prototype) ||
223 !(isWrite || readMethods.includes(targetFuncName))) {
224 return;
225 }
226 const method = async function (storeName, ...args) {
227 // isWrite ? 'readwrite' : undefined gzipps better, but fails in Edge :(
228 const tx = this.transaction(storeName, isWrite ? 'readwrite' : 'readonly');
229 let target = tx.store;
230 if (useIndex)
231 target = target.index(args.shift());
232 // Must reject if op rejects.
233 // If it's a write operation, must reject if tx.done rejects.
234 // Must reject with op rejection first.
235 // Must resolve with op value.
236 // Must handle both promises (no unhandled rejections)
237 return (await Promise.all([
238 target[targetFuncName](...args),
239 isWrite && tx.done,
240 ]))[0];
241 };
242 cachedMethods.set(prop, method);
243 return method;
244}
245replaceTraps((oldTraps) => ({
246 ...oldTraps,
247 get: (target, prop, receiver) => getMethod(target, prop) || oldTraps.get(target, prop, receiver),
248 has: (target, prop) => !!getMethod(target, prop) || oldTraps.has(target, prop),
249}));
250
251const advanceMethodProps = ['continue', 'continuePrimaryKey', 'advance'];
252const methodMap = {};
253const advanceResults = new WeakMap();
254const ittrProxiedCursorToOriginalProxy = new WeakMap();
255const cursorIteratorTraps = {
256 get(target, prop) {
257 if (!advanceMethodProps.includes(prop))
258 return target[prop];
259 let cachedFunc = methodMap[prop];
260 if (!cachedFunc) {
261 cachedFunc = methodMap[prop] = function (...args) {
262 advanceResults.set(this, ittrProxiedCursorToOriginalProxy.get(this)[prop](...args));
263 };
264 }
265 return cachedFunc;
266 },
267};
268async function* iterate(...args) {
269 // tslint:disable-next-line:no-this-assignment
270 let cursor = this;
271 if (!(cursor instanceof IDBCursor)) {
272 cursor = await cursor.openCursor(...args);
273 }
274 if (!cursor)
275 return;
276 cursor = cursor;
277 const proxiedCursor = new Proxy(cursor, cursorIteratorTraps);
278 ittrProxiedCursorToOriginalProxy.set(proxiedCursor, cursor);
279 // Map this double-proxy back to the original, so other cursor methods work.
280 reverseTransformCache.set(proxiedCursor, unwrap(cursor));
281 while (cursor) {
282 yield proxiedCursor;
283 // If one of the advancing methods was not called, call continue().
284 cursor = await (advanceResults.get(proxiedCursor) || cursor.continue());
285 advanceResults.delete(proxiedCursor);
286 }
287}
288function isIteratorProp(target, prop) {
289 return ((prop === Symbol.asyncIterator &&
290 instanceOfAny(target, [IDBIndex, IDBObjectStore, IDBCursor])) ||
291 (prop === 'iterate' && instanceOfAny(target, [IDBIndex, IDBObjectStore])));
292}
293replaceTraps((oldTraps) => ({
294 ...oldTraps,
295 get(target, prop, receiver) {
296 if (isIteratorProp(target, prop))
297 return iterate;
298 return oldTraps.get(target, prop, receiver);
299 },
300 has(target, prop) {
301 return isIteratorProp(target, prop) || oldTraps.has(target, prop);
302 },
303}));
304
305export { deleteDB, openDB, unwrap, wrap };