forked from cybrespace/pinafore
321 lines
11 KiB
JavaScript
321 lines
11 KiB
JavaScript
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)
|
||
}
|
||
})
|
||
} |