db.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  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. 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) {
  31. return fn();
  32. }
  33. if (fn) {
  34. this.callbacks.push(fn);
  35. }
  36. if (this.open) {
  37. return;
  38. }
  39. try {
  40. this.open = true;
  41. const req = indexedDB.open(
  42. NAME,
  43. VERSION * this.versionMultipler + this.userVersion(),
  44. );
  45. req.onsuccess = this.onOpenSuccess;
  46. req.onerror = this.onOpenError;
  47. req.onupgradeneeded = this.onUpgradeNeeded;
  48. } catch (error) {
  49. this.fail("exception", error);
  50. }
  51. }
  52. onOpenSuccess(event) {
  53. let error;
  54. const db = event.target.result;
  55. if (db.objectStoreNames.length === 0) {
  56. try {
  57. db.close();
  58. } catch (error1) {}
  59. this.open = false;
  60. this.fail("empty");
  61. } else if ((error = this.buggyIDB(db))) {
  62. try {
  63. db.close();
  64. } catch (error2) {}
  65. this.open = false;
  66. this.fail("buggy", error);
  67. } else {
  68. this.runCallbacks(db);
  69. this.open = false;
  70. db.close();
  71. }
  72. }
  73. onOpenError(event) {
  74. event.preventDefault();
  75. this.open = false;
  76. const { error } = event.target;
  77. switch (error.name) {
  78. case "QuotaExceededError":
  79. this.onQuotaExceededError();
  80. break;
  81. case "VersionError":
  82. this.onVersionError();
  83. break;
  84. case "InvalidStateError":
  85. this.fail("private_mode");
  86. break;
  87. default:
  88. this.fail("cant_open", error);
  89. }
  90. }
  91. fail(reason, error) {
  92. this.cachedDocs = null;
  93. this.useIndexedDB = false;
  94. if (!this.reason) {
  95. this.reason = reason;
  96. }
  97. if (!this.error) {
  98. this.error = error;
  99. }
  100. if (error) {
  101. if (typeof console.error === "function") {
  102. console.error("IDB error", error);
  103. }
  104. }
  105. this.runCallbacks();
  106. if (error && reason === "cant_open") {
  107. Raven.captureMessage(`${error.name}: ${error.message}`, {
  108. level: "warning",
  109. fingerprint: [error.name],
  110. });
  111. }
  112. }
  113. onQuotaExceededError() {
  114. this.reset();
  115. this.db();
  116. app.onQuotaExceeded();
  117. Raven.captureMessage("QuotaExceededError", { level: "warning" });
  118. }
  119. onVersionError() {
  120. const req = indexedDB.open(NAME);
  121. req.onsuccess = (event) => {
  122. return this.handleVersionMismatch(event.target.result.version);
  123. };
  124. req.onerror = function (event) {
  125. event.preventDefault();
  126. return this.fail("cant_open", error);
  127. };
  128. }
  129. handleVersionMismatch(actualVersion) {
  130. if (Math.floor(actualVersion / this.versionMultipler) !== VERSION) {
  131. this.fail("version");
  132. } else {
  133. this.setUserVersion(actualVersion - VERSION * this.versionMultipler);
  134. this.db();
  135. }
  136. }
  137. buggyIDB(db) {
  138. if (this.checkedBuggyIDB) {
  139. return;
  140. }
  141. this.checkedBuggyIDB = true;
  142. try {
  143. this.idbTransaction(db, {
  144. stores: $.makeArray(db.objectStoreNames).slice(0, 2),
  145. mode: "readwrite",
  146. }).abort(); // https://bugs.webkit.org/show_bug.cgi?id=136937
  147. return;
  148. } catch (error) {
  149. return error;
  150. }
  151. }
  152. runCallbacks(db) {
  153. let fn;
  154. while ((fn = this.callbacks.shift())) {
  155. fn(db);
  156. }
  157. }
  158. onUpgradeNeeded(event) {
  159. let db;
  160. if (!(db = event.target.result)) {
  161. return;
  162. }
  163. const objectStoreNames = $.makeArray(db.objectStoreNames);
  164. if (!$.arrayDelete(objectStoreNames, "docs")) {
  165. try {
  166. db.createObjectStore("docs");
  167. } catch (error) {}
  168. }
  169. for (var doc of Array.from(app.docs.all())) {
  170. if (!$.arrayDelete(objectStoreNames, doc.slug)) {
  171. try {
  172. db.createObjectStore(doc.slug);
  173. } catch (error1) {}
  174. }
  175. }
  176. for (var name of Array.from(objectStoreNames)) {
  177. try {
  178. db.deleteObjectStore(name);
  179. } catch (error2) {}
  180. }
  181. }
  182. store(doc, data, onSuccess, onError, _retry) {
  183. if (_retry == null) {
  184. _retry = true;
  185. }
  186. this.db((db) => {
  187. if (!db) {
  188. onError();
  189. return;
  190. }
  191. const txn = this.idbTransaction(db, {
  192. stores: ["docs", doc.slug],
  193. mode: "readwrite",
  194. ignoreError: false,
  195. });
  196. txn.oncomplete = () => {
  197. if (this.cachedDocs != null) {
  198. this.cachedDocs[doc.slug] = doc.mtime;
  199. }
  200. onSuccess();
  201. };
  202. txn.onerror = (event) => {
  203. event.preventDefault();
  204. if (
  205. (txn.error != null ? txn.error.name : undefined) ===
  206. "NotFoundError" &&
  207. _retry
  208. ) {
  209. this.migrate();
  210. setTimeout(() => {
  211. return this.store(doc, data, onSuccess, onError, false);
  212. }, 0);
  213. } else {
  214. onError(event);
  215. }
  216. };
  217. let store = txn.objectStore(doc.slug);
  218. store.clear();
  219. for (var path in data) {
  220. var content = data[path];
  221. store.add(content, path);
  222. }
  223. store = txn.objectStore("docs");
  224. store.put(doc.mtime, doc.slug);
  225. });
  226. }
  227. unstore(doc, onSuccess, onError, _retry) {
  228. if (_retry == null) {
  229. _retry = true;
  230. }
  231. this.db((db) => {
  232. if (!db) {
  233. onError();
  234. return;
  235. }
  236. const txn = this.idbTransaction(db, {
  237. stores: ["docs", doc.slug],
  238. mode: "readwrite",
  239. ignoreError: false,
  240. });
  241. txn.oncomplete = () => {
  242. if (this.cachedDocs != null) {
  243. delete this.cachedDocs[doc.slug];
  244. }
  245. onSuccess();
  246. };
  247. txn.onerror = function (event) {
  248. event.preventDefault();
  249. if (
  250. (txn.error != null ? txn.error.name : undefined) ===
  251. "NotFoundError" &&
  252. _retry
  253. ) {
  254. this.migrate();
  255. setTimeout(() => {
  256. return this.unstore(doc, onSuccess, onError, false);
  257. }, 0);
  258. } else {
  259. onError(event);
  260. }
  261. };
  262. let store = txn.objectStore("docs");
  263. store.delete(doc.slug);
  264. store = txn.objectStore(doc.slug);
  265. store.clear();
  266. });
  267. }
  268. version(doc, fn) {
  269. let version;
  270. if ((version = this.cachedVersion(doc)) != null) {
  271. fn(version);
  272. return;
  273. }
  274. this.db((db) => {
  275. if (!db) {
  276. fn(false);
  277. return;
  278. }
  279. const txn = this.idbTransaction(db, {
  280. stores: ["docs"],
  281. mode: "readonly",
  282. });
  283. const store = txn.objectStore("docs");
  284. const req = store.get(doc.slug);
  285. req.onsuccess = function () {
  286. fn(req.result);
  287. };
  288. req.onerror = function (event) {
  289. event.preventDefault();
  290. fn(false);
  291. };
  292. });
  293. }
  294. cachedVersion(doc) {
  295. if (!this.cachedDocs) {
  296. return;
  297. }
  298. return this.cachedDocs[doc.slug] || false;
  299. }
  300. versions(docs, fn) {
  301. let versions;
  302. if ((versions = this.cachedVersions(docs))) {
  303. fn(versions);
  304. return;
  305. }
  306. return this.db((db) => {
  307. if (!db) {
  308. fn(false);
  309. return;
  310. }
  311. const txn = this.idbTransaction(db, {
  312. stores: ["docs"],
  313. mode: "readonly",
  314. });
  315. txn.oncomplete = function () {
  316. fn(result);
  317. };
  318. const store = txn.objectStore("docs");
  319. var result = {};
  320. docs.forEach(function (doc) {
  321. const req = store.get(doc.slug);
  322. req.onsuccess = function () {
  323. result[doc.slug] = req.result;
  324. };
  325. req.onerror = function (event) {
  326. event.preventDefault();
  327. result[doc.slug] = false;
  328. };
  329. });
  330. });
  331. }
  332. cachedVersions(docs) {
  333. if (!this.cachedDocs) {
  334. return;
  335. }
  336. const result = {};
  337. for (var doc of Array.from(docs)) {
  338. result[doc.slug] = this.cachedVersion(doc);
  339. }
  340. return result;
  341. }
  342. load(entry, onSuccess, onError) {
  343. if (this.shouldLoadWithIDB(entry)) {
  344. onError = this.loadWithXHR.bind(this, entry, onSuccess, onError);
  345. return this.loadWithIDB(entry, onSuccess, onError);
  346. } else {
  347. return this.loadWithXHR(entry, onSuccess, onError);
  348. }
  349. }
  350. loadWithXHR(entry, onSuccess, onError) {
  351. return ajax({
  352. url: entry.fileUrl(),
  353. dataType: "html",
  354. success: onSuccess,
  355. error: onError,
  356. });
  357. }
  358. loadWithIDB(entry, onSuccess, onError) {
  359. return this.db((db) => {
  360. if (!db) {
  361. onError();
  362. return;
  363. }
  364. if (!db.objectStoreNames.contains(entry.doc.slug)) {
  365. onError();
  366. this.loadDocsCache(db);
  367. return;
  368. }
  369. const txn = this.idbTransaction(db, {
  370. stores: [entry.doc.slug],
  371. mode: "readonly",
  372. });
  373. const store = txn.objectStore(entry.doc.slug);
  374. const req = store.get(entry.dbPath());
  375. req.onsuccess = function () {
  376. if (req.result) {
  377. onSuccess(req.result);
  378. } else {
  379. onError();
  380. }
  381. };
  382. req.onerror = function (event) {
  383. event.preventDefault();
  384. onError();
  385. };
  386. this.loadDocsCache(db);
  387. });
  388. }
  389. loadDocsCache(db) {
  390. if (this.cachedDocs) {
  391. return;
  392. }
  393. this.cachedDocs = {};
  394. const txn = this.idbTransaction(db, {
  395. stores: ["docs"],
  396. mode: "readonly",
  397. });
  398. txn.oncomplete = () => {
  399. setTimeout(this.checkForCorruptedDocs, 50);
  400. };
  401. const req = txn.objectStore("docs").openCursor();
  402. req.onsuccess = (event) => {
  403. let cursor;
  404. if (!(cursor = event.target.result)) {
  405. return;
  406. }
  407. this.cachedDocs[cursor.key] = cursor.value;
  408. cursor.continue();
  409. };
  410. req.onerror = function (event) {
  411. event.preventDefault();
  412. };
  413. }
  414. checkForCorruptedDocs() {
  415. this.db((db) => {
  416. let slug;
  417. this.corruptedDocs = [];
  418. const docs = (() => {
  419. const result = [];
  420. for (var key in this.cachedDocs) {
  421. var value = this.cachedDocs[key];
  422. if (value) {
  423. result.push(key);
  424. }
  425. }
  426. return result;
  427. })();
  428. if (docs.length === 0) {
  429. return;
  430. }
  431. for (slug of Array.from(docs)) {
  432. if (!app.docs.findBy("slug", slug)) {
  433. this.corruptedDocs.push(slug);
  434. }
  435. }
  436. for (slug of Array.from(this.corruptedDocs)) {
  437. $.arrayDelete(docs, slug);
  438. }
  439. if (docs.length === 0) {
  440. setTimeout(this.deleteCorruptedDocs, 0);
  441. return;
  442. }
  443. const txn = this.idbTransaction(db, {
  444. stores: docs,
  445. mode: "readonly",
  446. ignoreError: false,
  447. });
  448. txn.oncomplete = () => {
  449. if (this.corruptedDocs.length > 0) {
  450. setTimeout(this.deleteCorruptedDocs, 0);
  451. }
  452. };
  453. for (var doc of Array.from(docs)) {
  454. txn.objectStore(doc).get("index").onsuccess = (event) => {
  455. if (!event.target.result) {
  456. this.corruptedDocs.push(event.target.source.name);
  457. }
  458. };
  459. }
  460. });
  461. }
  462. deleteCorruptedDocs() {
  463. this.db((db) => {
  464. let doc;
  465. const txn = this.idbTransaction(db, {
  466. stores: ["docs"],
  467. mode: "readwrite",
  468. ignoreError: false,
  469. });
  470. const store = txn.objectStore("docs");
  471. while ((doc = this.corruptedDocs.pop())) {
  472. this.cachedDocs[doc] = false;
  473. store.delete(doc);
  474. }
  475. });
  476. Raven.captureMessage("corruptedDocs", {
  477. level: "info",
  478. extra: { docs: this.corruptedDocs.join(",") },
  479. });
  480. }
  481. shouldLoadWithIDB(entry) {
  482. return (
  483. this.useIndexedDB &&
  484. (!this.cachedDocs || this.cachedDocs[entry.doc.slug])
  485. );
  486. }
  487. idbTransaction(db, options) {
  488. app.lastIDBTransaction = [options.stores, options.mode];
  489. const txn = db.transaction(options.stores, options.mode);
  490. if (options.ignoreError !== false) {
  491. txn.onerror = function (event) {
  492. event.preventDefault();
  493. };
  494. }
  495. if (options.ignoreAbort !== false) {
  496. txn.onabort = function (event) {
  497. event.preventDefault();
  498. };
  499. }
  500. return txn;
  501. }
  502. reset() {
  503. try {
  504. if (typeof indexedDB !== "undefined" && indexedDB !== null) {
  505. indexedDB.deleteDatabase(NAME);
  506. }
  507. } catch (error) {}
  508. }
  509. useIndexedDB() {
  510. try {
  511. if (!app.isSingleDoc() && window.indexedDB) {
  512. return true;
  513. } else {
  514. this.reason = "not_supported";
  515. return false;
  516. }
  517. } catch (error) {
  518. return false;
  519. }
  520. }
  521. migrate() {
  522. app.settings.set("schema", this.userVersion() + 1);
  523. }
  524. setUserVersion(version) {
  525. app.settings.set("schema", version);
  526. }
  527. userVersion() {
  528. return app.settings.get("schema");
  529. }
  530. };
  531. app.DB.initClass();
  532. return app.DB;
  533. })();