lazily render statuses, use lru cache on top of idb
This commit is contained in:
parent
8555e9e4c1
commit
5f12322ac8
5
package-lock.json
generated
5
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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>
|
||||
|
|
22
routes/_components/VirtualListLazyItem.html
Normal file
22
routes/_components/VirtualListLazyItem.html
Normal 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>
|
|
@ -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
|
||||
|
@ -86,3 +101,23 @@ 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
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue