about summary refs log tree commit diff
path: root/app/javascript/mastodon/storage
diff options
context:
space:
mode:
authorAkihiko Odaki <akihiko.odaki.4i@stu.hosei.ac.jp>2018-03-27 19:32:30 +0900
committerEugen Rochko <eugen@zeonfederated.com>2018-03-27 12:32:30 +0200
commitca42f9b0ebfa1f4e8e86745a79af138b5865daee (patch)
tree8892a8d9e2f7cbc621e231a29b89bbee3a590cca /app/javascript/mastodon/storage
parent31e7b7308489ecc8b43f83b78ec0a288c4195d5b (diff)
Cache media (#6902)
Diffstat (limited to 'app/javascript/mastodon/storage')
-rw-r--r--app/javascript/mastodon/storage/db.js28
-rw-r--r--app/javascript/mastodon/storage/modifier.js151
2 files changed, 179 insertions, 0 deletions
diff --git a/app/javascript/mastodon/storage/db.js b/app/javascript/mastodon/storage/db.js
new file mode 100644
index 000000000..e08fc3f3d
--- /dev/null
+++ b/app/javascript/mastodon/storage/db.js
@@ -0,0 +1,28 @@
+import { me } from '../initial_state';
+
+export default new Promise((resolve, reject) => {
+  // Microsoft Edge 17 does not support getAll according to:
+  // Catalog of standard and vendor APIs across browsers - Microsoft Edge Development
+  // https://developer.microsoft.com/en-us/microsoft-edge/platform/catalog/?q=specName%3Aindexeddb
+  if (!me || !('getAll' in IDBObjectStore.prototype)) {
+    reject();
+    return;
+  }
+
+  const request = indexedDB.open('mastodon:' + me);
+
+  request.onerror = reject;
+  request.onsuccess = ({ target }) => resolve(target.result);
+
+  request.onupgradeneeded = ({ target }) => {
+    const accounts = target.result.createObjectStore('accounts', { autoIncrement: true });
+    const statuses = target.result.createObjectStore('statuses', { autoIncrement: true });
+
+    accounts.createIndex('id', 'id', { unique: true });
+    accounts.createIndex('moved', 'moved');
+
+    statuses.createIndex('id', 'id', { unique: true });
+    statuses.createIndex('account', 'account');
+    statuses.createIndex('reblog', 'reblog');
+  };
+});
diff --git a/app/javascript/mastodon/storage/modifier.js b/app/javascript/mastodon/storage/modifier.js
new file mode 100644
index 000000000..63e49fe6e
--- /dev/null
+++ b/app/javascript/mastodon/storage/modifier.js
@@ -0,0 +1,151 @@
+import asyncDB from './db';
+import { autoPlayGif } from '../initial_state';
+
+const accountAssetKeys = ['avatar', 'avatar_static', 'header', 'header_static'];
+const avatarKey = autoPlayGif ? 'avatar' : 'avatar_static';
+const limit = 1024;
+const asyncCache = caches.open('mastodon-system');
+
+function put(name, objects, onupdate, oncreate) {
+  return asyncDB.then(db => new Promise((resolve, reject) => {
+    const putTransaction = db.transaction(name, 'readwrite');
+    const putStore = putTransaction.objectStore(name);
+    const putIndex = putStore.index('id');
+
+    objects.forEach(object => {
+      putIndex.getKey(object.id).onsuccess = retrieval => {
+        function addObject() {
+          putStore.add(object);
+        }
+
+        function deleteObject() {
+          putStore.delete(retrieval.target.result).onsuccess = addObject;
+        }
+
+        if (retrieval.target.result) {
+          if (onupdate) {
+            onupdate(object, retrieval.target.result, putStore, deleteObject);
+          } else {
+            deleteObject();
+          }
+        } else {
+          if (oncreate) {
+            oncreate(object, addObject);
+          } else {
+            addObject();
+          }
+        }
+      };
+    });
+
+    putTransaction.oncomplete = () => {
+      const readTransaction = db.transaction(name, 'readonly');
+      const readStore = readTransaction.objectStore(name);
+      const count = readStore.count();
+
+      count.onsuccess = () => {
+        const excess = count.result - limit;
+
+        if (excess > 0) {
+          const retrieval = readStore.getAll(null, excess);
+
+          retrieval.onsuccess = () => resolve(retrieval.result);
+          retrieval.onerror = reject;
+        } else {
+          resolve([]);
+        }
+      };
+
+      count.onerror = reject;
+    };
+
+    putTransaction.onerror = reject;
+  }));
+}
+
+function evictAccountsByRecords(records) {
+  asyncDB.then(db => {
+    const transaction = db.transaction(['accounts', 'statuses'], 'readwrite');
+    const accounts = transaction.objectStore('accounts');
+    const accountsIdIndex = accounts.index('id');
+    const accountsMovedIndex = accounts.index('moved');
+    const statuses = transaction.objectStore('statuses');
+    const statusesIndex = statuses.index('account');
+
+    function evict(toEvict) {
+      toEvict.forEach(record => {
+        asyncCache.then(cache => accountAssetKeys.forEach(key => cache.delete(records[key])));
+
+        accountsMovedIndex.getAll(record.id).onsuccess = ({ target }) => evict(target.result);
+
+        statusesIndex.getAll(record.id).onsuccess =
+          ({ target }) => evictStatusesByRecords(target.result);
+
+        accountsIdIndex.getKey(record.id).onsuccess =
+          ({ target }) => target.result && accounts.delete(target.result);
+      });
+    }
+
+    evict(records);
+  });
+}
+
+export function evictStatus(id) {
+  return evictStatuses([id]);
+}
+
+export function evictStatuses(ids) {
+  asyncDB.then(db => {
+    const store = db.transaction('statuses', 'readwrite').objectStore('statuses');
+    const idIndex = store.index('id');
+    const reblogIndex = store.index('reblog');
+
+    ids.forEach(id => {
+      reblogIndex.getAllKeys(id).onsuccess =
+        ({ target }) => target.result.forEach(reblogKey => store.delete(reblogKey));
+
+      idIndex.getKey(id).onsuccess =
+        ({ target }) => target.result && store.delete(target.result);
+    });
+  });
+}
+
+function evictStatusesByRecords(records) {
+  evictStatuses(records.map(({ id }) => id));
+}
+
+export function putAccounts(records) {
+  const newURLs = [];
+
+  put('accounts', records, (newRecord, oldKey, store, oncomplete) => {
+    store.get(oldKey).onsuccess = ({ target }) => {
+      accountAssetKeys.forEach(key => {
+        const newURL = newRecord[key];
+        const oldURL = target.result[key];
+
+        if (newURL !== oldURL) {
+          asyncCache.then(cache => cache.delete(oldURL));
+        }
+      });
+
+      const newURL = newRecord[avatarKey];
+      const oldURL = target.result[avatarKey];
+
+      if (newURL !== oldURL) {
+        newURLs.push(newURL);
+      }
+
+      oncomplete();
+    };
+  }, (newRecord, oncomplete) => {
+    newURLs.push(newRecord[avatarKey]);
+    oncomplete();
+  }).then(records => {
+    evictAccountsByRecords(records);
+    asyncCache.then(cache => cache.addAll(newURLs));
+  });
+}
+
+export function putStatuses(records) {
+  put('statuses', records).then(evictStatusesByRecords);
+}