lazily render statuses, use lru cache on top of idb

This commit is contained in:
Nolan Lawson 2018-01-23 18:15:14 -08:00
parent 8555e9e4c1
commit 5f12322ac8
9 changed files with 103 additions and 32 deletions

5
package-lock.json generated
View file

@ -5590,6 +5590,11 @@
"resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz",
"integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM="
},
"quick-lru": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz",
"integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g="
},
"randomatic": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz",

View file

@ -38,6 +38,7 @@
"npm-run-all": "^4.1.2",
"performance-now": "^2.1.0",
"pify": "^3.0.0",
"quick-lru": "^1.1.0",
"requestidlecallback": "^0.3.0",
"rimraf": "^2.6.2",
"sapper": "^0.3.2",

View file

@ -1,7 +1,8 @@
<:Window bind:online />
<div class="timeline" role="feed" aria-label="{{label}}" on:initialized>
<VirtualList component="{{StatusListItem}}"
items="{{keyedStatuses}}"
:makeProps
:items
on:scrollToBottom="onScrollToBottom()"
shown="{{initialized}}"
footerComponent="{{LoadingFooter}}"
@ -42,16 +43,18 @@
data: () => ({
StatusListItem: StatusListItem,
LoadingFooter: LoadingFooter,
statuses: [],
statusIds: [],
runningUpdate: false,
initialized: false
}),
computed: {
keyedStatuses: (statuses) => statuses.map(status => ({
props: status,
key: status.id
})),
lastStatusId: (statuses) => statuses.length && statuses[statuses.length - 1].id,
makeProps: ($currentInstance) => (statusId) => database.getStatus($currentInstance, statusId),
items: (statusIds) => {
return statusIds.map(statusId => ({
key: statusId
}))
},
lastStatusId: (statusIds) => statusIds.length && statusIds[statusIds.length - 1],
label: (timeline, $currentInstance) => {
if (timelines[timeline]) {
`${timelines[timeline].label} timeline for ${$currentInstance}`
@ -89,12 +92,16 @@
if (process.env.NODE_ENV !== 'production') {
console.log('addStatuses()')
}
let statuses = this.get('statuses')
if (!statuses) {
let instanceName = this.store.get('instanceName')
let timeline = this.get('timeline')
/* no await */ database.insertStatuses(instanceName, timeline, newStatuses)
let statusIds = this.get('statusIds')
if (!statusIds) {
return
}
let merged = mergeStatuses(statuses, newStatuses)
this.set({ statuses: merged })
let newStatusIds = newStatuses.map(status => status.id)
let merged = mergeStatuses(statusIds, newStatusIds)
this.set({ statusIds: merged })
},
async fetchStatusesAndPossiblyFallBack() {
let online = this.get('online')

View file

@ -1,11 +1,11 @@
<!-- TODO: setting height is hacky, just make this element the scroller -->
<div class="virtual-list {{shown ? '' : 'hidden'}}" style="height: {{$height}}px;">
{{#each $visibleItems as item @key}}
<VirtualListItem :component
offset="{{item.offset}}"
props="{{item.props}}"
key="{{item.key}}"
index="{{item.index}}"
<VirtualListLazyItem :component
offset="{{item.offset}}"
makeProps="{{makeProps}}"
key="{{item.key}}"
index="{{item.index}}"
/>
{{/each}}
{{#if $showFooter}}
@ -19,7 +19,7 @@
}
</style>
<script>
import VirtualListItem from './VirtualListItem'
import VirtualListLazyItem from './VirtualListLazyItem'
import VirtualListFooter from './VirtualListFooter.html'
import { virtualListStore } from '../_utils/virtualListStore'
import throttle from 'lodash/throttle'
@ -62,7 +62,7 @@
}),
store: () => virtualListStore,
components: {
VirtualListItem,
VirtualListLazyItem,
VirtualListFooter
},
computed: {

View file

@ -2,7 +2,10 @@
virtual-list-key="{{key}}"
ref:node
style="transform: translateY({{offset}}px);" >
<:Component {component} virtualProps="{{props}}" virtualIndex="{{index}}" virtualLength="{{$numItems}}"
<:Component {component}
virtualProps="{{props}}"
virtualIndex="{{index}}"
virtualLength="{{$numItems}}"
on:recalculateHeight="doRecalculateHeight()"/>
</div>
<style>

View file

@ -0,0 +1,22 @@
{{#if props}}
<VirtualListItem :component
:offset
:props
:key
:index
/>
{{/if}}
<script>
import VirtualListItem from './VirtualListItem'
export default {
async oncreate() {
let makeProps = this.get('makeProps')
let key = this.get('key')
let props = await makeProps(key)
this.set({ props: props })
},
components: {
VirtualListItem
}
}
</script>

View file

@ -13,6 +13,18 @@ import {
STATUSES_STORE, ACCOUNTS_STORE
} from './constants'
import QuickLRU from 'quick-lru'
const statusesCache = new QuickLRU({maxSize: 100})
if (process.browser && process.env.NODE_ENV !== 'production') {
window.cacheStats = {
cache: statusesCache,
cacheHits: 0,
cacheMisses: 0
}
}
export async function getTimeline(instanceName, timeline, maxId = null, limit = 20) {
const db = await getDatabase(instanceName, timeline)
return await dbPromise(db, [TIMELINE_STORE, STATUSES_STORE], 'readonly', (stores, callback) => {
@ -37,6 +49,9 @@ export async function getTimeline(instanceName, timeline, maxId = null, limit =
}
export async function insertStatuses(instanceName, timeline, statuses) {
for (let status of statuses) {
statusesCache.set(status.id, status)
}
const db = await getDatabase(instanceName, timeline)
await dbPromise(db, [TIMELINE_STORE, STATUSES_STORE, ACCOUNTS_STORE], 'readwrite', (stores) => {
let [ timelineStore, statusesStore, accountsStore ] = stores
@ -85,4 +100,24 @@ export async function getAccount(instanceName, accountId) {
export async function clearDatabaseForInstance(instanceName) {
await deleteDatabase(instanceName)
}
export async function getStatus(instanceName, statusId) {
if (statusesCache.has(statusId)) {
if (process.browser && process.env.NODE_ENV !== 'production') {
window.cacheStats.cacheHits++
}
return statusesCache.get(statusId)
}
const db = await getDatabase(instanceName)
let result = await dbPromise(db, STATUSES_STORE, 'readonly', (store, callback) => {
store.get(statusId).onsuccess = (e) => {
callback(e.target.result && e.target.result)
}
})
statusesCache.set(statusId, result)
if (process.browser && process.env.NODE_ENV !== 'production') {
window.cacheStats.cacheMisses++
}
return result
}

View file

@ -1,27 +1,26 @@
// Merge two lists of statuses for the same timeline, e.g. one from IDB
// and another from the network. In case of duplicates, prefer the fresh.
export function mergeStatuses(leftStatuses, rightStatuses) {
export function mergeStatuses(leftStatusIds, rightStatusIds) {
let leftIndex = 0
let rightIndex = 0
let merged = []
while (leftIndex < leftStatuses.length || rightIndex < rightStatuses.length) {
if (leftIndex === leftStatuses.length) {
merged.push(rightStatuses[rightIndex])
while (leftIndex < leftStatusIds.length || rightIndex < rightStatusIds.length) {
if (leftIndex === leftStatusIds.length) {
merged.push(rightStatusIds[rightIndex])
rightIndex++
continue
}
if (rightIndex === rightStatuses.length) {
merged.push(leftStatuses[leftIndex])
if (rightIndex === rightStatusIds.length) {
merged.push(leftStatusIds[leftIndex])
leftIndex++
continue
}
let left = leftStatuses[leftIndex]
let right = rightStatuses[rightIndex]
if (right.id === left.id) {
merged.push(right.pinafore_stale ? left : right)
let left = leftStatusIds[leftIndex]
let right = rightStatusIds[rightIndex]
if (right === left) {
rightIndex++
leftIndex++
} else if (parseInt(right.id, 10) > parseInt(left.id, 10)) {
} else if (parseInt(right, 10) > parseInt(left, 10)) {
merged.push(right)
rightIndex++
} else {

View file

@ -56,7 +56,7 @@ virtualListStore.compute('visibleItems',
let len = items.length
let i = -1
while (++i < len) {
let { props, key } = items[i]
let { key } = items[i]
let height = itemHeights[key] || 0
let currentOffset = totalOffset
totalOffset += height
@ -72,7 +72,6 @@ virtualListStore.compute('visibleItems',
}
visibleItems.push({
offset: currentOffset,
props: props,
key: key,
index: i
})