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",
|
"resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz",
|
||||||
"integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM="
|
"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": {
|
"randomatic": {
|
||||||
"version": "1.1.7",
|
"version": "1.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz",
|
||||||
|
|
|
@ -38,6 +38,7 @@
|
||||||
"npm-run-all": "^4.1.2",
|
"npm-run-all": "^4.1.2",
|
||||||
"performance-now": "^2.1.0",
|
"performance-now": "^2.1.0",
|
||||||
"pify": "^3.0.0",
|
"pify": "^3.0.0",
|
||||||
|
"quick-lru": "^1.1.0",
|
||||||
"requestidlecallback": "^0.3.0",
|
"requestidlecallback": "^0.3.0",
|
||||||
"rimraf": "^2.6.2",
|
"rimraf": "^2.6.2",
|
||||||
"sapper": "^0.3.2",
|
"sapper": "^0.3.2",
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
<:Window bind:online />
|
<:Window bind:online />
|
||||||
<div class="timeline" role="feed" aria-label="{{label}}" on:initialized>
|
<div class="timeline" role="feed" aria-label="{{label}}" on:initialized>
|
||||||
<VirtualList component="{{StatusListItem}}"
|
<VirtualList component="{{StatusListItem}}"
|
||||||
items="{{keyedStatuses}}"
|
:makeProps
|
||||||
|
:items
|
||||||
on:scrollToBottom="onScrollToBottom()"
|
on:scrollToBottom="onScrollToBottom()"
|
||||||
shown="{{initialized}}"
|
shown="{{initialized}}"
|
||||||
footerComponent="{{LoadingFooter}}"
|
footerComponent="{{LoadingFooter}}"
|
||||||
|
@ -42,16 +43,18 @@
|
||||||
data: () => ({
|
data: () => ({
|
||||||
StatusListItem: StatusListItem,
|
StatusListItem: StatusListItem,
|
||||||
LoadingFooter: LoadingFooter,
|
LoadingFooter: LoadingFooter,
|
||||||
statuses: [],
|
statusIds: [],
|
||||||
runningUpdate: false,
|
runningUpdate: false,
|
||||||
initialized: false
|
initialized: false
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
keyedStatuses: (statuses) => statuses.map(status => ({
|
makeProps: ($currentInstance) => (statusId) => database.getStatus($currentInstance, statusId),
|
||||||
props: status,
|
items: (statusIds) => {
|
||||||
key: status.id
|
return statusIds.map(statusId => ({
|
||||||
})),
|
key: statusId
|
||||||
lastStatusId: (statuses) => statuses.length && statuses[statuses.length - 1].id,
|
}))
|
||||||
|
},
|
||||||
|
lastStatusId: (statusIds) => statusIds.length && statusIds[statusIds.length - 1],
|
||||||
label: (timeline, $currentInstance) => {
|
label: (timeline, $currentInstance) => {
|
||||||
if (timelines[timeline]) {
|
if (timelines[timeline]) {
|
||||||
`${timelines[timeline].label} timeline for ${$currentInstance}`
|
`${timelines[timeline].label} timeline for ${$currentInstance}`
|
||||||
|
@ -89,12 +92,16 @@
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
console.log('addStatuses()')
|
console.log('addStatuses()')
|
||||||
}
|
}
|
||||||
let statuses = this.get('statuses')
|
let instanceName = this.store.get('instanceName')
|
||||||
if (!statuses) {
|
let timeline = this.get('timeline')
|
||||||
|
/* no await */ database.insertStatuses(instanceName, timeline, newStatuses)
|
||||||
|
let statusIds = this.get('statusIds')
|
||||||
|
if (!statusIds) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let merged = mergeStatuses(statuses, newStatuses)
|
let newStatusIds = newStatuses.map(status => status.id)
|
||||||
this.set({ statuses: merged })
|
let merged = mergeStatuses(statusIds, newStatusIds)
|
||||||
|
this.set({ statusIds: merged })
|
||||||
},
|
},
|
||||||
async fetchStatusesAndPossiblyFallBack() {
|
async fetchStatusesAndPossiblyFallBack() {
|
||||||
let online = this.get('online')
|
let online = this.get('online')
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
<!-- TODO: setting height is hacky, just make this element the scroller -->
|
<!-- TODO: setting height is hacky, just make this element the scroller -->
|
||||||
<div class="virtual-list {{shown ? '' : 'hidden'}}" style="height: {{$height}}px;">
|
<div class="virtual-list {{shown ? '' : 'hidden'}}" style="height: {{$height}}px;">
|
||||||
{{#each $visibleItems as item @key}}
|
{{#each $visibleItems as item @key}}
|
||||||
<VirtualListItem :component
|
<VirtualListLazyItem :component
|
||||||
offset="{{item.offset}}"
|
offset="{{item.offset}}"
|
||||||
props="{{item.props}}"
|
makeProps="{{makeProps}}"
|
||||||
key="{{item.key}}"
|
key="{{item.key}}"
|
||||||
index="{{item.index}}"
|
index="{{item.index}}"
|
||||||
/>
|
/>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{#if $showFooter}}
|
{{#if $showFooter}}
|
||||||
|
@ -19,7 +19,7 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
import VirtualListItem from './VirtualListItem'
|
import VirtualListLazyItem from './VirtualListLazyItem'
|
||||||
import VirtualListFooter from './VirtualListFooter.html'
|
import VirtualListFooter from './VirtualListFooter.html'
|
||||||
import { virtualListStore } from '../_utils/virtualListStore'
|
import { virtualListStore } from '../_utils/virtualListStore'
|
||||||
import throttle from 'lodash/throttle'
|
import throttle from 'lodash/throttle'
|
||||||
|
@ -62,7 +62,7 @@
|
||||||
}),
|
}),
|
||||||
store: () => virtualListStore,
|
store: () => virtualListStore,
|
||||||
components: {
|
components: {
|
||||||
VirtualListItem,
|
VirtualListLazyItem,
|
||||||
VirtualListFooter
|
VirtualListFooter
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
|
@ -2,7 +2,10 @@
|
||||||
virtual-list-key="{{key}}"
|
virtual-list-key="{{key}}"
|
||||||
ref:node
|
ref:node
|
||||||
style="transform: translateY({{offset}}px);" >
|
style="transform: translateY({{offset}}px);" >
|
||||||
<:Component {component} virtualProps="{{props}}" virtualIndex="{{index}}" virtualLength="{{$numItems}}"
|
<:Component {component}
|
||||||
|
virtualProps="{{props}}"
|
||||||
|
virtualIndex="{{index}}"
|
||||||
|
virtualLength="{{$numItems}}"
|
||||||
on:recalculateHeight="doRecalculateHeight()"/>
|
on:recalculateHeight="doRecalculateHeight()"/>
|
||||||
</div>
|
</div>
|
||||||
<style>
|
<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
|
STATUSES_STORE, ACCOUNTS_STORE
|
||||||
} from './constants'
|
} 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) {
|
export async function getTimeline(instanceName, timeline, maxId = null, limit = 20) {
|
||||||
const db = await getDatabase(instanceName, timeline)
|
const db = await getDatabase(instanceName, timeline)
|
||||||
return await dbPromise(db, [TIMELINE_STORE, STATUSES_STORE], 'readonly', (stores, callback) => {
|
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) {
|
export async function insertStatuses(instanceName, timeline, statuses) {
|
||||||
|
for (let status of statuses) {
|
||||||
|
statusesCache.set(status.id, status)
|
||||||
|
}
|
||||||
const db = await getDatabase(instanceName, timeline)
|
const db = await getDatabase(instanceName, timeline)
|
||||||
await dbPromise(db, [TIMELINE_STORE, STATUSES_STORE, ACCOUNTS_STORE], 'readwrite', (stores) => {
|
await dbPromise(db, [TIMELINE_STORE, STATUSES_STORE, ACCOUNTS_STORE], 'readwrite', (stores) => {
|
||||||
let [ timelineStore, statusesStore, accountsStore ] = stores
|
let [ timelineStore, statusesStore, accountsStore ] = stores
|
||||||
|
@ -86,3 +101,23 @@ export async function getAccount(instanceName, accountId) {
|
||||||
export async function clearDatabaseForInstance(instanceName) {
|
export async function clearDatabaseForInstance(instanceName) {
|
||||||
await deleteDatabase(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
|
// 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.
|
// 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 leftIndex = 0
|
||||||
let rightIndex = 0
|
let rightIndex = 0
|
||||||
let merged = []
|
let merged = []
|
||||||
while (leftIndex < leftStatuses.length || rightIndex < rightStatuses.length) {
|
while (leftIndex < leftStatusIds.length || rightIndex < rightStatusIds.length) {
|
||||||
if (leftIndex === leftStatuses.length) {
|
if (leftIndex === leftStatusIds.length) {
|
||||||
merged.push(rightStatuses[rightIndex])
|
merged.push(rightStatusIds[rightIndex])
|
||||||
rightIndex++
|
rightIndex++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (rightIndex === rightStatuses.length) {
|
if (rightIndex === rightStatusIds.length) {
|
||||||
merged.push(leftStatuses[leftIndex])
|
merged.push(leftStatusIds[leftIndex])
|
||||||
leftIndex++
|
leftIndex++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
let left = leftStatuses[leftIndex]
|
let left = leftStatusIds[leftIndex]
|
||||||
let right = rightStatuses[rightIndex]
|
let right = rightStatusIds[rightIndex]
|
||||||
if (right.id === left.id) {
|
if (right === left) {
|
||||||
merged.push(right.pinafore_stale ? left : right)
|
|
||||||
rightIndex++
|
rightIndex++
|
||||||
leftIndex++
|
leftIndex++
|
||||||
} else if (parseInt(right.id, 10) > parseInt(left.id, 10)) {
|
} else if (parseInt(right, 10) > parseInt(left, 10)) {
|
||||||
merged.push(right)
|
merged.push(right)
|
||||||
rightIndex++
|
rightIndex++
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -56,7 +56,7 @@ virtualListStore.compute('visibleItems',
|
||||||
let len = items.length
|
let len = items.length
|
||||||
let i = -1
|
let i = -1
|
||||||
while (++i < len) {
|
while (++i < len) {
|
||||||
let { props, key } = items[i]
|
let { key } = items[i]
|
||||||
let height = itemHeights[key] || 0
|
let height = itemHeights[key] || 0
|
||||||
let currentOffset = totalOffset
|
let currentOffset = totalOffset
|
||||||
totalOffset += height
|
totalOffset += height
|
||||||
|
@ -72,7 +72,6 @@ virtualListStore.compute('visibleItems',
|
||||||
}
|
}
|
||||||
visibleItems.push({
|
visibleItems.push({
|
||||||
offset: currentOffset,
|
offset: currentOffset,
|
||||||
props: props,
|
|
||||||
key: key,
|
key: key,
|
||||||
index: i
|
index: i
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue