pinafore/routes/_database/timelines.js

427 lines
14 KiB
JavaScript
Raw Normal View History

2018-02-11 18:35:25 +00:00
import { toPaddedBigInt, toReversePaddedBigInt } from './utils'
2018-02-14 03:34:37 +00:00
import { cloneForStorage } from './helpers'
2018-02-09 06:04:10 +00:00
import { dbPromise, getDatabase } from './databaseLifecycle'
2018-02-17 03:38:21 +00:00
import {
accountsCache, deleteFromCache, getInCache, hasInCache, notificationsCache, setInCache,
statusesCache
} from './cache'
2018-02-14 03:34:37 +00:00
import { scheduleCleanup } from './cleanup'
2018-02-09 06:04:10 +00:00
import {
2018-02-10 03:48:52 +00:00
ACCOUNTS_STORE,
NOTIFICATION_TIMELINES_STORE,
2018-02-11 18:35:25 +00:00
NOTIFICATIONS_STORE, PINNED_STATUSES_STORE,
2018-02-10 03:48:52 +00:00
STATUS_TIMELINES_STORE,
2018-02-14 03:34:37 +00:00
STATUSES_STORE,
ACCOUNT_ID,
REBLOG_ID,
STATUS_ID
2018-02-09 06:04:10 +00:00
} from './constants'
2018-02-11 17:36:01 +00:00
function createTimelineKeyRange (timeline, maxId) {
2018-02-09 06:04:10 +00:00
let negBigInt = maxId && toReversePaddedBigInt(maxId)
let start = negBigInt ? (timeline + '\u0000' + negBigInt) : (timeline + '\u0000')
let end = timeline + '\u0000\uffff'
2018-02-10 03:48:52 +00:00
return IDBKeyRange.bound(start, end, true, true)
}
2018-02-11 17:36:01 +00:00
// 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)
}
2018-02-11 22:11:03 +00:00
function cacheStatus (status, instanceName) {
2018-02-11 18:35:25 +00:00
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)
}
}
2018-02-10 03:48:52 +00:00
//
// pagination
//
2018-02-09 06:29:29 +00:00
async function getNotificationTimeline (instanceName, timeline, maxId, limit) {
2018-02-10 03:48:52 +00:00
let storeNames = [NOTIFICATION_TIMELINES_STORE, NOTIFICATIONS_STORE, STATUSES_STORE, ACCOUNTS_STORE]
2018-02-09 06:04:10 +00:00
const db = await getDatabase(instanceName)
2018-02-09 06:29:29 +00:00
return dbPromise(db, storeNames, 'readonly', (stores, callback) => {
2018-02-10 03:48:52 +00:00
let [ timelineStore, notificationsStore, statusesStore, accountsStore ] = stores
2018-02-11 17:36:01 +00:00
let keyRange = createTimelineKeyRange(timeline, maxId)
2018-02-09 06:04:10 +00:00
timelineStore.getAll(keyRange, limit).onsuccess = e => {
let timelineResults = e.target.result
let res = new Array(timelineResults.length)
timelineResults.forEach((timelineResult, i) => {
2018-02-10 03:48:52 +00:00
fetchNotification(notificationsStore, statusesStore, accountsStore, timelineResult.notificationId, notification => {
res[i] = notification
})
2018-02-09 06:04:10 +00:00
})
callback(res)
}
})
}
2018-02-09 06:29:29 +00:00
async function getStatusTimeline (instanceName, timeline, maxId, limit) {
2018-02-10 03:48:52 +00:00
let storeNames = [STATUS_TIMELINES_STORE, STATUSES_STORE, ACCOUNTS_STORE]
2018-02-09 06:04:10 +00:00
const db = await getDatabase(instanceName)
2018-02-09 06:29:29 +00:00
return dbPromise(db, storeNames, 'readonly', (stores, callback) => {
2018-02-10 03:48:52 +00:00
let [ timelineStore, statusesStore, accountsStore ] = stores
2018-02-11 17:36:01 +00:00
// 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/')
2018-02-11 17:37:13 +00:00
let getReq = isStatusThread
? timelineStore.getAll(createKeyRangeForStatusThread(timeline))
: timelineStore.getAll(createTimelineKeyRange(timeline, maxId), limit)
2018-02-09 06:04:10 +00:00
2018-02-11 17:36:01 +00:00
getReq.onsuccess = e => {
2018-02-09 06:04:10 +00:00
let timelineResults = e.target.result
2018-02-11 17:36:01 +00:00
if (isStatusThread) {
timelineResults = timelineResults.reverse()
}
2018-02-09 06:04:10 +00:00
let res = new Array(timelineResults.length)
timelineResults.forEach((timelineResult, i) => {
2018-02-10 03:48:52 +00:00
fetchStatus(statusesStore, accountsStore, timelineResult.statusId, status => {
res[i] = status
})
2018-02-09 06:04:10 +00:00
})
callback(res)
}
})
}
2018-02-09 06:29:29 +00:00
export async function getTimeline (instanceName, timeline, maxId = null, limit = 20) {
return timeline === 'notifications'
? getNotificationTimeline(instanceName, timeline, maxId, limit)
: getStatusTimeline(instanceName, timeline, maxId, limit)
2018-02-09 06:04:10 +00:00
}
2018-02-10 03:48:52 +00:00
//
// insertion
//
2018-02-11 17:37:13 +00:00
function putStatus (statusesStore, status) {
2018-02-10 03:48:52 +00:00
statusesStore.put(cloneForStorage(status))
}
2018-02-11 17:37:13 +00:00
function putAccount (accountsStore, account) {
2018-02-10 03:48:52 +00:00
accountsStore.put(cloneForStorage(account))
}
2018-02-11 17:37:13 +00:00
function putNotification (notificationsStore, notification) {
2018-02-10 03:48:52 +00:00
notificationsStore.put(cloneForStorage(notification))
}
2018-02-11 17:37:13 +00:00
function storeAccount (accountsStore, account) {
2018-02-10 03:48:52 +00:00
putAccount(accountsStore, account)
}
2018-02-11 17:37:13 +00:00
function storeStatus (statusesStore, accountsStore, status) {
2018-02-10 03:48:52 +00:00
putStatus(statusesStore, status)
putAccount(accountsStore, status.account)
if (status.reblog) {
putStatus(statusesStore, status.reblog)
putAccount(accountsStore, status.reblog.account)
}
}
2018-02-11 17:37:13 +00:00
function storeNotification (notificationsStore, statusesStore, accountsStore, notification) {
2018-02-10 03:48:52 +00:00
if (notification.status) {
storeStatus(statusesStore, accountsStore, notification.status)
}
storeAccount(accountsStore, notification.account)
putNotification(notificationsStore, notification)
}
2018-02-11 17:37:13 +00:00
function fetchAccount (accountsStore, id, callback) {
2018-02-10 03:48:52 +00:00
accountsStore.get(id).onsuccess = e => {
callback(e.target.result)
}
}
2018-02-11 17:37:13 +00:00
function fetchStatus (statusesStore, accountsStore, id, callback) {
2018-02-10 03:48:52 +00:00
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
})
}
}
}
2018-02-11 17:37:13 +00:00
function fetchNotification (notificationsStore, statusesStore, accountsStore, id, callback) {
2018-02-10 03:48:52 +00:00
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
})
}
}
}
2018-02-09 06:29:29 +00:00
function createTimelineId (timeline, id) {
2018-02-09 06:04:10 +00:00
// reverse chronological order, prefixed by timeline
return timeline + '\u0000' + toReversePaddedBigInt(id)
}
2018-02-09 06:29:29 +00:00
async function insertTimelineNotifications (instanceName, timeline, notifications) {
2018-02-14 03:34:37 +00:00
/* no await */ scheduleCleanup()
2018-02-09 06:04:10 +00:00
for (let notification of notifications) {
setInCache(notificationsCache, instanceName, notification.id, notification)
setInCache(accountsCache, instanceName, notification.account.id, notification.account)
2018-02-15 17:02:46 +00:00
if (notification.status) {
setInCache(statusesCache, instanceName, notification.status.id, notification.status)
}
2018-02-09 06:04:10 +00:00
}
const db = await getDatabase(instanceName)
2018-02-10 03:48:52 +00:00
let storeNames = [NOTIFICATION_TIMELINES_STORE, NOTIFICATIONS_STORE, ACCOUNTS_STORE, STATUSES_STORE]
2018-02-09 06:04:10 +00:00
await dbPromise(db, storeNames, 'readwrite', (stores) => {
2018-02-10 03:48:52 +00:00
let [ timelineStore, notificationsStore, accountsStore, statusesStore ] = stores
2018-02-09 06:04:10 +00:00
for (let notification of notifications) {
2018-02-10 03:48:52 +00:00
storeNotification(notificationsStore, statusesStore, accountsStore, notification)
2018-02-09 06:04:10 +00:00
timelineStore.put({
id: createTimelineId(timeline, notification.id),
notificationId: notification.id
})
}
})
}
2018-02-09 06:29:29 +00:00
async function insertTimelineStatuses (instanceName, timeline, statuses) {
2018-02-14 03:34:37 +00:00
/* no await */ scheduleCleanup()
2018-02-09 06:04:10 +00:00
for (let status of statuses) {
2018-02-11 18:35:25 +00:00
cacheStatus(status, instanceName)
2018-02-09 06:04:10 +00:00
}
const db = await getDatabase(instanceName)
2018-02-10 03:48:52 +00:00
let storeNames = [STATUS_TIMELINES_STORE, STATUSES_STORE, ACCOUNTS_STORE]
2018-02-09 06:04:10 +00:00
await dbPromise(db, storeNames, 'readwrite', (stores) => {
let [ timelineStore, statusesStore, accountsStore ] = stores
for (let status of statuses) {
2018-02-10 03:48:52 +00:00
storeStatus(statusesStore, accountsStore, status)
2018-02-09 06:04:10 +00:00
timelineStore.put({
id: createTimelineId(timeline, status.id),
statusId: status.id
})
}
})
}
2018-02-09 06:29:29 +00:00
export async function insertTimelineItems (instanceName, timeline, timelineItems) {
return timeline === 'notifications'
? insertTimelineNotifications(instanceName, timeline, timelineItems)
: insertTimelineStatuses(instanceName, timeline, timelineItems)
2018-02-09 06:04:10 +00:00
}
2018-02-10 03:48:52 +00:00
//
// 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
2018-02-09 06:04:10 +00:00
}
2018-02-10 03:48:52 +00:00
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
2018-02-09 06:29:29 +00:00
}
2018-02-11 18:35:25 +00:00
2018-02-17 03:38:21 +00:00
//
// 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)
}
})
}
2018-02-11 18:35:25 +00:00
//
// 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)
}
})
2018-02-11 22:11:03 +00:00
}
2018-02-17 03:38:21 +00:00
//
// 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))
}
})
}
2018-02-24 02:23:36 +00:00
//
2018-02-24 22:49:28 +00:00
// update statuses
2018-02-24 02:23:36 +00:00
//
2018-02-24 22:49:28 +00:00
async function updateStatus (instanceName, statusId, updateFunc) {
2018-02-24 02:23:36 +00:00
const db = await getDatabase(instanceName)
2018-02-24 22:49:28 +00:00
if (hasInCache(statusesCache, instanceName, statusId)) {
let status = getInCache(statusesCache, instanceName, statusId)
updateFunc(status)
cacheStatus(status, instanceName)
}
2018-02-24 02:23:36 +00:00
return dbPromise(db, STATUSES_STORE, 'readwrite', (statusesStore) => {
2018-02-24 22:49:28 +00:00
statusesStore.get(statusId).onsuccess = e => {
let status = e.target.result
updateFunc(status)
putStatus(statusesStore, status)
}
2018-02-24 02:23:36 +00:00
})
2018-02-24 22:49:28 +00:00
}
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
})
}
2018-02-25 02:20:33 +00:00
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
})
}