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.

211 lines
6.0 KiB

  1. import openDB from './db';
  2. const accountAssetKeys = ['avatar', 'avatar_static', 'header', 'header_static'];
  3. const storageMargin = 8388608;
  4. const storeLimit = 1024;
  5. // navigator.storage is not present on:
  6. // Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.100 Safari/537.36 Edge/16.16299
  7. // estimate method is not present on Chrome 57.0.2987.98 on Linux.
  8. export const storageFreeable = 'storage' in navigator && 'estimate' in navigator.storage;
  9. function openCache() {
  10. // ServiceWorker and Cache API is not available on iOS 11
  11. // https://webkit.org/status/#specification-service-workers
  12. return self.caches ? caches.open('mastodon-system') : Promise.reject();
  13. }
  14. function printErrorIfAvailable(error) {
  15. if (error) {
  16. console.warn(error);
  17. }
  18. }
  19. function put(name, objects, onupdate, oncreate) {
  20. return openDB().then(db => (new Promise((resolve, reject) => {
  21. const putTransaction = db.transaction(name, 'readwrite');
  22. const putStore = putTransaction.objectStore(name);
  23. const putIndex = putStore.index('id');
  24. objects.forEach(object => {
  25. putIndex.getKey(object.id).onsuccess = retrieval => {
  26. function addObject() {
  27. putStore.add(object);
  28. }
  29. function deleteObject() {
  30. putStore.delete(retrieval.target.result).onsuccess = addObject;
  31. }
  32. if (retrieval.target.result) {
  33. if (onupdate) {
  34. onupdate(object, retrieval.target.result, putStore, deleteObject);
  35. } else {
  36. deleteObject();
  37. }
  38. } else {
  39. if (oncreate) {
  40. oncreate(object, addObject);
  41. } else {
  42. addObject();
  43. }
  44. }
  45. };
  46. });
  47. putTransaction.oncomplete = () => {
  48. const readTransaction = db.transaction(name, 'readonly');
  49. const readStore = readTransaction.objectStore(name);
  50. const count = readStore.count();
  51. count.onsuccess = () => {
  52. const excess = count.result - storeLimit;
  53. if (excess > 0) {
  54. const retrieval = readStore.getAll(null, excess);
  55. retrieval.onsuccess = () => resolve(retrieval.result);
  56. retrieval.onerror = reject;
  57. } else {
  58. resolve([]);
  59. }
  60. };
  61. count.onerror = reject;
  62. };
  63. putTransaction.onerror = reject;
  64. })).then(resolved => {
  65. db.close();
  66. return resolved;
  67. }, error => {
  68. db.close();
  69. throw error;
  70. }));
  71. }
  72. function evictAccountsByRecords(records) {
  73. return openDB().then(db => {
  74. const transaction = db.transaction(['accounts', 'statuses'], 'readwrite');
  75. const accounts = transaction.objectStore('accounts');
  76. const accountsIdIndex = accounts.index('id');
  77. const accountsMovedIndex = accounts.index('moved');
  78. const statuses = transaction.objectStore('statuses');
  79. const statusesIndex = statuses.index('account');
  80. function evict(toEvict) {
  81. toEvict.forEach(record => {
  82. openCache()
  83. .then(cache => accountAssetKeys.forEach(key => cache.delete(records[key])))
  84. .catch(printErrorIfAvailable);
  85. accountsMovedIndex.getAll(record.id).onsuccess = ({ target }) => evict(target.result);
  86. statusesIndex.getAll(record.id).onsuccess =
  87. ({ target }) => evictStatusesByRecords(target.result);
  88. accountsIdIndex.getKey(record.id).onsuccess =
  89. ({ target }) => target.result && accounts.delete(target.result);
  90. });
  91. }
  92. evict(records);
  93. db.close();
  94. }).catch(printErrorIfAvailable);
  95. }
  96. export function evictStatus(id) {
  97. evictStatuses([id]);
  98. }
  99. export function evictStatuses(ids) {
  100. return openDB().then(db => {
  101. const transaction = db.transaction('statuses', 'readwrite');
  102. const store = transaction.objectStore('statuses');
  103. const idIndex = store.index('id');
  104. const reblogIndex = store.index('reblog');
  105. ids.forEach(id => {
  106. reblogIndex.getAllKeys(id).onsuccess =
  107. ({ target }) => target.result.forEach(reblogKey => store.delete(reblogKey));
  108. idIndex.getKey(id).onsuccess =
  109. ({ target }) => target.result && store.delete(target.result);
  110. });
  111. db.close();
  112. }).catch(printErrorIfAvailable);
  113. }
  114. function evictStatusesByRecords(records) {
  115. return evictStatuses(records.map(({ id }) => id));
  116. }
  117. export function putAccounts(records, avatarStatic) {
  118. const avatarKey = avatarStatic ? 'avatar_static' : 'avatar';
  119. const newURLs = [];
  120. put('accounts', records, (newRecord, oldKey, store, oncomplete) => {
  121. store.get(oldKey).onsuccess = ({ target }) => {
  122. accountAssetKeys.forEach(key => {
  123. const newURL = newRecord[key];
  124. const oldURL = target.result[key];
  125. if (newURL !== oldURL) {
  126. openCache()
  127. .then(cache => cache.delete(oldURL))
  128. .catch(printErrorIfAvailable);
  129. }
  130. });
  131. const newURL = newRecord[avatarKey];
  132. const oldURL = target.result[avatarKey];
  133. if (newURL !== oldURL) {
  134. newURLs.push(newURL);
  135. }
  136. oncomplete();
  137. };
  138. }, (newRecord, oncomplete) => {
  139. newURLs.push(newRecord[avatarKey]);
  140. oncomplete();
  141. }).then(records => Promise.all([
  142. evictAccountsByRecords(records),
  143. openCache().then(cache => cache.addAll(newURLs)),
  144. ])).then(freeStorage, error => {
  145. freeStorage();
  146. throw error;
  147. }).catch(printErrorIfAvailable);
  148. }
  149. export function putStatuses(records) {
  150. put('statuses', records)
  151. .then(evictStatusesByRecords)
  152. .catch(printErrorIfAvailable);
  153. }
  154. export function freeStorage() {
  155. return storageFreeable && navigator.storage.estimate().then(({ quota, usage }) => {
  156. if (usage + storageMargin < quota) {
  157. return null;
  158. }
  159. return openDB().then(db => new Promise((resolve, reject) => {
  160. const retrieval = db.transaction('accounts', 'readonly').objectStore('accounts').getAll(null, 1);
  161. retrieval.onsuccess = () => {
  162. if (retrieval.result.length > 0) {
  163. resolve(evictAccountsByRecords(retrieval.result).then(freeStorage));
  164. } else {
  165. resolve(caches.delete('mastodon-system'));
  166. }
  167. };
  168. retrieval.onerror = reject;
  169. db.close();
  170. }));
  171. });
  172. }