lazily render statuses, use lru cache on top of idb
This commit is contained in:
		
							parent
							
								
									8555e9e4c1
								
							
						
					
					
						commit
						5f12322ac8
					
				
					 9 changed files with 103 additions and 32 deletions
				
			
		
							
								
								
									
										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,9 +1,9 @@
 | 
			
		|||
<!-- 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
 | 
			
		||||
    <VirtualListLazyItem :component
 | 
			
		||||
                         offset="{{item.offset}}"
 | 
			
		||||
                     props="{{item.props}}"
 | 
			
		||||
                         makeProps="{{makeProps}}"
 | 
			
		||||
                         key="{{item.key}}"
 | 
			
		||||
                         index="{{item.index}}"
 | 
			
		||||
    />
 | 
			
		||||
| 
						 | 
				
			
			@ -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…
	
	Add table
		
		Reference in a new issue