refactor database

This commit is contained in:
Nolan Lawson 2018-01-21 17:18:56 -08:00
parent 924e803d16
commit 4b04cc92f1
11 changed files with 171 additions and 69 deletions

View file

@ -20,13 +20,11 @@
import LoadingFooter from './LoadingFooter.html' import LoadingFooter from './LoadingFooter.html'
import VirtualList from './VirtualList.html' import VirtualList from './VirtualList.html'
import { splice, push } from 'svelte-extras' import { splice, push } from 'svelte-extras'
import worker from 'workerize-loader!../_utils/database/database'
import { mergeStatuses } from '../_utils/statuses' import { mergeStatuses } from '../_utils/statuses'
import { mark, stop } from '../_utils/marks' import { mark, stop } from '../_utils/marks'
import { timelines } from '../_static/timelines' import { timelines } from '../_static/timelines'
import { toast } from '../_utils/toast' import { toast } from '../_utils/toast'
import { database } from '../_utils/database/database'
const database = worker()
const FETCH_LIMIT = 20 const FETCH_LIMIT = 20

View file

@ -1,15 +1,15 @@
import keyval from "idb-keyval" import { getKnownDbs } from './knownDbs'
import debounce from 'lodash/debounce' import debounce from 'lodash/debounce'
import { OBJECT_STORE, getDatabase } from './shared' import { TIMELINE_STORE, getTimelineDatabase } from './timelines'
const MAX_NUM_STORED_STATUSES = 1000 const MAX_NUM_STORED_STATUSES = 1000
const CLEANUP_INTERVAL = 60000 const CLEANUP_INTERVAL = 60000
async function cleanup(instanceName, timeline) { async function cleanup(instanceName, timeline) {
const db = await getDatabase(instanceName, timeline) const db = await getTimelineDatabase(instanceName, timeline)
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
const tx = db.transaction(OBJECT_STORE, 'readwrite') const tx = db.transaction(TIMELINE_STORE, 'readwrite')
const store = tx.objectStore(OBJECT_STORE) const store = tx.objectStore(TIMELINE_STORE)
const index = store.index('pinafore_id_as_negative_big_int') const index = store.index('pinafore_id_as_negative_big_int')
store.count().onsuccess = (e) => { store.count().onsuccess = (e) => {
@ -37,11 +37,18 @@ export const cleanupOldStatuses = debounce(async () => {
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
console.log('cleanupOldStatuses') console.log('cleanupOldStatuses')
} }
let knownDbs = (await keyval.get('known_dbs')) || {}
let dbNames = Object.keys(knownDbs) let knownDbs = await getKnownDbs()
for (let dbName of dbNames) { let instanceNames = Object.keys(knownDbs)
let [ instanceName, timeline ] = knownDbs[dbName] for (let instanceName of instanceNames) {
await cleanup(instanceName, timeline) let knownDbsForInstance = knownDbs[instanceName] || []
for (let knownDb of knownDbsForInstance) {
let {type, dbName} = knownDb
if (type !== 'timeline') {
continue
}
await cleanup(instanceName, dbName)
}
} }
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
console.log('done cleanupOldStatuses') console.log('done cleanupOldStatuses')

View file

@ -1,36 +1,10 @@
import { cleanupOldStatuses } from './cleanup' import worker from 'workerize-loader!./databaseInsideWorker'
import { OBJECT_STORE, getDatabase } from './shared'
import { toReversePaddedBigInt, transformStatusForStorage } from './utils'
export async function getTimeline(instanceName, timeline, maxId = null, limit = 20) { import * as databaseInsideWorker from './databaseInsideWorker'
const db = await getDatabase(instanceName, timeline)
return await new Promise((resolve, reject) => {
const tx = db.transaction(OBJECT_STORE, 'readonly')
const store = tx.objectStore(OBJECT_STORE)
const index = store.index('pinafore_id_as_negative_big_int')
let sinceAsNegativeBigInt = maxId ? toReversePaddedBigInt(maxId) : null
let query = sinceAsNegativeBigInt ? IDBKeyRange.lowerBound(sinceAsNegativeBigInt, false) : null
let res // workerize-loader causes weirdness during development
index.getAll(query, limit).onsuccess = (e) => { let database = process.browser && process.env.NODE_ENV === 'production' ? worker() : databaseInsideWorker
res = e.target.result
}
tx.oncomplete = () => resolve(res) export {
tx.onerror = () => reject(tx.error.name + ' ' + tx.error.message) database
})
}
export async function insertStatuses(instanceName, timeline, statuses) {
cleanupOldStatuses()
const db = await getDatabase(instanceName, timeline)
return await new Promise((resolve, reject) => {
const tx = db.transaction(OBJECT_STORE, 'readwrite')
const store = tx.objectStore(OBJECT_STORE)
for (let status of statuses) {
store.put(transformStatusForStorage(status))
}
tx.oncomplete = () => resolve()
tx.onerror = () => reject(tx.error.name + ' ' + tx.error.message)
})
} }

View file

@ -0,0 +1,65 @@
import { META_STORE, getMetaDatabase } from './meta'
import { cleanupOldStatuses } from './cleanupTimelines'
import { TIMELINE_STORE, getTimelineDatabase } from './timelines'
import { toReversePaddedBigInt, transformStatusForStorage } from './utils'
export async function getTimeline(instanceName, timeline, maxId = null, limit = 20) {
const db = await getTimelineDatabase(instanceName, timeline)
return await new Promise((resolve, reject) => {
const tx = db.transaction(TIMELINE_STORE, 'readonly')
const store = tx.objectStore(TIMELINE_STORE)
const index = store.index('pinafore_id_as_negative_big_int')
let sinceAsNegativeBigInt = maxId ? toReversePaddedBigInt(maxId) : null
let query = sinceAsNegativeBigInt ? IDBKeyRange.lowerBound(sinceAsNegativeBigInt, false) : null
let res
index.getAll(query, limit).onsuccess = (e) => {
res = e.target.result
}
tx.oncomplete = () => resolve(res)
tx.onerror = () => reject(tx.error.name + ' ' + tx.error.message)
})
}
export async function insertStatuses(instanceName, timeline, statuses) {
const db = await getTimelineDatabase(instanceName, timeline)
await new Promise((resolve, reject) => {
const tx = db.transaction(TIMELINE_STORE, 'readwrite')
const store = tx.objectStore(TIMELINE_STORE)
for (let status of statuses) {
store.put(transformStatusForStorage(status))
}
tx.oncomplete = () => resolve()
tx.onerror = () => reject(tx.error.name + ' ' + tx.error.message)
})
/* no await */ cleanupOldStatuses()
}
export async function setInstanceVerifyCredentials(instanceName, verifyCredentials) {
const db = await getMetaDatabase(instanceName)
return await new Promise((resolve, reject) => {
const tx = db.transaction(META_STORE, 'readwrite')
const store = tx.objectStore(META_STORE)
store.put({
key: 'verifyCredentials',
value: verifyCredentials
})
tx.oncomplete = () => resolve()
tx.onerror = () => reject(tx.error.name + ' ' + tx.error.message)
})
}
export async function getInstanceVerifyCredentials(instanceName, verifyCredentials) {
const db = await getMetaDatabase(instanceName)
return await new Promise((resolve, reject) => {
const tx = db.transaction(META_STORE, 'readwrite')
const store = tx.objectStore(META_STORE)
let res
store.get('verifyCredentials').onsuccess = (e) => {
res = e.target.result && e.target.result.value
}
tx.oncomplete = () => resolve(res)
tx.onerror = () => reject(tx.error.name + ' ' + tx.error.message)
})
}

View file

@ -0,0 +1,21 @@
import keyval from "idb-keyval"
export async function addKnownDb(instanceName, type, dbName) {
let knownDbs = (await keyval.get('known_dbs')) || {}
if (!knownDbs[instanceName]) {
knownDbs[instanceName] = []
}
if (!knownDbs[instanceName].some(db => db.type === type && db.dbName === dbName)) {
knownDbs[instanceName].push({type, dbName})
}
await keyval.set('known_dbs', knownDbs)
}
export async function getKnownDbs() {
return (await keyval.get('known_dbs')) || {}
}
export async function getKnownDbsForInstance(instanceName) {
let knownDbs = await getKnownDbs()
return knownDbs[instanceName] || []
}

View file

@ -0,0 +1,29 @@
import { addKnownDb } from './knownDbs'
const databaseCache = {}
export const META_STORE = 'meta'
export function getMetaDatabase(instanceName) {
const key = `${instanceName}_${META_STORE}`
if (databaseCache[key]) {
return Promise.resolve(databaseCache[key])
}
let dbName = key
addKnownDb(instanceName, 'meta', dbName)
databaseCache[key] = new Promise((resolve, reject) => {
let req = indexedDB.open(dbName, 1)
req.onerror = reject
req.onblocked = () => {
console.log('idb blocked')
}
req.onupgradeneeded = () => {
let db = req.result;
db.createObjectStore(META_STORE, {keyPath: 'key'})
}
req.onsuccess = () => resolve(req.result)
})
return databaseCache[key]
}

View file

@ -1,25 +1,21 @@
import keyval from "idb-keyval" import { addKnownDb } from './knownDbs'
const databaseCache = {} const databaseCache = {}
export const OBJECT_STORE = 'statuses' export const TIMELINE_STORE = 'statuses'
export function createDbName(instanceName, timeline) { export function createTimelineDbName(instanceName, timeline) {
return `${OBJECT_STORE}_${instanceName}_${timeline}` return `${instanceName}_timeline_${timeline}`
} }
export function getDatabase(instanceName, timeline) { export function getTimelineDatabase(instanceName, timeline) {
const key = `${instanceName}_${timeline}` const key = `${instanceName}_${timeline}`
if (databaseCache[key]) { if (databaseCache[key]) {
return Promise.resolve(databaseCache[key]) return Promise.resolve(databaseCache[key])
} }
let dbName = createDbName(instanceName, timeline) let dbName = createTimelineDbName(instanceName, timeline)
keyval.get('known_dbs').then(knownDbs => { addKnownDb(instanceName, 'timeline', dbName)
knownDbs = knownDbs || {}
knownDbs[dbName] = [instanceName, timeline]
keyval.set('known_dbs', knownDbs)
})
databaseCache[key] = new Promise((resolve, reject) => { databaseCache[key] = new Promise((resolve, reject) => {
let req = indexedDB.open(dbName, 1) let req = indexedDB.open(dbName, 1)
@ -29,7 +25,7 @@ export function getDatabase(instanceName, timeline) {
} }
req.onupgradeneeded = () => { req.onupgradeneeded = () => {
let db = req.result; let db = req.result;
let oStore = db.createObjectStore(OBJECT_STORE, { let oStore = db.createObjectStore(TIMELINE_STORE, {
keyPath: 'id' keyPath: 'id'
}) })
oStore.createIndex('pinafore_id_as_negative_big_int', 'pinafore_id_as_negative_big_int') oStore.createIndex('pinafore_id_as_negative_big_int', 'pinafore_id_as_negative_big_int')

View file

@ -1,7 +1,7 @@
import { get } from '../ajax' import { get } from '../ajax'
import { basename } from './utils' import { basename } from './utils'
export function getThisUserAccount(instanceName, accessToken) { export function getVerifyCredentials(instanceName, accessToken) {
let url = `${basename(instanceName)}/api/v1/accounts/verify_credentials` let url = `${basename(instanceName)}/api/v1/accounts/verify_credentials`
return get(url, { return get(url, {
'Authorization': `Bearer ${accessToken}` 'Authorization': `Bearer ${accessToken}`

View file

@ -6,14 +6,14 @@
<SettingsLayout page='settings/instances/{{params.instanceName}}' label="{{params.instanceName}}"> <SettingsLayout page='settings/instances/{{params.instanceName}}' label="{{params.instanceName}}">
<h1 class="instance-name-h1">{{params.instanceName}}</h1> <h1 class="instance-name-h1">{{params.instanceName}}</h1>
{{#if instanceUserAccount}} {{#if verifyCredentials}}
<h2>Logged in as:</h2> <h2>Logged in as:</h2>
<div class="acct-current-user"> <div class="acct-current-user">
<img alt="Profile picture for {{'@' + instanceUserAccount.acct}}" <img alt="Profile picture for {{'@' + verifyCredentials.acct}}"
class="acct-avatar" src="{{instanceUserAccount.avatar}}" /> class="acct-avatar" src="{{verifyCredentials.avatar}}" />
<a class="acct-handle" rel="noopener" target="_blank" <a class="acct-handle" rel="noopener" target="_blank"
href="{{instanceUserAccount.url}}">{{'@' + instanceUserAccount.acct}}</a> href="{{verifyCredentials.url}}">{{'@' + verifyCredentials.acct}}</a>
<span class="acct-display-name">{{instanceUserAccount.display_name}}</span> <span class="acct-display-name">{{verifyCredentials.display_name}}</span>
</div> </div>
<h2>Theme:</h2> <h2>Theme:</h2>
<form class="theme-chooser" aria-label="Choose a theme"> <form class="theme-chooser" aria-label="Choose a theme">
@ -97,7 +97,8 @@
import { store } from '../../_utils/store' import { store } from '../../_utils/store'
import Layout from '../../_components/Layout.html' import Layout from '../../_components/Layout.html'
import SettingsLayout from '../_components/SettingsLayout.html' import SettingsLayout from '../_components/SettingsLayout.html'
import { getThisUserAccount } from '../../_utils/mastodon/user' import { getVerifyCredentials } from '../../_utils/mastodon/user'
import { database } from '../../_utils/database/database'
import { themes } from '../../_static/themes' import { themes } from '../../_static/themes'
import { switchToTheme } from '../../_utils/themeEngine' import { switchToTheme } from '../../_utils/themeEngine'
import { goto } from 'sapper/runtime.js' import { goto } from 'sapper/runtime.js'
@ -117,9 +118,18 @@
let loggedInInstances = this.store.get('loggedInInstances') let loggedInInstances = this.store.get('loggedInInstances')
let instanceThemes = this.store.get('instanceThemes') let instanceThemes = this.store.get('instanceThemes')
let instanceData = loggedInInstances[instanceName] let instanceData = loggedInInstances[instanceName]
let instanceUserAccount = await getThisUserAccount(instanceName, instanceData.access_token) let verifyCredentials = await database.getInstanceVerifyCredentials(instanceName)
if (verifyCredentials) { // update
getVerifyCredentials(instanceName, instanceData.access_token).then(verifyCredentials => {
database.setInstanceVerifyCredentials(instanceName, verifyCredentials)
})
} else {
verifyCredentials = await getVerifyCredentials(instanceName, instanceData.access_token)
database.setInstanceVerifyCredentials(instanceName, verifyCredentials)
verifyCredentials = verifyCredentials
}
this.set({ this.set({
instanceUserAccount: instanceUserAccount, verifyCredentials: verifyCredentials,
selectedTheme: instanceThemes[instanceName] selectedTheme: instanceThemes[instanceName]
}) })
}, },

View file

@ -72,11 +72,12 @@
import Layout from '../../_components/Layout.html'; import Layout from '../../_components/Layout.html';
import SettingsLayout from '../_components/SettingsLayout.html' import SettingsLayout from '../_components/SettingsLayout.html'
import { registerApplication, generateAuthLink, getAccessTokenFromAuthCode } from '../../_utils/mastodon/oauth' import { registerApplication, generateAuthLink, getAccessTokenFromAuthCode } from '../../_utils/mastodon/oauth'
import { getThisUserAccount } from '../../_utils/mastodon/user' import { getVerifyCredentials } from '../../_utils/mastodon/user'
import { store } from '../../_utils/store' import { store } from '../../_utils/store'
import { goto } from 'sapper/runtime.js' import { goto } from 'sapper/runtime.js'
import { switchToTheme } from '../../_utils/themeEngine' import { switchToTheme } from '../../_utils/themeEngine'
import LoadingMask from '../../_components/LoadingMask' import LoadingMask from '../../_components/LoadingMask'
import { database } from '../../_utils/database/database'
const REDIRECT_URI = (typeof location !== 'undefined' ? const REDIRECT_URI = (typeof location !== 'undefined' ?
location.origin : 'https://pinafore.social') + '/settings/instances/add' location.origin : 'https://pinafore.social') + '/settings/instances/add'
@ -177,7 +178,9 @@
this.store.save() this.store.save()
switchToTheme('default') switchToTheme('default')
// fire off request for account so it's cached in the SW // fire off request for account so it's cached in the SW
getThisUserAccount(currentRegisteredInstanceName, instanceData.access_token) getVerifyCredentials(currentRegisteredInstanceName, instanceData.access_token).then(verifyCredentials => {
database.setInstanceVerifyCredentials(currentRegisteredInstanceName, verifyCredentials)
})
goto('/') goto('/')
} }
} }

View file

@ -42,7 +42,6 @@ const NETWORK_ONLY = [
] ]
const CACHE_FIRST = [ const CACHE_FIRST = [
'/api/v1/accounts/verify_credentials',
'/system/accounts/avatars' '/system/accounts/avatars'
] ]