Ei kuvausta
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.

wrap-idb-value.js 7.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. const instanceOfAny = (object, constructors) => constructors.some((c) => object instanceof c);
  2. let idbProxyableTypes;
  3. let cursorAdvanceMethods;
  4. // This is a function to prevent it throwing up in node environments.
  5. function getIdbProxyableTypes() {
  6. return (idbProxyableTypes ||
  7. (idbProxyableTypes = [
  8. IDBDatabase,
  9. IDBObjectStore,
  10. IDBIndex,
  11. IDBCursor,
  12. IDBTransaction,
  13. ]));
  14. }
  15. // This is a function to prevent it throwing up in node environments.
  16. function getCursorAdvanceMethods() {
  17. return (cursorAdvanceMethods ||
  18. (cursorAdvanceMethods = [
  19. IDBCursor.prototype.advance,
  20. IDBCursor.prototype.continue,
  21. IDBCursor.prototype.continuePrimaryKey,
  22. ]));
  23. }
  24. const cursorRequestMap = new WeakMap();
  25. const transactionDoneMap = new WeakMap();
  26. const transactionStoreNamesMap = new WeakMap();
  27. const transformCache = new WeakMap();
  28. const reverseTransformCache = new WeakMap();
  29. function promisifyRequest(request) {
  30. const promise = new Promise((resolve, reject) => {
  31. const unlisten = () => {
  32. request.removeEventListener('success', success);
  33. request.removeEventListener('error', error);
  34. };
  35. const success = () => {
  36. resolve(wrap(request.result));
  37. unlisten();
  38. };
  39. const error = () => {
  40. reject(request.error);
  41. unlisten();
  42. };
  43. request.addEventListener('success', success);
  44. request.addEventListener('error', error);
  45. });
  46. promise
  47. .then((value) => {
  48. // Since cursoring reuses the IDBRequest (*sigh*), we cache it for later retrieval
  49. // (see wrapFunction).
  50. if (value instanceof IDBCursor) {
  51. cursorRequestMap.set(value, request);
  52. }
  53. // Catching to avoid "Uncaught Promise exceptions"
  54. })
  55. .catch(() => { });
  56. // This mapping exists in reverseTransformCache but doesn't doesn't exist in transformCache. This
  57. // is because we create many promises from a single IDBRequest.
  58. reverseTransformCache.set(promise, request);
  59. return promise;
  60. }
  61. function cacheDonePromiseForTransaction(tx) {
  62. // Early bail if we've already created a done promise for this transaction.
  63. if (transactionDoneMap.has(tx))
  64. return;
  65. const done = new Promise((resolve, reject) => {
  66. const unlisten = () => {
  67. tx.removeEventListener('complete', complete);
  68. tx.removeEventListener('error', error);
  69. tx.removeEventListener('abort', error);
  70. };
  71. const complete = () => {
  72. resolve();
  73. unlisten();
  74. };
  75. const error = () => {
  76. reject(tx.error || new DOMException('AbortError', 'AbortError'));
  77. unlisten();
  78. };
  79. tx.addEventListener('complete', complete);
  80. tx.addEventListener('error', error);
  81. tx.addEventListener('abort', error);
  82. });
  83. // Cache it for later retrieval.
  84. transactionDoneMap.set(tx, done);
  85. }
  86. let idbProxyTraps = {
  87. get(target, prop, receiver) {
  88. if (target instanceof IDBTransaction) {
  89. // Special handling for transaction.done.
  90. if (prop === 'done')
  91. return transactionDoneMap.get(target);
  92. // Polyfill for objectStoreNames because of Edge.
  93. if (prop === 'objectStoreNames') {
  94. return target.objectStoreNames || transactionStoreNamesMap.get(target);
  95. }
  96. // Make tx.store return the only store in the transaction, or undefined if there are many.
  97. if (prop === 'store') {
  98. return receiver.objectStoreNames[1]
  99. ? undefined
  100. : receiver.objectStore(receiver.objectStoreNames[0]);
  101. }
  102. }
  103. // Else transform whatever we get back.
  104. return wrap(target[prop]);
  105. },
  106. set(target, prop, value) {
  107. target[prop] = value;
  108. return true;
  109. },
  110. has(target, prop) {
  111. if (target instanceof IDBTransaction &&
  112. (prop === 'done' || prop === 'store')) {
  113. return true;
  114. }
  115. return prop in target;
  116. },
  117. };
  118. function replaceTraps(callback) {
  119. idbProxyTraps = callback(idbProxyTraps);
  120. }
  121. function wrapFunction(func) {
  122. // Due to expected object equality (which is enforced by the caching in `wrap`), we
  123. // only create one new func per func.
  124. // Edge doesn't support objectStoreNames (booo), so we polyfill it here.
  125. if (func === IDBDatabase.prototype.transaction &&
  126. !('objectStoreNames' in IDBTransaction.prototype)) {
  127. return function (storeNames, ...args) {
  128. const tx = func.call(unwrap(this), storeNames, ...args);
  129. transactionStoreNamesMap.set(tx, storeNames.sort ? storeNames.sort() : [storeNames]);
  130. return wrap(tx);
  131. };
  132. }
  133. // Cursor methods are special, as the behaviour is a little more different to standard IDB. In
  134. // IDB, you advance the cursor and wait for a new 'success' on the IDBRequest that gave you the
  135. // cursor. It's kinda like a promise that can resolve with many values. That doesn't make sense
  136. // with real promises, so each advance methods returns a new promise for the cursor object, or
  137. // undefined if the end of the cursor has been reached.
  138. if (getCursorAdvanceMethods().includes(func)) {
  139. return function (...args) {
  140. // Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use
  141. // the original object.
  142. func.apply(unwrap(this), args);
  143. return wrap(cursorRequestMap.get(this));
  144. };
  145. }
  146. return function (...args) {
  147. // Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use
  148. // the original object.
  149. return wrap(func.apply(unwrap(this), args));
  150. };
  151. }
  152. function transformCachableValue(value) {
  153. if (typeof value === 'function')
  154. return wrapFunction(value);
  155. // This doesn't return, it just creates a 'done' promise for the transaction,
  156. // which is later returned for transaction.done (see idbObjectHandler).
  157. if (value instanceof IDBTransaction)
  158. cacheDonePromiseForTransaction(value);
  159. if (instanceOfAny(value, getIdbProxyableTypes()))
  160. return new Proxy(value, idbProxyTraps);
  161. // Return the same value back if we're not going to transform it.
  162. return value;
  163. }
  164. function wrap(value) {
  165. // We sometimes generate multiple promises from a single IDBRequest (eg when cursoring), because
  166. // IDB is weird and a single IDBRequest can yield many responses, so these can't be cached.
  167. if (value instanceof IDBRequest)
  168. return promisifyRequest(value);
  169. // If we've already transformed this value before, reuse the transformed value.
  170. // This is faster, but it also provides object equality.
  171. if (transformCache.has(value))
  172. return transformCache.get(value);
  173. const newValue = transformCachableValue(value);
  174. // Not all types are transformed.
  175. // These may be primitive types, so they can't be WeakMap keys.
  176. if (newValue !== value) {
  177. transformCache.set(value, newValue);
  178. reverseTransformCache.set(newValue, value);
  179. }
  180. return newValue;
  181. }
  182. const unwrap = (value) => reverseTransformCache.get(value);
  183. export { reverseTransformCache as a, instanceOfAny as i, replaceTraps as r, unwrap as u, wrap as w };