db.js 12 KB

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