pinafore/routes/_database/timelines.js
2018-02-24 20:35:22 -08:00

427 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { toPaddedBigInt, toReversePaddedBigInt } from './utils'
import { cloneForStorage } from './helpers'
import { dbPromise, getDatabase } from './databaseLifecycle'
import {
accountsCache, deleteFromCache, getInCache, hasInCache, notificationsCache, setInCache,
statusesCache
} from './cache'
import { scheduleCleanup } from './cleanup'
import {
ACCOUNTS_STORE,
NOTIFICATION_TIMELINES_STORE,
NOTIFICATIONS_STORE, PINNED_STATUSES_STORE,
STATUS_TIMELINES_STORE,
STATUSES_STORE,
ACCOUNT_ID,
REBLOG_ID,
STATUS_ID
} from './constants'
function createTimelineKeyRange (timeline, maxId) {
let negBigInt = maxId && toReversePaddedBigInt(maxId)
let start = negBigInt ? (timeline + '\u0000' + negBigInt) : (timeline + '\u0000')
let end = timeline + '\u0000\uffff'
return IDBKeyRange.bound(start, end, true, true)
}
// special case for threads these are in chronological order rather than reverse
// chronological order, and we fetch everything all at once rather than paginating
function createKeyRangeForStatusThread (timeline) {
let start = timeline + '\u0000'
let end = timeline + '\u0000\uffff'
return IDBKeyRange.bound(start, end, true, true)
}
function cacheStatus (status, instanceName) {
setInCache(statusesCache, instanceName, status.id, status)
setInCache(accountsCache, instanceName, status.account.id, status.account)
if (status.reblog) {
setInCache(accountsCache, instanceName, status.reblog.account.id, status.reblog.account)
}
}
//
// pagination
//
async function getNotificationTimeline (instanceName, timeline, maxId, limit) {
let storeNames = [NOTIFICATION_TIMELINES_STORE, NOTIFICATIONS_STORE, STATUSES_STORE, ACCOUNTS_STORE]
const db = await getDatabase(instanceName)
return dbPromise(db, storeNames, 'readonly', (stores, callback) => {
let [ timelineStore, notificationsStore, statusesStore, accountsStore ] = stores
let keyRange = createTimelineKeyRange(timeline, maxId)
timelineStore.getAll(keyRange, limit).onsuccess = e => {
let timelineResults = e.target.result
let res = new Array(timelineResults.length)
timelineResults.forEach((timelineResult, i) => {
fetchNotification(notificationsStore, statusesStore, accountsStore, timelineResult.notificationId, notification => {
res[i] = notification
})
})
callback(res)
}
})
}
async function getStatusTimeline (instanceName, timeline, maxId, limit) {
let storeNames = [STATUS_TIMELINES_STORE, STATUSES_STORE, ACCOUNTS_STORE]
const db = await getDatabase(instanceName)
return dbPromise(db, storeNames, 'readonly', (stores, callback) => {
let [ timelineStore, statusesStore, accountsStore ] = stores
// Status threads are a special case - these are in forward chronological order
// and we fetch them all at once instead of paginating.
let isStatusThread = timeline.startsWith('status/')
let getReq = isStatusThread
? timelineStore.getAll(createKeyRangeForStatusThread(timeline))
: timelineStore.getAll(createTimelineKeyRange(timeline, maxId), limit)
getReq.onsuccess = e => {
let timelineResults = e.target.result
if (isStatusThread) {
timelineResults = timelineResults.reverse()
}
let res = new Array(timelineResults.length)
timelineResults.forEach((timelineResult, i) => {
fetchStatus(statusesStore, accountsStore, timelineResult.statusId, status => {
res[i] = status
})
})
callback(res)
}
})
}
export async function getTimeline (instanceName, timeline, maxId = null, limit = 20) {
return timeline === 'notifications'
? getNotificationTimeline(instanceName, timeline, maxId, limit)
: getStatusTimeline(instanceName, timeline, maxId, limit)
}
//
// insertion
//
function putStatus (statusesStore, status) {
statusesStore.put(cloneForStorage(status))
}
function putAccount (accountsStore, account) {
accountsStore.put(cloneForStorage(account))
}
function putNotification (notificationsStore, notification) {
notificationsStore.put(cloneForStorage(notification))
}
function storeAccount (accountsStore, account) {
putAccount(accountsStore, account)
}
function storeStatus (statusesStore, accountsStore, status) {
putStatus(statusesStore, status)
putAccount(accountsStore, status.account)
if (status.reblog) {
putStatus(statusesStore, status.reblog)
putAccount(accountsStore, status.reblog.account)
}
}
function storeNotification (notificationsStore, statusesStore, accountsStore, notification) {
if (notification.status) {
storeStatus(statusesStore, accountsStore, notification.status)
}
storeAccount(accountsStore, notification.account)
putNotification(notificationsStore, notification)
}
function fetchAccount (accountsStore, id, callback) {
accountsStore.get(id).onsuccess = e => {
callback(e.target.result)
}
}
function fetchStatus (statusesStore, accountsStore, id, callback) {
statusesStore.get(id).onsuccess = e => {
let status = e.target.result
callback(status)
fetchAccount(accountsStore, status[ACCOUNT_ID], account => {
status.account = account
})
if (status[REBLOG_ID]) {
fetchStatus(statusesStore, accountsStore, status[REBLOG_ID], reblog => {
status.reblog = reblog
})
}
}
}
function fetchNotification (notificationsStore, statusesStore, accountsStore, id, callback) {
notificationsStore.get(id).onsuccess = e => {
let notification = e.target.result
callback(notification)
fetchAccount(accountsStore, notification[ACCOUNT_ID], account => {
notification.account = account
})
if (notification[STATUS_ID]) {
fetchStatus(statusesStore, accountsStore, notification[STATUS_ID], status => {
notification.status = status
})
}
}
}
function createTimelineId (timeline, id) {
// reverse chronological order, prefixed by timeline
return timeline + '\u0000' + toReversePaddedBigInt(id)
}
async function insertTimelineNotifications (instanceName, timeline, notifications) {
/* no await */ scheduleCleanup()
for (let notification of notifications) {
setInCache(notificationsCache, instanceName, notification.id, notification)
setInCache(accountsCache, instanceName, notification.account.id, notification.account)
if (notification.status) {
setInCache(statusesCache, instanceName, notification.status.id, notification.status)
}
}
const db = await getDatabase(instanceName)
let storeNames = [NOTIFICATION_TIMELINES_STORE, NOTIFICATIONS_STORE, ACCOUNTS_STORE, STATUSES_STORE]
await dbPromise(db, storeNames, 'readwrite', (stores) => {
let [ timelineStore, notificationsStore, accountsStore, statusesStore ] = stores
for (let notification of notifications) {
storeNotification(notificationsStore, statusesStore, accountsStore, notification)
timelineStore.put({
id: createTimelineId(timeline, notification.id),
notificationId: notification.id
})
}
})
}
async function insertTimelineStatuses (instanceName, timeline, statuses) {
/* no await */ scheduleCleanup()
for (let status of statuses) {
cacheStatus(status, instanceName)
}
const db = await getDatabase(instanceName)
let storeNames = [STATUS_TIMELINES_STORE, STATUSES_STORE, ACCOUNTS_STORE]
await dbPromise(db, storeNames, 'readwrite', (stores) => {
let [ timelineStore, statusesStore, accountsStore ] = stores
for (let status of statuses) {
storeStatus(statusesStore, accountsStore, status)
timelineStore.put({
id: createTimelineId(timeline, status.id),
statusId: status.id
})
}
})
}
export async function insertTimelineItems (instanceName, timeline, timelineItems) {
return timeline === 'notifications'
? insertTimelineNotifications(instanceName, timeline, timelineItems)
: insertTimelineStatuses(instanceName, timeline, timelineItems)
}
//
// get
//
export async function getStatus (instanceName, id) {
if (hasInCache(statusesCache, instanceName, id)) {
return getInCache(statusesCache, instanceName, id)
}
const db = await getDatabase(instanceName)
let storeNames = [STATUSES_STORE, ACCOUNTS_STORE]
let result = await dbPromise(db, storeNames, 'readonly', (stores, callback) => {
let [ statusesStore, accountsStore ] = stores
fetchStatus(statusesStore, accountsStore, id, callback)
})
setInCache(statusesCache, instanceName, id, result)
return result
}
export async function getNotification (instanceName, id) {
if (hasInCache(notificationsCache, instanceName, id)) {
return getInCache(notificationsCache, instanceName, id)
}
const db = await getDatabase(instanceName)
let storeNames = [NOTIFICATIONS_STORE, STATUSES_STORE, ACCOUNTS_STORE]
let result = await dbPromise(db, storeNames, 'readonly', (stores, callback) => {
let [ notificationsStore, statusesStore, accountsStore ] = stores
fetchNotification(notificationsStore, statusesStore, accountsStore, id, callback)
})
setInCache(notificationsCache, instanceName, id, result)
return result
}
//
// lookup by reblogs
//
export async function getReblogsForStatus (instanceName, id) {
const db = await getDatabase(instanceName)
await dbPromise(db, STATUSES_STORE, 'readonly', (statusesStore, callback) => {
statusesStore.index(REBLOG_ID).getAll(IDBKeyRange.only(id)).onsuccess = e => {
callback(e.target.result)
}
})
}
//
// deletes
//
export async function deleteStatusesAndNotifications (instanceName, statusIds, notificationIds) {
for (let statusId of statusIds) {
deleteFromCache(statusesCache, instanceName, statusId)
}
for (let notificationId of notificationIds) {
deleteFromCache(notificationsCache, instanceName, notificationId)
}
const db = await getDatabase(instanceName)
let storeNames = [
STATUSES_STORE,
STATUS_TIMELINES_STORE,
NOTIFICATIONS_STORE,
NOTIFICATION_TIMELINES_STORE,
PINNED_STATUSES_STORE
]
await dbPromise(db, storeNames, 'readwrite', (stores) => {
let [
statusesStore,
statusTimelinesStore,
notificationsStore,
notificationTimelinesStore,
pinnedStatusesStore
] = stores
function deleteStatus (statusId) {
pinnedStatusesStore.delete(statusId).onerror = e => {
e.preventDefault()
e.stopPropagation()
}
statusesStore.delete(statusId)
let getAllReq = statusTimelinesStore.index('statusId')
.getAllKeys(IDBKeyRange.only(statusId))
getAllReq.onsuccess = e => {
for (let result of e.target.result) {
statusTimelinesStore.delete(result)
}
}
}
function deleteNotification (notificationId) {
notificationsStore.delete(notificationId)
let getAllReq = notificationTimelinesStore.index('statusId')
.getAllKeys(IDBKeyRange.only(notificationId))
getAllReq.onsuccess = e => {
for (let result of e.target.result) {
notificationTimelinesStore.delete(result)
}
}
}
for (let statusId of statusIds) {
deleteStatus(statusId)
}
for (let notificationId of notificationIds) {
deleteNotification(notificationId)
}
})
}
//
// pinned statuses
//
export async function insertPinnedStatuses (instanceName, accountId, statuses) {
for (let status of statuses) {
cacheStatus(status, instanceName)
}
const db = await getDatabase(instanceName)
let storeNames = [PINNED_STATUSES_STORE, STATUSES_STORE, ACCOUNTS_STORE]
await dbPromise(db, storeNames, 'readwrite', (stores) => {
let [ pinnedStatusesStore, statusesStore, accountsStore ] = stores
statuses.forEach((status, i) => {
storeStatus(statusesStore, accountsStore, status)
pinnedStatusesStore.put({
id: accountId + '\u0000' + toPaddedBigInt(i),
statusId: status.id
})
})
})
}
export async function getPinnedStatuses (instanceName, accountId) {
let storeNames = [PINNED_STATUSES_STORE, STATUSES_STORE, ACCOUNTS_STORE]
const db = await getDatabase(instanceName)
return dbPromise(db, storeNames, 'readonly', (stores, callback) => {
let [ pinnedStatusesStore, statusesStore, accountsStore ] = stores
let keyRange = IDBKeyRange.bound(
accountId + '\u0000',
accountId + '\u0000\uffff'
)
pinnedStatusesStore.getAll(keyRange).onsuccess = e => {
let pinnedResults = e.target.result
let res = new Array(pinnedResults.length)
pinnedResults.forEach((pinnedResult, i) => {
fetchStatus(statusesStore, accountsStore, pinnedResult.statusId, status => {
res[i] = status
})
})
callback(res)
}
})
}
//
// notifications by status
//
export async function getNotificationIdsForStatus (instanceName, statusId) {
const db = await getDatabase(instanceName)
return dbPromise(db, NOTIFICATIONS_STORE, 'readonly', (notificationStore, callback) => {
notificationStore.index(statusId).getAllKeys(IDBKeyRange.only(statusId)).onsuccess = e => {
callback(Array.from(e.target.result))
}
})
}
//
// update statuses
//
async function updateStatus (instanceName, statusId, updateFunc) {
const db = await getDatabase(instanceName)
if (hasInCache(statusesCache, instanceName, statusId)) {
let status = getInCache(statusesCache, instanceName, statusId)
updateFunc(status)
cacheStatus(status, instanceName)
}
return dbPromise(db, STATUSES_STORE, 'readwrite', (statusesStore) => {
statusesStore.get(statusId).onsuccess = e => {
let status = e.target.result
updateFunc(status)
putStatus(statusesStore, status)
}
})
}
export async function setStatusFavorited (instanceName, statusId, favorited) {
return updateStatus(instanceName, statusId, status => {
let delta = (favorited ? 1 : 0) - (status.favourited ? 1 : 0)
status.favourited = favorited
status.favourites_count = (status.favourites_count || 0) + delta
})
}
export async function setStatusReblogged (instanceName, statusId, reblogged) {
return updateStatus(instanceName, statusId, status => {
let delta = (reblogged ? 1 : 0) - (status.reblogged ? 1 : 0)
status.reblogged = reblogged
status.reblogs_count = (status.reblogs_count || 0) + delta
})
}