db.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. // TODO: This file was created by bulk-decaffeinate.
  2. // Sanity-check the conversion and remove this comment.
  3. /*
  4. * decaffeinate suggestions:
  5. * DS101: Remove unnecessary use of Array.from
  6. * DS102: Remove unnecessary code created because of implicit returns
  7. * DS205: Consider reworking code to avoid use of IIFEs
  8. * DS206: Consider reworking classes to avoid initClass
  9. * DS207: Consider shorter variations of null checks
  10. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
  11. */
  12. (function() {
  13. let NAME = undefined;
  14. let VERSION = undefined;
  15. const Cls = (app.DB = class DB {
  16. static initClass() {
  17. NAME = 'docs';
  18. VERSION = 15;
  19. }
  20. constructor() {
  21. this.onOpenSuccess = this.onOpenSuccess.bind(this);
  22. this.onOpenError = this.onOpenError.bind(this);
  23. this.checkForCorruptedDocs = this.checkForCorruptedDocs.bind(this);
  24. this.deleteCorruptedDocs = this.deleteCorruptedDocs.bind(this);
  25. this.versionMultipler = $.isIE() ? 1e5 : 1e9;
  26. this.useIndexedDB = this.useIndexedDB();
  27. this.callbacks = [];
  28. }
  29. db(fn) {
  30. if (!this.useIndexedDB) { return fn(); }
  31. if (fn) { this.callbacks.push(fn); }
  32. if (this.open) { return; }
  33. try {
  34. this.open = true;
  35. const req = indexedDB.open(NAME, (VERSION * this.versionMultipler) + this.userVersion());
  36. req.onsuccess = this.onOpenSuccess;
  37. req.onerror = this.onOpenError;
  38. req.onupgradeneeded = this.onUpgradeNeeded;
  39. } catch (error) {
  40. this.fail('exception', error);
  41. }
  42. }
  43. onOpenSuccess(event) {
  44. let error;
  45. const db = event.target.result;
  46. if (db.objectStoreNames.length === 0) {
  47. try { db.close(); } catch (error1) {}
  48. this.open = false;
  49. this.fail('empty');
  50. } else if (error = this.buggyIDB(db)) {
  51. try { db.close(); } catch (error2) {}
  52. this.open = false;
  53. this.fail('buggy', error);
  54. } else {
  55. this.runCallbacks(db);
  56. this.open = false;
  57. db.close();
  58. }
  59. }
  60. onOpenError(event) {
  61. event.preventDefault();
  62. this.open = false;
  63. const {
  64. error
  65. } = event.target;
  66. switch (error.name) {
  67. case 'QuotaExceededError':
  68. this.onQuotaExceededError();
  69. break;
  70. case 'VersionError':
  71. this.onVersionError();
  72. break;
  73. case 'InvalidStateError':
  74. this.fail('private_mode');
  75. break;
  76. default:
  77. this.fail('cant_open', error);
  78. }
  79. }
  80. fail(reason, error) {
  81. this.cachedDocs = null;
  82. this.useIndexedDB = false;
  83. if (!this.reason) { this.reason = reason; }
  84. if (!this.error) { this.error = error; }
  85. if (error) { if (typeof console.error === 'function') {
  86. console.error('IDB error', error);
  87. } }
  88. this.runCallbacks();
  89. if (error && (reason === 'cant_open')) {
  90. Raven.captureMessage(`${error.name}: ${error.message}`, {level: 'warning', fingerprint: [error.name]});
  91. }
  92. }
  93. onQuotaExceededError() {
  94. this.reset();
  95. this.db();
  96. app.onQuotaExceeded();
  97. Raven.captureMessage('QuotaExceededError', {level: 'warning'});
  98. }
  99. onVersionError() {
  100. const req = indexedDB.open(NAME);
  101. req.onsuccess = event => {
  102. return this.handleVersionMismatch(event.target.result.version);
  103. };
  104. req.onerror = function(event) {
  105. event.preventDefault();
  106. return this.fail('cant_open', error);
  107. };
  108. }
  109. handleVersionMismatch(actualVersion) {
  110. if (Math.floor(actualVersion / this.versionMultipler) !== VERSION) {
  111. this.fail('version');
  112. } else {
  113. this.setUserVersion(actualVersion - (VERSION * this.versionMultipler));
  114. this.db();
  115. }
  116. }
  117. buggyIDB(db) {
  118. if (this.checkedBuggyIDB) { return; }
  119. this.checkedBuggyIDB = true;
  120. try {
  121. this.idbTransaction(db, {stores: $.makeArray(db.objectStoreNames).slice(0, 2), mode: 'readwrite'}).abort(); // https://bugs.webkit.org/show_bug.cgi?id=136937
  122. return;
  123. } catch (error) {
  124. return error;
  125. }
  126. }
  127. runCallbacks(db) {
  128. let fn;
  129. while ((fn = this.callbacks.shift())) { fn(db); }
  130. }
  131. onUpgradeNeeded(event) {
  132. let db;
  133. if (!(db = event.target.result)) { return; }
  134. const objectStoreNames = $.makeArray(db.objectStoreNames);
  135. if (!$.arrayDelete(objectStoreNames, 'docs')) {
  136. try { db.createObjectStore('docs'); } catch (error) {}
  137. }
  138. for (var doc of Array.from(app.docs.all())) {
  139. if (!$.arrayDelete(objectStoreNames, doc.slug)) {
  140. try { db.createObjectStore(doc.slug); } catch (error1) {}
  141. }
  142. }
  143. for (var name of Array.from(objectStoreNames)) {
  144. try { db.deleteObjectStore(name); } catch (error2) {}
  145. }
  146. }
  147. store(doc, data, onSuccess, onError, _retry) {
  148. if (_retry == null) { _retry = true; }
  149. this.db(db => {
  150. if (!db) {
  151. onError();
  152. return;
  153. }
  154. const txn = this.idbTransaction(db, {stores: ['docs', doc.slug], mode: 'readwrite', ignoreError: false});
  155. txn.oncomplete = () => {
  156. if (this.cachedDocs != null) {
  157. this.cachedDocs[doc.slug] = doc.mtime;
  158. }
  159. onSuccess();
  160. };
  161. txn.onerror = event => {
  162. event.preventDefault();
  163. if (((txn.error != null ? txn.error.name : undefined) === 'NotFoundError') && _retry) {
  164. this.migrate();
  165. setTimeout(() => {
  166. return this.store(doc, data, onSuccess, onError, false);
  167. }
  168. , 0);
  169. } else {
  170. onError(event);
  171. }
  172. };
  173. let store = txn.objectStore(doc.slug);
  174. store.clear();
  175. for (var path in data) { var content = data[path]; store.add(content, path); }
  176. store = txn.objectStore('docs');
  177. store.put(doc.mtime, doc.slug);
  178. });
  179. }
  180. unstore(doc, onSuccess, onError, _retry) {
  181. if (_retry == null) { _retry = true; }
  182. this.db(db => {
  183. if (!db) {
  184. onError();
  185. return;
  186. }
  187. const txn = this.idbTransaction(db, {stores: ['docs', doc.slug], mode: 'readwrite', ignoreError: false});
  188. txn.oncomplete = () => {
  189. if (this.cachedDocs != null) {
  190. delete this.cachedDocs[doc.slug];
  191. }
  192. onSuccess();
  193. };
  194. txn.onerror = function(event) {
  195. event.preventDefault();
  196. if (((txn.error != null ? txn.error.name : undefined) === 'NotFoundError') && _retry) {
  197. this.migrate();
  198. setTimeout(() => {
  199. return this.unstore(doc, onSuccess, onError, false);
  200. }
  201. , 0);
  202. } else {
  203. onError(event);
  204. }
  205. };
  206. let store = txn.objectStore('docs');
  207. store.delete(doc.slug);
  208. store = txn.objectStore(doc.slug);
  209. store.clear();
  210. });
  211. }
  212. version(doc, fn) {
  213. let version;
  214. if ((version = this.cachedVersion(doc)) != null) {
  215. fn(version);
  216. return;
  217. }
  218. this.db(db => {
  219. if (!db) {
  220. fn(false);
  221. return;
  222. }
  223. const txn = this.idbTransaction(db, {stores: ['docs'], mode: 'readonly'});
  224. const store = txn.objectStore('docs');
  225. const req = store.get(doc.slug);
  226. req.onsuccess = function() {
  227. fn(req.result);
  228. };
  229. req.onerror = function(event) {
  230. event.preventDefault();
  231. fn(false);
  232. };
  233. });
  234. }
  235. cachedVersion(doc) {
  236. if (!this.cachedDocs) { return; }
  237. return this.cachedDocs[doc.slug] || false;
  238. }
  239. versions(docs, fn) {
  240. let versions;
  241. if (versions = this.cachedVersions(docs)) {
  242. fn(versions);
  243. return;
  244. }
  245. return this.db(db => {
  246. if (!db) {
  247. fn(false);
  248. return;
  249. }
  250. const txn = this.idbTransaction(db, {stores: ['docs'], mode: 'readonly'});
  251. txn.oncomplete = function() {
  252. fn(result);
  253. };
  254. const store = txn.objectStore('docs');
  255. var result = {};
  256. docs.forEach(function(doc) {
  257. const req = store.get(doc.slug);
  258. req.onsuccess = function() {
  259. result[doc.slug] = req.result;
  260. };
  261. req.onerror = function(event) {
  262. event.preventDefault();
  263. result[doc.slug] = false;
  264. };
  265. });
  266. });
  267. }
  268. cachedVersions(docs) {
  269. if (!this.cachedDocs) { return; }
  270. const result = {};
  271. for (var doc of Array.from(docs)) { result[doc.slug] = this.cachedVersion(doc); }
  272. return result;
  273. }
  274. load(entry, onSuccess, onError) {
  275. if (this.shouldLoadWithIDB(entry)) {
  276. onError = this.loadWithXHR.bind(this, entry, onSuccess, onError);
  277. return this.loadWithIDB(entry, onSuccess, onError);
  278. } else {
  279. return this.loadWithXHR(entry, onSuccess, onError);
  280. }
  281. }
  282. loadWithXHR(entry, onSuccess, onError) {
  283. return ajax({
  284. url: entry.fileUrl(),
  285. dataType: 'html',
  286. success: onSuccess,
  287. error: onError
  288. });
  289. }
  290. loadWithIDB(entry, onSuccess, onError) {
  291. return this.db(db => {
  292. if (!db) {
  293. onError();
  294. return;
  295. }
  296. if (!db.objectStoreNames.contains(entry.doc.slug)) {
  297. onError();
  298. this.loadDocsCache(db);
  299. return;
  300. }
  301. const txn = this.idbTransaction(db, {stores: [entry.doc.slug], mode: 'readonly'});
  302. const store = txn.objectStore(entry.doc.slug);
  303. const req = store.get(entry.dbPath());
  304. req.onsuccess = function() {
  305. if (req.result) { onSuccess(req.result); } else { onError(); }
  306. };
  307. req.onerror = function(event) {
  308. event.preventDefault();
  309. onError();
  310. };
  311. this.loadDocsCache(db);
  312. });
  313. }
  314. loadDocsCache(db) {
  315. if (this.cachedDocs) { return; }
  316. this.cachedDocs = {};
  317. const txn = this.idbTransaction(db, {stores: ['docs'], mode: 'readonly'});
  318. txn.oncomplete = () => {
  319. setTimeout(this.checkForCorruptedDocs, 50);
  320. };
  321. const req = txn.objectStore('docs').openCursor();
  322. req.onsuccess = event => {
  323. let cursor;
  324. if (!(cursor = event.target.result)) { return; }
  325. this.cachedDocs[cursor.key] = cursor.value;
  326. cursor.continue();
  327. };
  328. req.onerror = function(event) {
  329. event.preventDefault();
  330. };
  331. }
  332. checkForCorruptedDocs() {
  333. this.db(db => {
  334. let slug;
  335. this.corruptedDocs = [];
  336. const docs = ((() => {
  337. const result = [];
  338. for (var key in this.cachedDocs) {
  339. var value = this.cachedDocs[key];
  340. if (value) {
  341. result.push(key);
  342. }
  343. }
  344. return result;
  345. })());
  346. if (docs.length === 0) { return; }
  347. for (slug of Array.from(docs)) {
  348. if (!app.docs.findBy('slug', slug)) {
  349. this.corruptedDocs.push(slug);
  350. }
  351. }
  352. for (slug of Array.from(this.corruptedDocs)) {
  353. $.arrayDelete(docs, slug);
  354. }
  355. if (docs.length === 0) {
  356. setTimeout(this.deleteCorruptedDocs, 0);
  357. return;
  358. }
  359. const txn = this.idbTransaction(db, {stores: docs, mode: 'readonly', ignoreError: false});
  360. txn.oncomplete = () => {
  361. if (this.corruptedDocs.length > 0) { setTimeout(this.deleteCorruptedDocs, 0); }
  362. };
  363. for (var doc of Array.from(docs)) {
  364. txn.objectStore(doc).get('index').onsuccess = event => {
  365. if (!event.target.result) { this.corruptedDocs.push(event.target.source.name); }
  366. };
  367. }
  368. });
  369. }
  370. deleteCorruptedDocs() {
  371. this.db(db => {
  372. let doc;
  373. const txn = this.idbTransaction(db, {stores: ['docs'], mode: 'readwrite', ignoreError: false});
  374. const store = txn.objectStore('docs');
  375. while ((doc = this.corruptedDocs.pop())) {
  376. this.cachedDocs[doc] = false;
  377. store.delete(doc);
  378. }
  379. });
  380. Raven.captureMessage('corruptedDocs', {level: 'info', extra: { docs: this.corruptedDocs.join(',') }});
  381. }
  382. shouldLoadWithIDB(entry) {
  383. return this.useIndexedDB && (!this.cachedDocs || this.cachedDocs[entry.doc.slug]);
  384. }
  385. idbTransaction(db, options) {
  386. app.lastIDBTransaction = [options.stores, options.mode];
  387. const txn = db.transaction(options.stores, options.mode);
  388. if (options.ignoreError !== false) {
  389. txn.onerror = function(event) {
  390. event.preventDefault();
  391. };
  392. }
  393. if (options.ignoreAbort !== false) {
  394. txn.onabort = function(event) {
  395. event.preventDefault();
  396. };
  397. }
  398. return txn;
  399. }
  400. reset() {
  401. try { if (typeof indexedDB !== 'undefined' && indexedDB !== null) {
  402. indexedDB.deleteDatabase(NAME);
  403. } } catch (error) {}
  404. }
  405. useIndexedDB() {
  406. try {
  407. if (!app.isSingleDoc() && window.indexedDB) {
  408. return true;
  409. } else {
  410. this.reason = 'not_supported';
  411. return false;
  412. }
  413. } catch (error) {
  414. return false;
  415. }
  416. }
  417. migrate() {
  418. app.settings.set('schema', this.userVersion() + 1);
  419. }
  420. setUserVersion(version) {
  421. app.settings.set('schema', version);
  422. }
  423. userVersion() {
  424. return app.settings.get('schema');
  425. }
  426. });
  427. Cls.initClass();
  428. return Cls;
  429. })();