db.js 14 KB

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