diff options
Diffstat (limited to 'app/javascript/mastodon/storage')
-rw-r--r-- | app/javascript/mastodon/storage/db.js | 28 | ||||
-rw-r--r-- | app/javascript/mastodon/storage/modifier.js | 151 |
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); +} |