pinafore/routes/_database/timelines.js

322 lines
11 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 { dbPromise, getDatabase } from './databaseLifecycle'
import { accountsCache, getInCache, hasInCache, notificationsCache, setInCache, statusesCache } from './cache'
import {
ACCOUNTS_STORE,
NOTIFICATION_TIMELINES_STORE,
NOTIFICATIONS_STORE, PINNED_STATUSES_STORE,
STATUS_TIMELINES_STORE,
STATUSES_STORE
} from './constants'
const TIMESTAMP = '__pinafore_ts'
const ACCOUNT_ID = '__pinafore_acct_id'
const STATUS_ID = '__pinafore_status_id'
const REBLOG_ID = '__pinafore_reblog_id'
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 cloneForStorage (obj) {
let res = {}
let keys = Object.keys(obj)
for (let key of keys) {
let value = obj[key]
// save storage space by skipping nulls, 0s, falses, empty strings, and empty arrays
if (!value || (Array.isArray(value) && value.length === 0)) {
continue
}
switch (key) {
case 'account':
res[ACCOUNT_ID] = value.id
break
case 'status':
res[STATUS_ID] = value.id
break
case 'reblog':
res[REBLOG_ID] = value.id
break
default:
res[key] = value
break
}
}
res[TIMESTAMP] = Date.now()
return res
}
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) {
for (let notification of notifications) {
setInCache(notificationsCache, instanceName, notification.id, notification)
setInCache(accountsCache, instanceName, notification.account.id, notification.account)
}
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) {
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
}
//
// 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)
}
})
}