You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
185 lines
7.3 KiB
185 lines
7.3 KiB
const instanceOfAny = (object, constructors) => constructors.some((c) => object instanceof c); |
|
|
|
let idbProxyableTypes; |
|
let cursorAdvanceMethods; |
|
// This is a function to prevent it throwing up in node environments. |
|
function getIdbProxyableTypes() { |
|
return (idbProxyableTypes || |
|
(idbProxyableTypes = [ |
|
IDBDatabase, |
|
IDBObjectStore, |
|
IDBIndex, |
|
IDBCursor, |
|
IDBTransaction, |
|
])); |
|
} |
|
// This is a function to prevent it throwing up in node environments. |
|
function getCursorAdvanceMethods() { |
|
return (cursorAdvanceMethods || |
|
(cursorAdvanceMethods = [ |
|
IDBCursor.prototype.advance, |
|
IDBCursor.prototype.continue, |
|
IDBCursor.prototype.continuePrimaryKey, |
|
])); |
|
} |
|
const cursorRequestMap = new WeakMap(); |
|
const transactionDoneMap = new WeakMap(); |
|
const transactionStoreNamesMap = new WeakMap(); |
|
const transformCache = new WeakMap(); |
|
const reverseTransformCache = new WeakMap(); |
|
function promisifyRequest(request) { |
|
const promise = new Promise((resolve, reject) => { |
|
const unlisten = () => { |
|
request.removeEventListener('success', success); |
|
request.removeEventListener('error', error); |
|
}; |
|
const success = () => { |
|
resolve(wrap(request.result)); |
|
unlisten(); |
|
}; |
|
const error = () => { |
|
reject(request.error); |
|
unlisten(); |
|
}; |
|
request.addEventListener('success', success); |
|
request.addEventListener('error', error); |
|
}); |
|
promise |
|
.then((value) => { |
|
// Since cursoring reuses the IDBRequest (*sigh*), we cache it for later retrieval |
|
// (see wrapFunction). |
|
if (value instanceof IDBCursor) { |
|
cursorRequestMap.set(value, request); |
|
} |
|
// Catching to avoid "Uncaught Promise exceptions" |
|
}) |
|
.catch(() => { }); |
|
// This mapping exists in reverseTransformCache but doesn't doesn't exist in transformCache. This |
|
// is because we create many promises from a single IDBRequest. |
|
reverseTransformCache.set(promise, request); |
|
return promise; |
|
} |
|
function cacheDonePromiseForTransaction(tx) { |
|
// Early bail if we've already created a done promise for this transaction. |
|
if (transactionDoneMap.has(tx)) |
|
return; |
|
const done = new Promise((resolve, reject) => { |
|
const unlisten = () => { |
|
tx.removeEventListener('complete', complete); |
|
tx.removeEventListener('error', error); |
|
tx.removeEventListener('abort', error); |
|
}; |
|
const complete = () => { |
|
resolve(); |
|
unlisten(); |
|
}; |
|
const error = () => { |
|
reject(tx.error || new DOMException('AbortError', 'AbortError')); |
|
unlisten(); |
|
}; |
|
tx.addEventListener('complete', complete); |
|
tx.addEventListener('error', error); |
|
tx.addEventListener('abort', error); |
|
}); |
|
// Cache it for later retrieval. |
|
transactionDoneMap.set(tx, done); |
|
} |
|
let idbProxyTraps = { |
|
get(target, prop, receiver) { |
|
if (target instanceof IDBTransaction) { |
|
// Special handling for transaction.done. |
|
if (prop === 'done') |
|
return transactionDoneMap.get(target); |
|
// Polyfill for objectStoreNames because of Edge. |
|
if (prop === 'objectStoreNames') { |
|
return target.objectStoreNames || transactionStoreNamesMap.get(target); |
|
} |
|
// Make tx.store return the only store in the transaction, or undefined if there are many. |
|
if (prop === 'store') { |
|
return receiver.objectStoreNames[1] |
|
? undefined |
|
: receiver.objectStore(receiver.objectStoreNames[0]); |
|
} |
|
} |
|
// Else transform whatever we get back. |
|
return wrap(target[prop]); |
|
}, |
|
set(target, prop, value) { |
|
target[prop] = value; |
|
return true; |
|
}, |
|
has(target, prop) { |
|
if (target instanceof IDBTransaction && |
|
(prop === 'done' || prop === 'store')) { |
|
return true; |
|
} |
|
return prop in target; |
|
}, |
|
}; |
|
function replaceTraps(callback) { |
|
idbProxyTraps = callback(idbProxyTraps); |
|
} |
|
function wrapFunction(func) { |
|
// Due to expected object equality (which is enforced by the caching in `wrap`), we |
|
// only create one new func per func. |
|
// Edge doesn't support objectStoreNames (booo), so we polyfill it here. |
|
if (func === IDBDatabase.prototype.transaction && |
|
!('objectStoreNames' in IDBTransaction.prototype)) { |
|
return function (storeNames, ...args) { |
|
const tx = func.call(unwrap(this), storeNames, ...args); |
|
transactionStoreNamesMap.set(tx, storeNames.sort ? storeNames.sort() : [storeNames]); |
|
return wrap(tx); |
|
}; |
|
} |
|
// Cursor methods are special, as the behaviour is a little more different to standard IDB. In |
|
// IDB, you advance the cursor and wait for a new 'success' on the IDBRequest that gave you the |
|
// cursor. It's kinda like a promise that can resolve with many values. That doesn't make sense |
|
// with real promises, so each advance methods returns a new promise for the cursor object, or |
|
// undefined if the end of the cursor has been reached. |
|
if (getCursorAdvanceMethods().includes(func)) { |
|
return function (...args) { |
|
// Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use |
|
// the original object. |
|
func.apply(unwrap(this), args); |
|
return wrap(cursorRequestMap.get(this)); |
|
}; |
|
} |
|
return function (...args) { |
|
// Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use |
|
// the original object. |
|
return wrap(func.apply(unwrap(this), args)); |
|
}; |
|
} |
|
function transformCachableValue(value) { |
|
if (typeof value === 'function') |
|
return wrapFunction(value); |
|
// This doesn't return, it just creates a 'done' promise for the transaction, |
|
// which is later returned for transaction.done (see idbObjectHandler). |
|
if (value instanceof IDBTransaction) |
|
cacheDonePromiseForTransaction(value); |
|
if (instanceOfAny(value, getIdbProxyableTypes())) |
|
return new Proxy(value, idbProxyTraps); |
|
// Return the same value back if we're not going to transform it. |
|
return value; |
|
} |
|
function wrap(value) { |
|
// We sometimes generate multiple promises from a single IDBRequest (eg when cursoring), because |
|
// IDB is weird and a single IDBRequest can yield many responses, so these can't be cached. |
|
if (value instanceof IDBRequest) |
|
return promisifyRequest(value); |
|
// If we've already transformed this value before, reuse the transformed value. |
|
// This is faster, but it also provides object equality. |
|
if (transformCache.has(value)) |
|
return transformCache.get(value); |
|
const newValue = transformCachableValue(value); |
|
// Not all types are transformed. |
|
// These may be primitive types, so they can't be WeakMap keys. |
|
if (newValue !== value) { |
|
transformCache.set(value, newValue); |
|
reverseTransformCache.set(newValue, value); |
|
} |
|
return newValue; |
|
} |
|
const unwrap = (value) => reverseTransformCache.get(value); |
|
|
|
export { reverseTransformCache as a, instanceOfAny as i, replaceTraps as r, unwrap as u, wrap as w };
|
|
|