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.

490 lines
24 KiB

2 years ago
  1. (function ($, undefined) {
  2. 'use strict';
  3. var indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
  4. var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;
  5. var IDBCursor = window.IDBCursor || window.webkitIDBCursor || {};
  6. if (typeof IDBCursor.PREV === "undefined") {
  7. IDBCursor.PREV = "prev";
  8. }
  9. if (typeof IDBCursor.NEXT === "undefined") {
  10. IDBCursor.NEXT = "next";
  11. }
  12. /**
  13. * Best to use the constant IDBTransaction since older version support numeric types while the latest spec
  14. * supports strings
  15. */
  16. var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;
  17. function getDefaultTransaction(mode) {
  18. var result = null;
  19. switch (mode) {
  20. case 0:
  21. case 1:
  22. case "readwrite":
  23. case "readonly":
  24. result = mode;
  25. break;
  26. default:
  27. result = IDBTransaction.READ_WRITE || "readwrite";
  28. }
  29. return result;
  30. }
  31. $.extend({
  32. /**
  33. * The IndexedDB object used to open databases
  34. * @param {Object} dbName - name of the database
  35. * @param {Object} config - version, onupgradeneeded, onversionchange, schema
  36. */
  37. "indexedDB": function (dbName, config) {
  38. if (config) {
  39. // Parse the config argument
  40. if (typeof config === "number") config = {
  41. "version": config
  42. };
  43. var version = config.version;
  44. if (config.schema && !version) {
  45. var max = -1;
  46. for (var key in config.schema) {
  47. max = max > key ? max : key;
  48. }
  49. version = config.version || max;
  50. }
  51. }
  52. var wrap = {
  53. "request": function (req, args) {
  54. return $.Deferred(function (dfd) {
  55. try {
  56. var idbRequest = typeof req === "function" ? req(args) : req;
  57. idbRequest.onsuccess = function (e) {
  58. dfd.resolveWith(idbRequest, [idbRequest.result, e]);
  59. };
  60. idbRequest.onerror = function (e) {
  61. dfd.rejectWith(idbRequest, [idbRequest.error, e]);
  62. };
  63. if (typeof idbRequest.onblocked !== "undefined" && idbRequest.onblocked === null) {
  64. idbRequest.onblocked = function (e) {
  65. var res;
  66. try {
  67. res = idbRequest.result;
  68. } catch (e) {
  69. res = null; // Required for Older Chrome versions, accessing result causes error
  70. }
  71. dfd.notifyWith(idbRequest, [res, e]);
  72. };
  73. }
  74. if (typeof idbRequest.onupgradeneeded !== "undefined" && idbRequest.onupgradeneeded === null) {
  75. idbRequest.onupgradeneeded = function (e) {
  76. dfd.notifyWith(idbRequest, [idbRequest.result, e]);
  77. };
  78. }
  79. } catch (e) {
  80. e.name = "exception";
  81. dfd.rejectWith(idbRequest, ["exception", e]);
  82. }
  83. });
  84. },
  85. // Wraps the IDBTransaction to return promises, and other dependent methods
  86. "transaction": function (idbTransaction) {
  87. return {
  88. "objectStore": function (storeName) {
  89. try {
  90. return wrap.objectStore(idbTransaction.objectStore(storeName));
  91. } catch (e) {
  92. idbTransaction.readyState !== idbTransaction.DONE && idbTransaction.abort();
  93. return wrap.objectStore(null);
  94. }
  95. },
  96. "createObjectStore": function (storeName, storeParams) {
  97. try {
  98. return wrap.objectStore(idbTransaction.db.createObjectStore(storeName, storeParams));
  99. } catch (e) {
  100. idbTransaction.readyState !== idbTransaction.DONE && idbTransaction.abort();
  101. }
  102. },
  103. "deleteObjectStore": function (storeName) {
  104. try {
  105. idbTransaction.db.deleteObjectStore(storeName);
  106. } catch (e) {
  107. idbTransaction.readyState !== idbTransaction.DONE && idbTransaction.abort();
  108. }
  109. },
  110. "abort": function () {
  111. idbTransaction.abort();
  112. }
  113. };
  114. },
  115. "objectStore": function (idbObjectStore) {
  116. var result = {};
  117. // Define CRUD operations
  118. var crudOps = ["add", "put", "get", "delete", "clear", "count"];
  119. for (var i = 0; i < crudOps.length; i++) {
  120. result[crudOps[i]] = (function (op) {
  121. return function () {
  122. return wrap.request(function (args) {
  123. return idbObjectStore[op].apply(idbObjectStore, args);
  124. }, arguments);
  125. };
  126. })(crudOps[i]);
  127. }
  128. result.each = function (callback, range, direction) {
  129. return wrap.cursor(function () {
  130. if (direction) {
  131. return idbObjectStore.openCursor(wrap.range(range), direction);
  132. } else {
  133. return idbObjectStore.openCursor(wrap.range(range));
  134. }
  135. }, callback);
  136. };
  137. result.index = function (name) {
  138. return wrap.index(function () {
  139. return idbObjectStore.index(name);
  140. });
  141. };
  142. result.createIndex = function (prop, options, indexName) {
  143. if (arguments.length === 2 && typeof options === "string") {
  144. indexName = arguments[1];
  145. options = null;
  146. }
  147. if (!indexName) {
  148. indexName = prop;
  149. }
  150. return wrap.index(function () {
  151. return idbObjectStore.createIndex(indexName, prop, options);
  152. });
  153. };
  154. result.deleteIndex = function (indexName) {
  155. return idbObjectStore.deleteIndex(indexName);
  156. };
  157. return result;
  158. },
  159. "range": function (r) {
  160. if ($.isArray(r)) {
  161. if (r.length === 1) {
  162. return IDBKeyRange.only(r[0]);
  163. } else {
  164. return IDBKeyRange.bound(r[0], r[1], (typeof r[2] === 'undefined') ? false : r[2], (typeof r[3] === 'undefined') ? false : r[3]);
  165. }
  166. } else if (typeof r === "undefined") {
  167. return null;
  168. } else {
  169. return r;
  170. }
  171. },
  172. "cursor": function (idbCursor, callback) {
  173. return $.Deferred(function (dfd) {
  174. try {
  175. var cursorReq = typeof idbCursor === "function" ? idbCursor() : idbCursor;
  176. cursorReq.onsuccess = function (e) {
  177. if (!cursorReq.result) {
  178. dfd.resolveWith(cursorReq, [null, e]);
  179. return;
  180. }
  181. var elem = {
  182. // Delete, update do not move
  183. "delete": function () {
  184. return wrap.request(function () {
  185. return cursorReq.result["delete"]();
  186. });
  187. },
  188. "update": function (data) {
  189. return wrap.request(function () {
  190. return cursorReq.result["update"](data);
  191. });
  192. },
  193. "next": function (key) {
  194. this.data = key;
  195. },
  196. "key": cursorReq.result.key,
  197. "value": cursorReq.result.value
  198. };
  199. dfd.notifyWith(cursorReq, [elem, e]);
  200. var result = callback.apply(cursorReq, [elem]);
  201. try {
  202. if (result === false) {
  203. dfd.resolveWith(cursorReq, [null, e]);
  204. } else if (typeof result === "number") {
  205. cursorReq.result["advance"].apply(cursorReq.result, [result]);
  206. } else {
  207. if (elem.data) cursorReq.result["continue"].apply(cursorReq.result, [elem.data]);
  208. else cursorReq.result["continue"]();
  209. }
  210. } catch (e) {
  211. dfd.rejectWith(cursorReq, [cursorReq.result, e]);
  212. }
  213. };
  214. cursorReq.onerror = function (e) {
  215. dfd.rejectWith(cursorReq, [cursorReq.result, e]);
  216. };
  217. } catch (e) {
  218. e.type = "exception";
  219. dfd.rejectWith(cursorReq, [null, e]);
  220. }
  221. });
  222. },
  223. "index": function (index) {
  224. try {
  225. var idbIndex = (typeof index === "function" ? index() : index);
  226. } catch (e) {
  227. idbIndex = null;
  228. }
  229. return {
  230. "each": function (callback, range, direction) {
  231. return wrap.cursor(function () {
  232. if (direction) {
  233. return idbIndex.openCursor(wrap.range(range), direction);
  234. } else {
  235. return idbIndex.openCursor(wrap.range(range));
  236. }
  237. }, callback);
  238. },
  239. "eachKey": function (callback, range, direction) {
  240. return wrap.cursor(function () {
  241. if (direction) {
  242. return idbIndex.openKeyCursor(wrap.range(range), direction);
  243. } else {
  244. return idbIndex.openKeyCursor(wrap.range(range));
  245. }
  246. }, callback);
  247. },
  248. "get": function (key) {
  249. if (typeof idbIndex.get === "function") {
  250. return wrap.request(idbIndex.get(key));
  251. } else {
  252. return idbIndex.openCursor(wrap.range(key));
  253. }
  254. },
  255. "count": function () {
  256. if (typeof idbIndex.count === "function") {
  257. return wrap.request(idbIndex.count());
  258. } else {
  259. throw "Count not implemented for cursors";
  260. }
  261. },
  262. "getKey": function (key) {
  263. if (typeof idbIndex.getKey === "function") {
  264. return wrap.request(idbIndex.getKey(key));
  265. } else {
  266. return idbIndex.openKeyCursor(wrap.range(key));
  267. }
  268. }
  269. };
  270. }
  271. };
  272. // Start with opening the database
  273. var dbPromise = wrap.request(function () {
  274. return version ? indexedDB.open(dbName, parseInt(version)) : indexedDB.open(dbName);
  275. });
  276. dbPromise.then(function (db, e) {
  277. db.onversionchange = function () {
  278. // Try to automatically close the database if there is a version change request
  279. if (!(config && config.onversionchange && config.onversionchange() !== false)) {
  280. db.close();
  281. }
  282. };
  283. }, function (error, e) {
  284. // Nothing much to do if an error occurs
  285. }, function (db, e) {
  286. if (e && e.type === "upgradeneeded") {
  287. if (config && config.schema) {
  288. // Assuming that version is always an integer
  289. for (var i = e.oldVersion + 1; i <= e.newVersion; i++) {
  290. typeof config.schema[i] === "function" && config.schema[i].call(this, wrap.transaction(this.transaction));
  291. }
  292. }
  293. if (config && typeof config.upgrade === "function") {
  294. config.upgrade.call(this, wrap.transaction(this.transaction));
  295. }
  296. }
  297. });
  298. return $.extend(dbPromise, {
  299. "cmp": function (key1, key2) {
  300. return indexedDB.cmp(key1, key2);
  301. },
  302. "deleteDatabase": function () {
  303. // Kinda looks ugly coz DB is opened before it needs to be deleted.
  304. // Blame it on the API
  305. return $.Deferred(function (dfd) {
  306. dbPromise.then(function (db, e) {
  307. db.close();
  308. wrap.request(function () {
  309. return indexedDB.deleteDatabase(dbName);
  310. }).then(function (result, e) {
  311. dfd.resolveWith(this, [result, e]);
  312. }, function (error, e) {
  313. dfd.rejectWith(this, [error, e]);
  314. }, function (db, e) {
  315. dfd.notifyWith(this, [db, e]);
  316. });
  317. }, function (error, e) {
  318. dfd.rejectWith(this, [error, e]);
  319. }, function (db, e) {
  320. dfd.notifyWith(this, [db, e]);
  321. });
  322. });
  323. },
  324. "transaction": function (storeNames, mode) {
  325. !$.isArray(storeNames) && (storeNames = [storeNames]);
  326. mode = getDefaultTransaction(mode);
  327. return $.Deferred(function (dfd) {
  328. dbPromise.then(function (db, e) {
  329. var idbTransaction;
  330. try {
  331. idbTransaction = db.transaction(storeNames, mode);
  332. idbTransaction.onabort = idbTransaction.onerror = function (e) {
  333. dfd.rejectWith(idbTransaction, [e]);
  334. };
  335. idbTransaction.oncomplete = function (e) {
  336. dfd.resolveWith(idbTransaction, [e]);
  337. };
  338. } catch (e) {
  339. e.type = "exception";
  340. dfd.rejectWith(this, [e]);
  341. return;
  342. }
  343. try {
  344. dfd.notifyWith(idbTransaction, [wrap.transaction(idbTransaction)]);
  345. } catch (e) {
  346. e.type = "exception";
  347. dfd.rejectWith(this, [e]);
  348. }
  349. }, function (err, e) {
  350. dfd.rejectWith(this, [e, err]);
  351. }, function (res, e) {
  352. //dfd.notifyWith(this, ["", e]);
  353. });
  354. });
  355. },
  356. "objectStore": function (storeName, mode) {
  357. var me = this,
  358. result = {};
  359. function op(callback) {
  360. return $.Deferred(function (dfd) {
  361. function onTransactionProgress(trans, callback) {
  362. try {
  363. callback(trans.objectStore(storeName)).then(function (result, e) {
  364. dfd.resolveWith(this, [result, e]);
  365. }, function (err, e) {
  366. dfd.rejectWith(this, [err, e]);
  367. });
  368. } catch (e) {
  369. e.name = "exception";
  370. dfd.rejectWith(trans, [e, e]);
  371. }
  372. }
  373. me.transaction(storeName, getDefaultTransaction(mode)).then(function () {
  374. // Nothing to do when transaction is complete
  375. }, function (err, e) {
  376. // If transaction fails, CrudOp fails
  377. if (err.code === err.NOT_FOUND_ERR && (mode === true || typeof mode === "object")) {
  378. var db = this.result;
  379. db.close();
  380. dbPromise = wrap.request(function () {
  381. return indexedDB.open(dbName, (parseInt(db.version, 10) || 1) + 1);
  382. });
  383. dbPromise.then(function (db, e) {
  384. db.onversionchange = function () {
  385. // Try to automatically close the database if there is a version change request
  386. if (!(config && config.onversionchange && config.onversionchange() !== false)) {
  387. db.close();
  388. }
  389. };
  390. me.transaction(storeName, getDefaultTransaction(mode)).then(function () {
  391. // Nothing much to do
  392. }, function (err, e) {
  393. dfd.rejectWith(this, [err, e]);
  394. }, function (trans, e) {
  395. onTransactionProgress(trans, callback);
  396. });
  397. }, function (err, e) {
  398. dfd.rejectWith(this, [err, e]);
  399. }, function (db, e) {
  400. if (e.type === "upgradeneeded") {
  401. try {
  402. db.createObjectStore(storeName, mode === true ? {
  403. "autoIncrement": true
  404. } : mode);
  405. } catch (ex) {
  406. dfd.rejectWith(this, [ex, e]);
  407. }
  408. }
  409. });
  410. } else {
  411. dfd.rejectWith(this, [err, e]);
  412. }
  413. }, function (trans) {
  414. onTransactionProgress(trans, callback);
  415. });
  416. });
  417. }
  418. function crudOp(opName, args) {
  419. return op(function (wrappedObjectStore) {
  420. return wrappedObjectStore[opName].apply(wrappedObjectStore, args);
  421. });
  422. }
  423. function indexOp(opName, indexName, args) {
  424. return op(function (wrappedObjectStore) {
  425. var index = wrappedObjectStore.index(indexName);
  426. return index[opName].apply(index[opName], args);
  427. });
  428. }
  429. var crud = ["add", "delete", "get", "put", "clear", "count", "each"];
  430. for (var i = 0; i < crud.length; i++) {
  431. result[crud[i]] = (function (op) {
  432. return function () {
  433. return crudOp(op, arguments);
  434. };
  435. })(crud[i]);
  436. }
  437. result.index = function (indexName) {
  438. return {
  439. "each": function (callback, range, direction) {
  440. return indexOp("each", indexName, [callback, range, direction]);
  441. },
  442. "eachKey": function (callback, range, direction) {
  443. return indexOp("eachKey", indexName, [callback, range, direction]);
  444. },
  445. "get": function (key) {
  446. return indexOp("get", indexName, [key]);
  447. },
  448. "count": function () {
  449. return indexOp("count", indexName, []);
  450. },
  451. "getKey": function (key) {
  452. return indexOp("getKey", indexName, [key]);
  453. }
  454. };
  455. };
  456. return result;
  457. }
  458. });
  459. }
  460. });
  461. $.indexedDB.IDBCursor = IDBCursor;
  462. $.indexedDB.IDBTransaction = IDBTransaction;
  463. $.idb = $.indexedDB;
  464. })(jQuery);